From a2f189df10e93fc1d47a428b2daa760f6478e9ac Mon Sep 17 00:00:00 2001 From: vtroc7 Date: Sat, 10 Oct 2026 23:01:34 +0300 Subject: [PATCH] Scope match lifetimes and restore gameplay and menu presentation Prepare progression and loading through Bevy scheduling, preserve round HUD and bombsite props, restore grenade and T5 shadow presentation, and keep menus usable while classes load. Report update progress and continue startup after update failures. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 - Cargo.lock | 1075 +++++++++++------ Cargo.toml | 8 +- THIRD-PARTY-LICENSES.txt | 621 ++++------ crates/approved_tests/README.md | 13 +- crates/approved_tests/src/main.rs | 35 +- crates/approved_tests/src/report.rs | 51 + .../src/scenarios/heavy_gameplay_lifecycle.rs | 37 + .../src/scenarios/master_duo_chaos.rs | 4 +- crates/asset_core/src/lib.rs | 2 + crates/asset_core/src/map_content.rs | 90 ++ crates/asset_game/Cargo.toml | 1 + crates/asset_game/src/menu_catalog.rs | 9 + crates/asset_game/src/weapon_catalog.rs | 2 + .../asset_game/src/weapon_catalog/editor.rs | 19 + .../src/weapon_catalog/presentation.rs | 2 + .../src/weapon_catalog/publication.rs | 2 + .../asset_game/src/weapon_catalog/registry.rs | 233 ++++ .../src/material_compile/technique.rs | 4 + .../src/material_compile/technique/t5.rs | 3 + .../src/material_images/materialization.rs | 12 +- crates/asset_material/src/t5_tech_map.rs | 1 + crates/asset_material/src/t6_techset.rs | 38 +- crates/asset_world/src/glass_catalog.rs | 124 +- crates/assets/src/lane/iw4.rs | 8 + crates/assets/src/lane/sink.rs | 5 + crates/assets/src/lane/t5.rs | 1 + crates/assets/src/lane/t6.rs | 34 +- .../src/lane/t6/common_compile/models.rs | 5 +- crates/assets/src/lib.rs | 1 - crates/assets/src/match_load.rs | 53 +- crates/assets/src/plugin.rs | 2 - crates/assets/src/prepared.rs | 7 +- .../assets/src/session_load/common_cache.rs | 119 +- .../assets/src/session_load/common_walks.rs | 35 +- crates/assets/src/session_load/match_walk.rs | 3 + crates/assets/src/session_load/mod.rs | 3 + crates/assets/src/teardown.rs | 58 - crates/audio/src/ambient.rs | 146 ++- crates/audio/src/backend.rs | 44 +- crates/audio/src/breath.rs | 7 +- crates/audio/src/clip_store.rs | 3 - crates/audio/src/cue.rs | 35 +- crates/audio/src/cue_execution.rs | 2 + crates/audio/src/destructible_loops.rs | 5 +- crates/audio/src/emit.rs | 1 + crates/audio/src/entity_events.rs | 39 +- crates/audio/src/event.rs | 6 +- crates/audio/src/frontend.rs | 70 +- crates/audio/src/lib.rs | 4 +- crates/audio/src/match_set.rs | 58 +- crates/audio/src/messages.rs | 4 +- crates/audio/src/playback.rs | 107 +- crates/audio/src/rumble.rs | 10 +- crates/audio/src/runtime.rs | 3 + crates/audio/src/script_ambient.rs | 6 +- crates/audio/src/script_mix.rs | 16 +- crates/audio/src/script_music.rs | 22 +- crates/audio/src/shellshock.rs | 8 +- crates/bootstrap/src/bench/facts.rs | 14 +- crates/bootstrap/src/bench/milestones.rs | 6 +- crates/bootstrap/src/launch.rs | 94 +- crates/bootstrap/src/plugins.rs | 20 +- crates/bots/src/plugin.rs | 61 +- crates/clipmap_iw4/src/world_trace.rs | 13 +- crates/console/Cargo.toml | 1 + crates/console/src/barracks_menu.rs | 8 +- crates/console/src/class_dispatch.rs | 5 +- crates/console/src/class_menu.rs | 179 ++- crates/console/src/diagnostics.rs | 2 +- crates/console/src/feature_dispatch/mod.rs | 6 +- crates/console/src/feature_dispatch/replay.rs | 8 +- .../console/src/feature_dispatch/session.rs | 23 +- .../src/feature_dispatch/state_dump.rs | 58 +- crates/console/src/feature_dispatch/ui.rs | 2 +- crates/console/src/frontend.rs | 22 +- crates/console/src/gamepad.rs | 2 +- crates/console/src/local_account.rs | 93 ++ crates/console/src/plugin/dispatch_state.rs | 6 + crates/console/src/plugin/mod.rs | 159 ++- crates/console/src/user_settings.rs | 8 +- crates/console/src/weapon_dispatch.rs | 219 ++-- crates/d3d9_sm3/src/abi.rs | 2 + crates/d3d9_sm3/src/wgsl.rs | 7 +- crates/dpvs_iw4/src/walk.rs | 5 - crates/dxbc_sm5/src/wgsl.rs | 2 +- crates/entity_iw4/Cargo.toml | 1 + crates/entity_iw4/src/glass.rs | 7 +- crates/frame/Cargo.toml | 2 +- crates/frame/src/lib.rs | 17 +- crates/frame/src/scope.rs | 604 +++++++++ crates/frame/src/session.rs | 81 +- crates/fx/src/draw.rs | 32 +- crates/fx/src/elem.rs | 38 +- crates/fx/src/motion.rs | 45 +- crates/fx/src/sort.rs | 10 +- crates/fx/src/spawn.rs | 30 +- crates/fx/src/update.rs | 55 +- crates/fx_iw4/src/integrate.rs | 43 +- crates/fx_iw4/src/lib.rs | 10 +- crates/fx_iw4/src/random.rs | 23 + crates/fx_iw4/src/velocity.rs | 34 +- crates/gamemode_iw4/src/lib.rs | 1 + crates/gamemode_iw4/src/progression.rs | 129 ++ crates/hud/Cargo.toml | 1 + crates/hud/src/lib.rs | 2 +- crates/hud/src/menus/host.rs | 105 +- crates/hud/src/menus/mod.rs | 116 +- crates/hud/src/menus/runtime.rs | 8 + crates/hud/src/overhead_names.rs | 6 +- crates/hud/src/playercard.rs | 3 + crates/hud/src/plugin.rs | 96 +- crates/hud/src/reticle.rs | 7 +- crates/launcher/src/main.rs | 10 +- crates/net/src/authority/runtime.rs | 137 +-- crates/net/src/client/fire_verdict.rs | 12 +- crates/net/src/client/input.rs | 1 - crates/net/src/client/predict.rs | 14 - .../net/src/client/presentation/entities.rs | 15 +- .../presentation/entity_event_dispatch.rs | 26 +- crates/net/src/client/runtime.rs | 205 ++-- crates/net/src/lib.rs | 2 +- crates/net/src/plugin.rs | 17 +- crates/net/src/role_matrix.rs | 16 +- crates/net/src/session_core.rs | 13 +- crates/net/src/signon.rs | 73 +- crates/net/src/transport/master.rs | 4 +- crates/net/src/transport/meta_wire.rs | 3 + crates/playerstate_iw4/src/lib.rs | 12 +- crates/playerstate_iw4/src/playerstate.rs | 5 + crates/render/Cargo.toml | 2 +- crates/render/src/diag/acceptance.rs | 11 +- crates/render/src/diag/capture.rs | 8 +- crates/render/src/extract.rs | 6 +- crates/render/src/plugin.rs | 4 +- crates/render_anim/src/anim/fpv.rs | 9 +- .../render_anim/src/anim/remote_animation.rs | 8 + .../src/anim/viewmodel_controller.rs | 15 +- crates/render_anim/src/gaps.rs | 7 +- crates/render_anim/src/occupancy/dyn_ent.rs | 32 +- .../render_anim/src/occupancy/dyn_ent_phys.rs | 11 +- .../render_anim/src/occupancy/fpv_present.rs | 80 +- crates/render_anim/src/occupancy/held_sync.rs | 21 +- crates/render_anim/src/occupancy/item.rs | 23 +- .../render_anim/src/occupancy/match_reset.rs | 70 -- crates/render_anim/src/occupancy/missile.rs | 29 +- crates/render_anim/src/occupancy/mod.rs | 1 - .../render_anim/src/occupancy/remote_body.rs | 75 +- .../render_anim/src/occupancy/script_model.rs | 34 +- crates/render_anim/src/plugin.rs | 40 +- crates/render_frontend/Cargo.toml | 2 +- .../src/adapters/anim/dyn_ent.rs | 1 + .../src/adapters/anim/dyn_ent_brush.rs | 22 +- .../src/adapters/anim/dyn_ent_wake.rs | 11 +- .../render_frontend/src/adapters/anim/mod.rs | 4 +- .../render_frontend/src/adapters/fx/system.rs | 76 +- .../src/adapters/motion_tracker.rs | 3 +- .../src/assemble/drawsurf/dof.rs | 1 + .../src/assemble/drawsurf/film_vision_view.rs | 1 + .../src/assemble/drawsurf/fog.rs | 8 +- .../src/assemble/drawsurf/frame_products.rs | 4 +- .../src/assemble/drawsurf/material_admit.rs | 14 +- .../src/assemble/drawsurf/material_compile.rs | 1 + .../src/assemble/drawsurf/material_runtime.rs | 3 + .../drawsurf/retained_list/static_lane.rs | 8 +- .../src/assemble/drawsurf/sm3_wgsl.rs | 1 + .../src/assemble/drawsurf/tess/glass.rs | 3 - .../src/assemble/drawsurf/tess/sky.rs | 18 +- .../src/assemble/drawsurf/tess/smodel.rs | 3 +- .../src/assemble/drawsurf/tess/world.rs | 3 +- .../src/assemble/drawsurf/tess/xmodel.rs | 4 - .../src/assemble/drawsurf/wgsl_disk_cache.rs | 2 +- .../src/assemble/match_reset.rs | 46 - crates/render_frontend/src/assemble/mod.rs | 19 +- crates/render_frontend/src/prepare/mod.rs | 166 ++- .../src/prepare/scene/cell_frustum_cmds.rs | 8 +- .../render_frontend/src/prepare/scene/cull.rs | 4 +- .../src/prepare/scene/spawn.rs | 303 +++-- .../src/prepare/scene/world_gpu.rs | 44 +- .../src/prepare/scene/world_images.rs | 28 +- .../src/prepare/scene/world_occupancy.rs | 62 +- .../src/prepare/scene/world_plan.rs | 28 +- .../src/prepare/worker_cmds.rs | 5 - crates/render_fx/src/combat.rs | 2 +- crates/render_fx/src/fire_occurrence.rs | 13 +- crates/render_fx/src/plugin.rs | 39 +- crates/render_fx/src/present.rs | 4 - crates/render_fx/src/system.rs | 50 +- crates/render_fx/src/tracer.rs | 2 +- .../src/drawsurf/colour_submit/geometry.rs | 2 +- .../src/drawsurf/colour_submit/mod.rs | 23 +- .../src/drawsurf/colour_submit/publication.rs | 4 +- .../render_gpu/src/drawsurf/exact_pipeline.rs | 16 +- crates/render_gpu/src/drawsurf/floatz.rs | 6 +- .../src/drawsurf/{floatz.wgsl => floatz.wesl} | 5 +- .../src/drawsurf/geometry_diagnostic.rs | 6 +- ...agnostic.wgsl => geometry_diagnostic.wesl} | 2 +- .../render_gpu/src/drawsurf/gpu_resources.rs | 4 +- crates/render_gpu/src/drawsurf/hud_blood.rs | 2 + crates/render_gpu/src/drawsurf/iw_tess.rs | 2 + crates/render_gpu/src/drawsurf/postfx.rs | 2 + crates/render_gpu/src/drawsurf/scene_depth.rs | 22 +- .../src/drawsurf/shellshock_screen.rs | 1 + crates/render_gpu/src/drawsurf/sun_effects.rs | 6 +- .../{sun_effects.wgsl => sun_effects.wesl} | 5 +- crates/render_gpu/src/plugin.rs | 58 +- crates/render_material/src/catalog.rs | 6 + crates/render_material/src/dxbc_abi.rs | 1 + crates/render_material/src/program_abi.rs | 1 + crates/render_material/src/sm3_abi.rs | 4 + crates/render_scene/Cargo.toml | 1 + crates/render_scene/src/camera.rs | 1 + crates/render_scene/src/dpvs_cells.rs | 1 + crates/render_scene/src/plugin.rs | 27 +- crates/render_scene/src/view_parms.rs | 2 +- crates/replay/src/plugin.rs | 22 +- crates/session/src/admission.rs | 17 +- crates/session/src/lib.rs | 5 +- crates/session/src/life_front.rs | 10 +- crates/session/src/lifecycle.rs | 156 +-- crates/session/src/local_arm.rs | 27 +- crates/session/src/match_apply.rs | 220 ++-- crates/session/src/plugin.rs | 1 + crates/session/src/readiness.rs | 13 +- crates/session/src/scope_control.rs | 182 +++ crates/session/src/weapon_content.rs | 7 +- crates/sim/src/carrier.rs | 71 +- crates/sim/src/combat.rs | 1 + crates/sim/src/content.rs | 83 +- crates/sim/src/damage.rs | 2 +- crates/sim/src/equipment.rs | 11 +- crates/sim/src/frame.rs | 225 +++- crates/sim/src/item.rs | 6 +- crates/sim/src/lib.rs | 2 + crates/sim/src/match_state/loadout.rs | 2 +- crates/sim/src/match_state/snapshot_meta.rs | 1 + crates/sim/src/persistent_data.rs | 80 ++ crates/sim/src/progression.rs | 399 ++++++ crates/sim/src/script/host/arrays.rs | 8 +- crates/sim/src/script/host/audio.rs | 8 +- crates/sim/src/script/host/client_effects.rs | 4 +- crates/sim/src/script/host/controls.rs | 37 +- crates/sim/src/script/host/entities.rs | 11 +- crates/sim/src/script/host/entity_damage.rs | 42 +- crates/sim/src/script/host/guidance.rs | 12 +- crates/sim/src/script/host/hud.rs | 24 +- crates/sim/src/script/host/iw4_gametype.rs | 6 +- crates/sim/src/script/host/mechanics.rs | 67 +- crates/sim/src/script/host/natives/engine.rs | 121 +- crates/sim/src/script/host/natives/iw4.rs | 59 +- crates/sim/src/script/host/natives/math.rs | 8 +- crates/sim/src/script/host/natives/player.rs | 78 +- .../src/script/host/natives/scene_effects.rs | 8 +- crates/sim/src/script/host/natives/skill.rs | 51 +- crates/sim/src/script/host/natives/t5.rs | 54 +- crates/sim/src/script/host/objectives.rs | 28 +- crates/sim/src/script/host/physics.rs | 14 +- crates/sim/src/script/host/players.rs | 187 +-- crates/sim/src/script/host/presence.rs | 35 +- crates/sim/src/script/host/registry.rs | 10 +- crates/sim/src/script/host/restart.rs | 48 +- crates/sim/src/script/host/rumble.rs | 6 +- .../src/script/host/sentry_placement/mod.rs | 2 +- crates/sim/src/script/host/spectators.rs | 15 +- crates/sim/src/script/host/tables.rs | 12 +- crates/sim/src/script/host/trace.rs | 8 +- crates/sim/src/script/host/triggers.rs | 86 +- crates/sim/src/script/host/turrets.rs | 46 +- crates/sim/src/script/host/vehicles.rs | 70 +- crates/sim/src/script/host/weapons.rs | 69 +- crates/sim/src/script/mod.rs | 4 +- crates/sim/src/script/runtime/lifecycle.rs | 27 +- crates/sim/src/script/runtime/mod.rs | 196 +-- crates/sim/src/script/runtime/state.rs | 154 ++- crates/sim/src/script_player.rs | 28 +- crates/sim/src/skill_rating.rs | 20 + crates/sim/src/step.rs | 29 +- crates/sim/src/weapon_content.rs | 13 + crates/sim/src/world.rs | 44 +- crates/sim/src/world/content.rs | 14 + crates/ui/menus/classes.json | 26 + crates/ui/menus/frontend.json | 9 +- crates/ui/src/classes/equip_txn.rs | 100 +- crates/ui/src/classes/select.rs | 11 +- crates/ui/src/classes/setup.rs | 47 + crates/ui/src/classes/store.rs | 14 +- crates/ui/src/launch_report.rs | 6 +- crates/ui/src/menu_load.rs | 11 +- crates/updater/Cargo.toml | 1 + crates/updater/src/lib.rs | 405 +++++-- crates/weapon_iw4/src/sprint.rs | 9 +- crates/weapon_iw4/src/tick.rs | 2 + docs/ENTITIES.md | 6 +- docs/GSC-RUNTIME.md | 4 +- docs/MAP-LOAD.md | 23 +- docs/PRESENTATION.md | 10 +- docs/RUN.md | 16 + docs/WINDOWS.md | 9 +- xtask/src/publish.rs | 73 +- xtask/src/publish_check.rs | 82 +- xtask/src/server.rs | 1 + 301 files changed, 8259 insertions(+), 4037 deletions(-) create mode 100644 crates/asset_core/src/map_content.rs delete mode 100644 crates/assets/src/teardown.rs create mode 100644 crates/frame/src/scope.rs create mode 100644 crates/gamemode_iw4/src/progression.rs delete mode 100644 crates/render_anim/src/occupancy/match_reset.rs delete mode 100644 crates/render_frontend/src/assemble/match_reset.rs rename crates/render_gpu/src/drawsurf/{floatz.wgsl => floatz.wesl} (96%) rename crates/render_gpu/src/drawsurf/{geometry_diagnostic.wgsl => geometry_diagnostic.wesl} (98%) rename crates/render_gpu/src/drawsurf/{sun_effects.wgsl => sun_effects.wesl} (99%) create mode 100644 crates/session/src/scope_control.rs create mode 100644 crates/sim/src/progression.rs diff --git a/.gitignore b/.gitignore index b5983eb..4cf483f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ iw4l.community.*.tmp # the agent clones and the RE tooling and its bases all stay on this machine. /context/ -# Nix / direnv .direnv/ result result-* diff --git a/Cargo.lock b/Cargo.lock index e1b3995..0c0c9e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,16 +27,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "accesskit_consumer" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" -dependencies = [ - "accesskit", - "hashbrown 0.16.1", -] - [[package]] name = "accesskit_consumer" version = "0.38.0" @@ -47,6 +37,20 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "accesskit_ios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "750c4e9f6ce888dfe8a10c0f1b5ceb646a7854fd46579d74919219d1bb314083" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", +] + [[package]] name = "accesskit_macos" version = "0.26.3" @@ -54,7 +58,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" dependencies = [ "accesskit", - "accesskit_consumer 0.38.0", + "accesskit_consumer", "hashbrown 0.16.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", @@ -63,12 +67,12 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.32.1" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1" dependencies = [ "accesskit", - "accesskit_consumer 0.35.0", + "accesskit_consumer", "hashbrown 0.16.1", "static_assertions", "windows", @@ -77,11 +81,12 @@ dependencies = [ [[package]] name = "accesskit_winit" -version = "0.32.2" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" +checksum = "d5b41e63a69f36d9f1f41e70464c7e5f72eee485ef26aab19f0b4f86e6c0a84c" dependencies = [ "accesskit", + "accesskit_ios", "accesskit_macos", "accesskit_windows", "raw-window-handle", @@ -191,13 +196,74 @@ dependencies = [ "libm", ] +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "approved_tests" version = "0.1.0" dependencies = [ "asset_transport", "serde_json", - "toml", + "toml 0.9.12+spec-1.1.0", "updater", ] @@ -233,6 +299,7 @@ dependencies = [ "parking_lot", "percent-encoding", "windows-sys 0.59.0", + "wl-clipboard-rs", "x11rb", ] @@ -345,6 +412,7 @@ dependencies = [ "fastfile_t5", "fastfile_t6", "fx_iw4", + "gamemode_iw4", "serde", "serde_json", "structured_data_iw4", @@ -698,9 +766,9 @@ checksum = "f09c37bc0e9f0924b7dae9988265ef3c76c88538f41a3b06caf4bed07cee5226" [[package]] name = "bevy" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "950238d3049d81006a6fd6b898a34cfc01bb5462a7668795a54d640aed19fd0a" +checksum = "8bd0e4ea5ec677698955f9f285221eab78606d8c27b57b889def199c16ce36e6" dependencies = [ "bevy_internal", "tracing", @@ -708,9 +776,9 @@ dependencies = [ [[package]] name = "bevy_a11y" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86f2cb45b000a9d121a5a7606f9af4a49e72b8b6a3885dc2acac2f0897a04f1" +checksum = "f1070b9b7f087a5073be2da713367f29a831c85cd3391afcfc95bc0498125704" dependencies = [ "accesskit", "bevy_app", @@ -721,18 +789,18 @@ dependencies = [ [[package]] name = "bevy_android" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e5e2a949be318bf7184eb084622892afcc1e8b2af44973ac53ab53201756e0" +checksum = "17fd56a4708085b36f8aa1a011e347f8bd9334f0343bb3d5d9c3742897968a55" dependencies = [ "android-activity", ] [[package]] name = "bevy_anti_alias" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815414ea2e11afe1f2396d89279cfa0986907ef3ec673ec936d9fa695b2bfbf9" +checksum = "f4df27fadf88e6ff72bf63a5e80e3921536eabfcde3196db347c31b6bbb39606" dependencies = [ "bevy_app", "bevy_asset", @@ -741,6 +809,7 @@ dependencies = [ "bevy_derive", "bevy_diagnostic", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_math", "bevy_reflect", @@ -752,9 +821,9 @@ dependencies = [ [[package]] name = "bevy_app" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671ae6f3a8d84f9ed696c531a138558596e041231a8414b6efeafa8469e841f9" +checksum = "a2489da2972d57cccd19060d7faa44c4c4cfb296a657c84ee6ab8bde2b1c0115" dependencies = [ "bevy_derive", "bevy_ecs", @@ -775,9 +844,9 @@ dependencies = [ [[package]] name = "bevy_asset" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a05907de6a362d2c609a7a1d1b4dca3b58b768f746b719ca1a3c7dfa87d391" +checksum = "b88b3767a2c3f5a017b2febca08a670819ea2acb46846e4d8cd02980aeb0ed3e" dependencies = [ "async-broadcast", "async-channel", @@ -818,9 +887,9 @@ dependencies = [ [[package]] name = "bevy_asset_macros" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9444998cc37b51dfd1572c23a501865733ea65619a7d88b47aa654ae2290a4f" +checksum = "875b4fd236612744dbd61fb0a77ffde0aee37b8b780cf8a89caa308511c70646" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -830,9 +899,9 @@ dependencies = [ [[package]] name = "bevy_camera" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20105c3e77c2fb391bf3ded3a473d60e4058b837c9bf4e3412225833e524c75a" +checksum = "f32c0d8436fb9373dc3b64fa30c3dec51d6a7cfda61de3c70fd1f62f8a903887" dependencies = [ "bevy_app", "bevy_asset", @@ -843,6 +912,7 @@ dependencies = [ "bevy_math", "bevy_mesh", "bevy_reflect", + "bevy_shape", "bevy_transform", "bevy_utils", "bevy_window", @@ -856,9 +926,9 @@ dependencies = [ [[package]] name = "bevy_clipboard" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dc5fb913f25c5a16a2a1aa2942ba3f1365a6d0f2db3c0ea5bc0be01f85dca5d" +checksum = "f0267b0beafd7e587a1b25b5ce14b271577f5fc1d8fd7828b2b66f0cf5dd649b" dependencies = [ "arboard", "bevy_app", @@ -872,10 +942,11 @@ dependencies = [ [[package]] name = "bevy_color" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2dfc0091be6eb62fec9158b2d3e6f21b002be7bdb42dd3b8322e949c8426b0" +checksum = "c0e57bfc1a0da1c018235fb98bb0e15ab61f1d0253122020ab6e1450e6cfbcf9" dependencies = [ + "bevy_curve", "bevy_math", "bevy_reflect", "bytemuck", @@ -888,9 +959,9 @@ dependencies = [ [[package]] name = "bevy_core_pipeline" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885d640bbff6c4fa3beadbb043d0d1ae53b2fda864da18a2bf2670fb90428865" +checksum = "a4a077b301be4b373a4cb224c54988e1f8385ecf3ab9e60bd3727a65b3d6901a" dependencies = [ "bevy_app", "bevy_asset", @@ -899,6 +970,7 @@ dependencies = [ "bevy_derive", "bevy_diagnostic", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_light", "bevy_log", @@ -916,10 +988,25 @@ dependencies = [ ] [[package]] -name = "bevy_derive" -version = "0.19.0" +name = "bevy_curve" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293df2b2f7ed65ef2ce02b2e7359faac1a9a92a8cc96c182c9de77eb06049ae0" +checksum = "21ae28ed95c1c8a1bfc8930471840ee5a4b99b6b31138b17598e7ee6f4291b64" +dependencies = [ + "approx", + "bevy_math", + "bevy_reflect", + "itertools 0.14.0", + "serde", + "thiserror 2.0.19", + "variadics_please", +] + +[[package]] +name = "bevy_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd59e9e600d792e60df17b4d52719b28c288bf047884ac89ad29d9c7a6592d2c" dependencies = [ "bevy_macro_utils", "quote", @@ -928,9 +1015,9 @@ dependencies = [ [[package]] name = "bevy_diagnostic" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a73e11555051f37c3e0e9ef7775e77108f2482310242996fb5bb6ef8b685b78e" +checksum = "7f8851dc1b253b350bf941695457007e6e44ad4b8f075bf52a1ef202e6866b6a" dependencies = [ "atomic-waker", "bevy_app", @@ -945,9 +1032,9 @@ dependencies = [ [[package]] name = "bevy_ecs" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4654b9b3535fa7813d2878a50e1b5ad80a361dc1c913b96ded57667f65d70a01" +checksum = "d5541a1658c5029b39e3ec59c6ecb47ad0a6d85fa083312a5ee2a95d6982a926" dependencies = [ "arrayvec", "bevy_ecs_macros", @@ -964,6 +1051,7 @@ dependencies = [ "indexmap", "log", "nonmax", + "rand", "serde", "slotmap", "smallvec", @@ -974,9 +1062,9 @@ dependencies = [ [[package]] name = "bevy_ecs_macro_logic" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee0b98a74141ce8dce4654c60020ebbaefab32646f42af6bbae55f282ae65ff7" +checksum = "5197fea7643d2733e8b6d81c3dfa99adad3a754cdfd159c00255cea157b795d7" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -986,9 +1074,9 @@ dependencies = [ [[package]] name = "bevy_ecs_macros" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc958a194d226d618404e0fea99a3c8b4146207a00a3ba07fa712bf71607240" +checksum = "3dde5305169503c9101404bf8cf17e8351f56fccf5152b8e5d7d7c32dcf1c486" dependencies = [ "bevy_ecs_macro_logic", "bevy_macro_utils", @@ -999,19 +1087,49 @@ dependencies = [ [[package]] name = "bevy_encase_derive" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dc5bf9e6cb46c4401a66625b5f7eb9654a7f3c8586423c428b79586a1c55cd" +checksum = "d4e4c4183c39ae8d00e53afeeab66aa71c41b08d82638149f9d485a2a62df569" dependencies = [ "bevy_macro_utils", "encase_derive_impl", ] [[package]] -name = "bevy_gilrs" -version = "0.19.0" +name = "bevy_extract" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7998fe83c89d527efa83c85c574a17f7bb6e34881d638ecb7f706d9d79e82f3c" +checksum = "08e27fc07ef8e3ab7c217fed1b171d42c90f3b6e4f7f526ffa28f9006ac49aa4" +dependencies = [ + "bevy_app", + "bevy_camera", + "bevy_derive", + "bevy_ecs", + "bevy_extract_macros", + "bevy_log", + "bevy_platform", + "bevy_reflect", + "bevy_time", + "bevy_utils", +] + +[[package]] +name = "bevy_extract_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5c67e8c55ef35c65295e8cb614eda169780e21e2748f6df470d3bff21849a3" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_gilrs" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a8c6c22d01a7b2e949ac1be313fffca62a23ae0b802a884f009e6dc70a4cea" dependencies = [ "bevy_app", "bevy_ecs", @@ -1025,14 +1143,15 @@ dependencies = [ [[package]] name = "bevy_gizmos" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c0cbd55cc33b6412777d60d8c24a96bee148c82a5731f35ac85b2df4a7cf957" +checksum = "dfc92f635ec5eaef5733afee45e6a0452e8c65de42d818dc7269dc5265087689" dependencies = [ "bevy_app", "bevy_asset", "bevy_camera", "bevy_color", + "bevy_curve", "bevy_ecs", "bevy_gizmos_macros", "bevy_input", @@ -1040,6 +1159,7 @@ dependencies = [ "bevy_math", "bevy_mesh", "bevy_reflect", + "bevy_shape", "bevy_time", "bevy_transform", "bevy_utils", @@ -1048,9 +1168,9 @@ dependencies = [ [[package]] name = "bevy_gizmos_macros" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9903f2614f53bacacb92bef1e407afe3a27a4abddb500bfd479909c5d7b254df" +checksum = "501e72c4ac31caef17ef9e627dee212d2107b95402a891ec6f0fc4107dd0cdf4" dependencies = [ "bevy_macro_utils", "quote", @@ -1059,9 +1179,9 @@ dependencies = [ [[package]] name = "bevy_gizmos_render" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd569456c4b19bafa6d37c1bd9cc743fdadda3a2f23d79d2ceb0a088ea8d475f" +checksum = "42f5be0c9f114dc9b2c929076517d088e059b971edec5dfff4d5a225df6796df" dependencies = [ "bevy_app", "bevy_asset", @@ -1069,6 +1189,7 @@ dependencies = [ "bevy_color", "bevy_core_pipeline", "bevy_ecs", + "bevy_extract", "bevy_gizmos", "bevy_image", "bevy_log", @@ -1078,6 +1199,7 @@ dependencies = [ "bevy_reflect", "bevy_render", "bevy_shader", + "bevy_shape", "bevy_sprite_render", "bevy_transform", "bevy_utils", @@ -1087,9 +1209,9 @@ dependencies = [ [[package]] name = "bevy_image" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37bc41f69a0c6ade2e7961602517545b39ab8e42bc8c4a729bd24ffee3bcd904" +checksum = "bb818dd8d705809d54adea08e346edce81a51ddbf06406ddf12a72ee467a3855" dependencies = [ "bevy_app", "bevy_asset", @@ -1114,9 +1236,9 @@ dependencies = [ [[package]] name = "bevy_input" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd221cf0732f5bf06caf594bdb233361ac735d4d416cf5849757b64bf4e8472a" +checksum = "261ca5cc8a0bf228bfe4e841570db18a3ab1d034ce089772b8ec49c163ae74ea" dependencies = [ "bevy_app", "bevy_ecs", @@ -1131,11 +1253,12 @@ dependencies = [ [[package]] name = "bevy_input_focus" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1a14f56c6e722048ec9dc20dbbc6f46b8037f61e319ea776829199278da166" +checksum = "83c81d369d0ab3ec17ba14bab1e07da7cacf3751a3104a93b1a3cd99d887887b" dependencies = [ "bevy_app", + "bevy_camera", "bevy_ecs", "bevy_input", "bevy_math", @@ -1148,9 +1271,9 @@ dependencies = [ [[package]] name = "bevy_internal" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a85632a749f956f92b7b391c31ce1f229e57b06de879d1b60a3bb91e4e240" +checksum = "a4ed1cb58d1761426a077a8aff55e6a2e0cdfd19e411211d4b0217409330bb84" dependencies = [ "bevy_a11y", "bevy_android", @@ -1161,9 +1284,11 @@ dependencies = [ "bevy_clipboard", "bevy_color", "bevy_core_pipeline", + "bevy_curve", "bevy_derive", "bevy_diagnostic", "bevy_ecs", + "bevy_extract", "bevy_gilrs", "bevy_gizmos", "bevy_gizmos_render", @@ -1182,6 +1307,7 @@ dependencies = [ "bevy_reflect", "bevy_render", "bevy_shader", + "bevy_shape", "bevy_sprite", "bevy_sprite_render", "bevy_state", @@ -1199,14 +1325,15 @@ dependencies = [ [[package]] name = "bevy_light" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7b5009e9cc765c30b21daf2277ea4423cbd347b05280ea0943b50c5a80cd42" +checksum = "529d5ec7db637bbce7d016b2bfd0798362ad67118de8e9d37aaac24ecb3337cd" dependencies = [ "bevy_app", "bevy_asset", "bevy_camera", "bevy_color", + "bevy_curve", "bevy_ecs", "bevy_image", "bevy_log", @@ -1214,6 +1341,7 @@ dependencies = [ "bevy_mesh", "bevy_platform", "bevy_reflect", + "bevy_shape", "bevy_transform", "bevy_utils", "half", @@ -1224,9 +1352,9 @@ dependencies = [ [[package]] name = "bevy_log" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2017a5fe12ea230d00cfdca00c65c262924be26e0324f7e0033c64f8cc265888" +checksum = "a87d5152292b0e581a589cffb6b7e83e0afe345279d4126545ce7b786bd8a47a" dependencies = [ "android_log-sys", "bevy_app", @@ -1244,9 +1372,9 @@ dependencies = [ [[package]] name = "bevy_macro_utils" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "746a19912c6dc1bbe79188778573e8a253d5832c696b2fcb95578c17b29ff7ba" +checksum = "c0d03a948df65138e30917d91237797d7e38763135ebacd0f28c891e1a3b2b52" dependencies = [ "proc-macro2", "quote", @@ -1256,9 +1384,9 @@ dependencies = [ [[package]] name = "bevy_material" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "423710403b4a4d8e9ee872c6ac8108787f2396422b582d15a660a40731ddfbf0" +checksum = "5be7ee4b2f4764e8d58a59afb47a3ba8bae36f955f409f08c658cace2808ef40" dependencies = [ "bevy_asset", "bevy_derive", @@ -1279,9 +1407,9 @@ dependencies = [ [[package]] name = "bevy_material_macros" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6928e167592472f6ee900f80de199cc2dafd31d4f082a86e8d594b81b973e3d" +checksum = "70551fa201e1416ab44968631b7ce6320c194acfdb652a1971ec6882af122333" dependencies = [ "bevy_macro_utils", "quote", @@ -1290,19 +1418,16 @@ dependencies = [ [[package]] name = "bevy_math" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c280440d2a5bc07ea393b5346b5712eec73ea30f0133097b8b13dade385beb" +checksum = "598cbdcec4d0f0d966905996240ac76f77a644c583f95fb612d4f59f5f649038" dependencies = [ "approx", - "arrayvec", "bevy_reflect", "derive_more", "glam", - "itertools", "libm", "rand", - "rand_distr", "serde", "thiserror 2.0.19", "variadics_please", @@ -1310,9 +1435,9 @@ dependencies = [ [[package]] name = "bevy_mesh" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59863e6f37ef0ba1f37021bce9dd940f9dc31d15e03f548cda5fd3e674f409e" +checksum = "09358ac9a3ede5855568c10eaba6bcf126bf86639e786fb21b413457ebec5f26" dependencies = [ "bevy_app", "bevy_asset", @@ -1323,6 +1448,7 @@ dependencies = [ "bevy_mikktspace", "bevy_platform", "bevy_reflect", + "bevy_shape", "bevy_transform", "bitflags 2.13.1", "bytemuck", @@ -1344,9 +1470,9 @@ checksum = "bff34eb29ff4b8a8688bc7299f14fb6b597461ca80fec03ed7d22939ab33e48f" [[package]] name = "bevy_pbr" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ebc777de2d7f45888c68e8e04949bfb8a40aee62f700283fc340f525e67f52" +checksum = "fb760932d1e683e7c00011f5c99e4e1cc893646a3a7b7224da6daf9bfa94853d" dependencies = [ "arrayvec", "bevy_app", @@ -1357,6 +1483,7 @@ dependencies = [ "bevy_derive", "bevy_diagnostic", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_light", "bevy_log", @@ -1367,6 +1494,7 @@ dependencies = [ "bevy_reflect", "bevy_render", "bevy_shader", + "bevy_shape", "bevy_tasks", "bevy_transform", "bevy_utils", @@ -1386,9 +1514,9 @@ dependencies = [ [[package]] name = "bevy_picking" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93468ac5937f76be3af763d496f01e899edf6d9c1a1d39a87ae4ce8eba36f78b" +checksum = "47a04fb644c62a57a4f338f5cd8b61519c569139742eafe87dccb523e4b7cf2e" dependencies = [ "bevy_app", "bevy_asset", @@ -1399,6 +1527,7 @@ dependencies = [ "bevy_math", "bevy_platform", "bevy_reflect", + "bevy_shape", "bevy_time", "bevy_transform", "bevy_window", @@ -1408,10 +1537,11 @@ dependencies = [ [[package]] name = "bevy_platform" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3120670bb2308980f723477c9d82476fcda31f08be9b256c3f0acdee82a65094" +checksum = "000bc467d1b344b6609fcf217a9b08d349d49c8e85dc4179fb7656f2b6e3db47" dependencies = [ + "bytemuck", "critical-section", "foldhash 0.2.0", "futures-channel", @@ -1430,17 +1560,19 @@ dependencies = [ [[package]] name = "bevy_post_process" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57379b51aa0f1b2066c956263e1f12686d22cf8939cf9644eebf2ee42d3dd460" +checksum = "d684c2ac93e94d9b7ba767ce02ea435ef244677a24746241726de2aa552ba1a4" dependencies = [ "bevy_app", "bevy_asset", "bevy_camera", "bevy_color", "bevy_core_pipeline", + "bevy_curve", "bevy_derive", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_math", "bevy_platform", @@ -1455,15 +1587,15 @@ dependencies = [ [[package]] name = "bevy_ptr" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b511e89f7078fd21fdea77061a0ca3e737297c7011bb10c073e0aecb17c9fea6" +checksum = "41935a00c02004753bd7ed5094754397800b3f673bf15553bb007b06216872d5" [[package]] name = "bevy_reflect" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92abd59cbba1524c1847e9a50f74d94e6b7836c80a8edfa9c47e59f7fd81021d" +checksum = "46eecc733575b873deedcc37639e36290092388657091b3321fa6e073e7818e6" dependencies = [ "assert_type_match", "bevy_platform", @@ -1489,9 +1621,9 @@ dependencies = [ [[package]] name = "bevy_reflect_derive" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fa4e6b97fd2d582dd5ed96762a70d04505a477c833cf4f69d016dcd03d2d04" +checksum = "dfaa1e315676c625ed8d30ba8a671ebf917cad6355320295b9802a8cbc06853b" dependencies = [ "bevy_macro_utils", "indexmap", @@ -1503,9 +1635,9 @@ dependencies = [ [[package]] name = "bevy_render" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89b2cc850a4c3fd12a6a088a4fc3e80118b2e1b569da8936f6d4d99ca1b6ee7" +checksum = "5f438a921fd3ef82cc09ccf5e0cd8f1214dcae6286890f9d4acba0409d9a61a8" dependencies = [ "async-channel", "bevy_app", @@ -1516,6 +1648,8 @@ dependencies = [ "bevy_diagnostic", "bevy_ecs", "bevy_encase_derive", + "bevy_extract", + "bevy_extract_macros", "bevy_image", "bevy_log", "bevy_material", @@ -1526,6 +1660,7 @@ dependencies = [ "bevy_reflect", "bevy_render_macros", "bevy_shader", + "bevy_shape", "bevy_tasks", "bevy_time", "bevy_transform", @@ -1539,15 +1674,16 @@ dependencies = [ "glam", "image", "indexmap", - "itertools", + "itertools 0.14.0", "js-sys", - "naga", "nonmax", "offset-allocator", "profiling", "send_wrapper", "smallvec", + "static_assertions", "thiserror 2.0.19", + "tracing", "variadics_please", "wasm-bindgen", "weak-table", @@ -1558,10 +1694,11 @@ dependencies = [ [[package]] name = "bevy_render_macros" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0eb5cffe5253a6ab4e4b84b2e40a6a2f4673deb16fd63ab9dfc4a015449873a" +checksum = "119ca6d445eba5c9976c658f29bac854094b27c27624aa92c4d9668c319618d0" dependencies = [ + "bevy_extract_macros", "bevy_macro_utils", "proc-macro2", "quote", @@ -1570,28 +1707,42 @@ dependencies = [ [[package]] name = "bevy_shader" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0ac51378bb6a021165ee334f846b515cd06082e24ce962adce58ad7df1c39a" +checksum = "c2becafd54079da13835d6a42fc743e305d6270c27a91de613afe6e3f4b5de19" dependencies = [ "bevy_asset", "bevy_platform", "bevy_reflect", "bevy_utils", - "naga", - "naga_oil", "serde", "thiserror 2.0.19", "tracing", - "wgpu-naga-bridge", + "wesl", "wgpu-types", ] [[package]] -name = "bevy_sprite" -version = "0.19.0" +name = "bevy_shape" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c679b88cc655881d403929bcdf02d30f688fd8916eb1635e6e7f8585d8ae6894" +checksum = "aa8b6096ad1ec7dfe1af8e51d7cfa7de1a9e0a3bd02be58a061092c5e1ff1eb1" +dependencies = [ + "arrayvec", + "bevy_math", + "bevy_reflect", + "derive_more", + "rand", + "rand_distr", + "serde", + "thiserror 2.0.19", +] + +[[package]] +name = "bevy_sprite" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24dc4f69160513952a1969a3df5b4a2c729acfe0caf773854acea4a8c71f0918" dependencies = [ "bevy_app", "bevy_asset", @@ -1614,9 +1765,9 @@ dependencies = [ [[package]] name = "bevy_sprite_render" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a65ca9a286dd5815ca4d2d41d4f83bdbb22c8a639d10b4c33eac114b369c36" +checksum = "8918649806697740b61d3fd0559cb8c7de2ade5046aacf8abbef20e49ea1de0c" dependencies = [ "bevy_app", "bevy_asset", @@ -1625,6 +1776,7 @@ dependencies = [ "bevy_core_pipeline", "bevy_derive", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_material", "bevy_math", @@ -1633,6 +1785,7 @@ dependencies = [ "bevy_reflect", "bevy_render", "bevy_shader", + "bevy_shape", "bevy_sprite", "bevy_text", "bevy_transform", @@ -1642,14 +1795,16 @@ dependencies = [ "derive_more", "fixedbitset", "nonmax", + "smallvec", + "static_assertions", "tracing", ] [[package]] name = "bevy_state" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c22e694ea52e42994aea6ed2d0b378fa1e9d5a9b615db88626e33508bc03f35" +checksum = "09292e08e1eb34d2dd915630fb3c04ba75220c7819943c9f213d3a5f9a8be1e8" dependencies = [ "bevy_app", "bevy_ecs", @@ -1663,9 +1818,9 @@ dependencies = [ [[package]] name = "bevy_state_macros" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835f45091fa0df1ff3cb10f4ca5a397d4e9249b550f1be6893b08a2a06d6eed7" +checksum = "fec1a8f6a253e39ddf7e447b1b9286caacb205ed8e619e6fb9a2e010d58dd04b" dependencies = [ "bevy_macro_utils", "quote", @@ -1674,9 +1829,9 @@ dependencies = [ [[package]] name = "bevy_tasks" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa34e6b9b804851ec08a41c0346c859d82e2fa98c906d74b965ee61bbbb688e4" +checksum = "fbd8116f12dc50bde3758a72fba85e1f4c00e6c66d0ad58bdc46c0c0690829a8" dependencies = [ "async-channel", "async-executor", @@ -1693,9 +1848,9 @@ dependencies = [ [[package]] name = "bevy_text" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e1d7eb8b10229f424b4fc56c864a26c32ac02b8cd184e2cc25eceea7b5e892" +checksum = "119d2b37ce3f5ea5a5b9092a7c9eec8d3734de62592d2869bd1d925976f34d05" dependencies = [ "bevy_app", "bevy_asset", @@ -1722,9 +1877,9 @@ dependencies = [ [[package]] name = "bevy_time" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c21e3be2aa4426ea1e0a7036d3d1dbbded84c319459d9f3e2a616b33563372f2" +checksum = "13901174b792bb7848d8e088075f0009ea409b99179934c0d5bdd2b95622694e" dependencies = [ "bevy_app", "bevy_ecs", @@ -1737,9 +1892,9 @@ dependencies = [ [[package]] name = "bevy_transform" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95fefe69c9223d10e6b52a0fdf12811bab9d0c7dcb27570cb86824b451f180d" +checksum = "83379023d77a0739569a5bcb859eed652781a1b3271e94a86dfbb56fc5d067e0" dependencies = [ "bevy_app", "bevy_ecs", @@ -1750,13 +1905,14 @@ dependencies = [ "derive_more", "serde", "thiserror 2.0.19", + "tracing", ] [[package]] name = "bevy_ui" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5519c799ecf14e2eeece8b9b823250a00c9df14523e2638b647fccfe4ff0d20" +checksum = "7926fba78f133376db0cba73a054db6931952379b5bfdc63f2fe9ba0c3b96c39" dependencies = [ "accesskit", "bevy_a11y", @@ -1792,10 +1948,11 @@ dependencies = [ [[package]] name = "bevy_ui_render" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d3921936d88bd2638dd4d87bff8fc3b9908a046a50658548045102249f93e" +checksum = "620b51b51eb8e7d8b2362ba96263142829560d9aa254e2f28032570667c6b0fb" dependencies = [ + "bevy_a11y", "bevy_app", "bevy_asset", "bevy_camera", @@ -1803,6 +1960,7 @@ dependencies = [ "bevy_core_pipeline", "bevy_derive", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_input_focus", "bevy_math", @@ -1820,14 +1978,15 @@ dependencies = [ "bytemuck", "derive_more", "indexmap", + "smallvec", "tracing", ] [[package]] name = "bevy_ui_widgets" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fc616ccc51b36dea5cb1de91ed82569b41649923388e5ba82cb7d2f1ef18d8" +checksum = "6442cd402df6a54f0e51ed8c1bd004c8f9c6715d0f8045e33c68ea9aebb1ce16" dependencies = [ "accesskit", "bevy_a11y", @@ -1839,19 +1998,22 @@ dependencies = [ "bevy_log", "bevy_math", "bevy_picking", + "bevy_platform", "bevy_reflect", "bevy_text", + "bevy_time", "bevy_ui", "bevy_window", "parley", "smol_str", + "web-sys", ] [[package]] name = "bevy_utils" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aceea6c7ffdd568f866d5ca4dfe50c33440c54f7bc230c13a9ba7ab0c91aed1" +checksum = "5cbd58495039d4e63e6fcfef1242fecab33ffe62b4a5e010467e69e790879812" dependencies = [ "async-channel", "bevy_platform", @@ -1862,9 +2024,9 @@ dependencies = [ [[package]] name = "bevy_window" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efcc255b43db156fba393b2ff8fa552aac5e7e02ae4c6298eacdbab655ddf988" +checksum = "dc9a83f3cbaa04155b5dc9b247819c0e215aee754451a4324b82ddeb80dc53f1" dependencies = [ "bevy_app", "bevy_ecs", @@ -1879,9 +2041,9 @@ dependencies = [ [[package]] name = "bevy_winit" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308c9eb04f74f340593b3def6a7c1778fd42581d779c6db836d690c7d471f94c" +checksum = "1ef0a5a55524e793ab13536ef244229fa66a4839fae6564df2aa4b3c13956a82" dependencies = [ "accesskit", "accesskit_winit", @@ -1932,9 +2094,9 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" dependencies = [ "bit-vec", ] @@ -2210,28 +2372,21 @@ dependencies = [ "trace_iw4", ] -[[package]] -name = "codespan-reporting" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - [[package]] name = "codespan-reporting" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ - "serde", - "termcolor", "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -2274,6 +2429,7 @@ dependencies = [ "net", "perf", "render", + "render_anim", "render_frontend", "render_fx", "replay", @@ -2388,15 +2544,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - [[package]] name = "coreaudio-rs" version = "0.14.2" @@ -2553,12 +2700,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" -[[package]] -name = "data-encoding" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" - [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -2752,6 +2893,7 @@ dependencies = [ name = "entity_iw4" version = "0.1.0" dependencies = [ + "asset_core", "libm", "math_iw4", ] @@ -2903,15 +3045,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "font-types" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" -dependencies = [ - "bytemuck", -] - [[package]] name = "font-types" version = "0.12.2" @@ -2923,15 +3056,15 @@ dependencies = [ [[package]] name = "fontique" -version = "0.9.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c20b425addb8661e97fe1d51c4d8bcec3ec29ed6ad0db983976a7521276b8f7" +checksum = "6688bc1294fe7117d788937b6c53480169b29c566954af490830d4c09da9516a" dependencies = [ "hashbrown 0.17.1", "linebender_resource_handle", "memmap2", "parlance", - "read-fonts 0.39.2", + "read-fonts", "smallvec", ] @@ -3203,10 +3336,11 @@ dependencies = [ [[package]] name = "glam" -version = "0.32.1" +version = "0.33.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" +checksum = "37bfe73dc8ec21f54d181e5a60554327ec3f1c600c455a75ccbfc65d2106a6c4" dependencies = [ + "approx", "bytemuck", "encase", "libm", @@ -3214,6 +3348,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "gpu-allocator" version = "0.28.0" @@ -3228,40 +3368,13 @@ dependencies = [ "windows", ] -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.13.1", - "gpu-descriptor-types", - "hashbrown 0.15.5", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "grid" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" - [[package]] name = "guillotiere" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" dependencies = [ "euclid", - "svg_fmt", ] [[package]] @@ -3298,14 +3411,13 @@ dependencies = [ [[package]] name = "harfrust" -version = "0.6.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551ed25397e4b444e89686602877d5cf3a7f6e3d548dcac37a8357d1e195f4df" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" dependencies = [ "bitflags 2.13.1", "bytemuck", - "core_maths", - "read-fonts 0.39.2", + "read-fonts", "smallvec", ] @@ -3368,21 +3480,15 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hexasphere" -version = "18.0.0" +version = "19.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177ea6330876de4ef08e184f827b98acc037614a4ce409902c5b0d53a9c2e951" +checksum = "163385eff5a0cf606ebe2c10b3a44ab02f4306e83fbd068a64f5e1d72a3049ac" dependencies = [ "constgebra", "glam", "tinyvec", ] -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - [[package]] name = "http" version = "1.5.0" @@ -3455,6 +3561,7 @@ dependencies = [ "playerstate_iw4", "render_material", "sim", + "structured_data_iw4", "weapon_iw4", ] @@ -3720,6 +3827,8 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -3764,6 +3873,12 @@ version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -3773,6 +3888,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -3883,9 +4007,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", @@ -3896,6 +4020,15 @@ dependencies = [ name = "killcam_iw4" version = "0.1.0" +[[package]] +name = "lalrpop-util" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884f3e747ed2dcee867cda1b0c31a048f9e20de2d916a248949319921a2e666e" +dependencies = [ + "regex-automata", +] + [[package]] name = "launcher" version = "0.1.0" @@ -3912,6 +4045,72 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lexical" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc8a009b2ff1f419ccc62706f04fe0ca6e67b37460513964a3dfdb919bb37d6" +dependencies = [ + "lexical-core", +] + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + [[package]] name = "libc" version = "0.2.189" @@ -4042,6 +4241,38 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn 2.0.119", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -4183,26 +4414,31 @@ dependencies = [ ] [[package]] -name = "naga" -version = "29.0.4" +name = "mutants" +version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" +checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" + +[[package]] +name = "naga" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" dependencies = [ "arrayvec", "bit-set", "bitflags 2.13.1", "cfg-if", "cfg_aliases", - "codespan-reporting 0.13.1", + "codespan-reporting", "half", - "hashbrown 0.16.1", - "hexf-parse", + "hashbrown 0.17.1", "indexmap", "libm", "log", + "naga-types", "num-traits", "once_cell", - "pp-rs", "rustc-hash 1.1.0", "spirv", "thiserror 2.0.19", @@ -4210,20 +4446,16 @@ dependencies = [ ] [[package]] -name = "naga_oil" -version = "0.22.0" +name = "naga-types" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "319f03062940ed85c03da020042618356bb8ca5263020322930883b05e30208e" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" dependencies = [ - "codespan-reporting 0.12.0", - "data-encoding", + "hashbrown 0.17.1", "indexmap", - "naga", - "regex", "rustc-hash 1.1.0", + "serde", "thiserror 2.0.19", - "tracing", - "unicode-ident", ] [[package]] @@ -4299,6 +4531,15 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nonmax" version = "0.5.5" @@ -4668,6 +4909,7 @@ dependencies = [ "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", + "objc2-core-graphics", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", ] @@ -4743,6 +4985,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -4768,6 +5016,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -4820,9 +5078,9 @@ checksum = "4b6937eda350acc1a5d05872c3cbf99fe78619c269096e2be3d4a350058639d5" [[package]] name = "parley" -version = "0.9.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fad031076f48f0d4d85ce1aea9b94b4e715a4d636a030a123038f8f5b5e4343" +checksum = "22d2ff88bd3f7d68d1d9b09c7e6209f9a8e8c05088295140a2bcf2e9b17038c5" dependencies = [ "fontique", "harfrust", @@ -4833,14 +5091,14 @@ dependencies = [ "linebender_resource_handle", "parlance", "parley_data", - "skrifa 0.42.1", + "skrifa", ] [[package]] name = "parley_data" -version = "0.9.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ab9ace3fad1b9ed603ddac5b595e69931fc50263d7e04e4055015b77b02da5" +checksum = "1567535334d6ba2d3cde19221ba9a7bd0fabb3cbd99046ddfb10ae061cfcc889" dependencies = [ "icu_properties", ] @@ -4888,6 +5146,17 @@ dependencies = [ "cc", ] +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -5004,15 +5273,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "pp-rs" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb458bb7f6e250e6eb79d5026badc10a3ebb8f9a15d1fff0f13d17c71f4d6dee" -dependencies = [ - "unicode-xid", -] - [[package]] name = "presser" version = "0.3.1" @@ -5215,16 +5475,6 @@ dependencies = [ "objc2-quartz-core 0.3.2", ] -[[package]] -name = "read-fonts" -version = "0.39.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" -dependencies = [ - "bytemuck", - "font-types 0.11.3", -] - [[package]] name = "read-fonts" version = "0.41.0" @@ -5232,7 +5482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" dependencies = [ "bytemuck", - "font-types 0.12.2", + "font-types", "once_cell", ] @@ -5269,18 +5519,6 @@ dependencies = [ "bitflags 2.13.1", ] -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" version = "0.4.16" @@ -5510,6 +5748,7 @@ dependencies = [ "bevy", "diag", "dpvs_iw4", + "frame", "fx_iw4", "hud_iw4", "lighting_iw4", @@ -5898,9 +6137,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" dependencies = [ "serde_core", ] @@ -6030,16 +6269,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "skrifa" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" -dependencies = [ - "bytemuck", - "read-fonts 0.39.2", -] - [[package]] name = "skrifa" version = "0.44.0" @@ -6047,7 +6276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" dependencies = [ "bytemuck", - "read-fonts 0.41.0", + "read-fonts", ] [[package]] @@ -6117,9 +6346,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.10.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" dependencies = [ "portable-atomic", ] @@ -6179,19 +6408,13 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "svg_fmt" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" - [[package]] name = "swash" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" dependencies = [ - "skrifa 0.44.0", + "skrifa", "yazi", "zeno", ] @@ -6367,23 +6590,14 @@ dependencies = [ [[package]] name = "taffy" -version = "0.10.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aea22054047c16c3f34d3ac473a2170be1424b1115b2a3adcf28cfb067c88859" +checksum = "639627c87f43b9181c811f40a6296409e093a17bc761214cba3c15df74f86b99" dependencies = [ "arrayvec", - "grid", "serde", "slotmap", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", + "smallvec", ] [[package]] @@ -6552,6 +6766,21 @@ dependencies = [ "winnow 0.7.15", ] +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -6563,9 +6792,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" dependencies = [ "serde_core", ] @@ -6577,25 +6806,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime 1.1.2+spec-1.1.0", "toml_parser", "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" [[package]] name = "tower" @@ -6760,6 +6989,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -6838,6 +7078,17 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsynn" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501a7adf1a4bd9951501e5c66621e972ef8874d787628b7f90e64f936ef7ec0a" +dependencies = [ + "mutants", + "proc-macro2", + "rustc-hash 2.1.3", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -6848,6 +7099,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" name = "updater" version = "0.1.0" dependencies = [ + "diag", "fs2", "reqwest", "rustls", @@ -6855,7 +7107,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "toml", + "toml 0.9.12+spec-1.1.0", "windows-sys 0.61.2", "zstd", ] @@ -6878,6 +7130,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.24.0" @@ -6898,13 +7156,12 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "variadics_please" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +checksum = "b53d0f7f2d0182759a549f1f313ce16b07d8f9ba93098157b5fa10ee3e61117f" dependencies = [ - "proc-macro2", "quote", - "syn 2.0.119", + "unsynn", ] [[package]] @@ -6955,9 +7212,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -6968,19 +7225,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" dependencies = [ "js-sys", + "tokio", "wasm-bindgen", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6988,22 +7246,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] @@ -7118,9 +7376,13 @@ dependencies = [ [[package]] name = "weak-table" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" +checksum = "5eba2ec791141b0bf4db542f6b038a8f9310cee33e076cb181245780b09a24ff" +dependencies = [ + "cfg-if", + "hashbrown 0.16.1", +] [[package]] name = "weapon_iw4" @@ -7134,9 +7396,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" dependencies = [ "js-sys", "wasm-bindgen", @@ -7174,10 +7436,50 @@ dependencies = [ ] [[package]] -name = "wgpu" -version = "29.0.4" +name = "wesl" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +checksum = "fdd8f9619baa4c7bde4df0899d6f626b70ee92d6e9c18c5f74473419b5bb6fa6" +dependencies = [ + "annotate-snippets", + "anstream", + "derive_more", + "glob", + "half", + "itertools 0.15.0", + "num-traits", + "serde", + "thiserror 2.0.19", + "toml 1.1.8+spec-1.1.0", + "wesl-core", + "wesl-macros", + "wgsl-parse", + "wgsl-types", +] + +[[package]] +name = "wesl-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84dd3e600497eda0b47afcd21bd84c8953a9dfce7e93ed27a38ba97ac0747e03" + +[[package]] +name = "wesl-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8767ec26e75262a8e581d5f188b1f63a288baaf885bb33c9f4f73a693e06fc" +dependencies = [ + "itertools 0.15.0", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "wgpu" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" dependencies = [ "arrayvec", "bitflags 2.13.1", @@ -7185,9 +7487,8 @@ dependencies = [ "cfg-if", "cfg_aliases", "document-features", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "log", - "naga", "portable-atomic", "profiling", "raw-window-handle", @@ -7200,9 +7501,9 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" +checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" dependencies = [ "arrayvec", "bit-set", @@ -7211,10 +7512,11 @@ dependencies = [ "bytemuck", "cfg_aliases", "document-features", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap", "log", "naga", + "naga-types", "once_cell", "parking_lot", "portable-atomic", @@ -7232,27 +7534,27 @@ dependencies = [ [[package]] name = "wgpu-core-deps-apple" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" +checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" +checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" +checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" dependencies = [ "android_system_properties", "arrayvec", @@ -7264,14 +7566,15 @@ dependencies = [ "cfg-if", "cfg_aliases", "gpu-allocator", - "gpu-descriptor", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "libc", "libloading", "log", "naga", + "naga-types", "objc2 0.6.4", "objc2-core-foundation", + "objc2-core-graphics", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", "objc2-quartz-core 0.3.2", @@ -7286,6 +7589,7 @@ dependencies = [ "raw-window-metal", "renderdoc-sys", "smallvec", + "static_assertions", "thiserror 2.0.19", "wgpu-naga-bridge", "wgpu-types", @@ -7295,9 +7599,9 @@ dependencies = [ [[package]] name = "wgpu-naga-bridge" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" +checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" dependencies = [ "naga", "wgpu-types", @@ -7305,19 +7609,48 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" dependencies = [ "bitflags 2.13.1", "bytemuck", "js-sys", "log", + "naga-types", "raw-window-handle", "serde", + "static_assertions", "web-sys", ] +[[package]] +name = "wgsl-parse" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37cee524ba5cfabfd65d10a316b646f8a4c50f3663e7e787471f21b783261f47" +dependencies = [ + "annotate-snippets", + "derive_more", + "itertools 0.15.0", + "lalrpop-util", + "lexical", + "logos", + "thiserror 2.0.19", + "wgsl-types", +] + +[[package]] +name = "wgsl-types" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e119dd12e57d937397535201959ddddb5dbf26505402db5ef305ebb1ca9a7e5f" +dependencies = [ + "half", + "itertools 0.15.0", + "num-traits", +] + [[package]] name = "winapi" version = "0.3.9" @@ -7706,6 +8039,24 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wl-clipboard-rs" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d7888ccd4896447b2d14d3a9350a85df2aeb6f181e2e7a31349d104ac46cac1" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.4", + "thiserror 2.0.19", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "writeable" version = "0.6.3" @@ -7785,7 +8136,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "toml", + "toml 0.9.12+spec-1.1.0", "updater", "zip", ] diff --git a/Cargo.toml b/Cargo.toml index d0388e3..d861f5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,9 @@ edition = "2024" license = "Apache-2.0" [workspace.dependencies] -bevy = { version = "0.19", default-features = false, features = [ +bevy = { version = "0.20", default-features = false, features = [ + "bevy_state", + "debug", "bevy_window", "bevy_winit", "bevy_ui", @@ -100,7 +102,7 @@ bevy = { version = "0.19", default-features = false, features = [ "bevy_ui_widgets", "ui_picking", ] } -bevy_ecs = "0.19" +bevy_ecs = "0.20" miniz_oxide = "0.8" lzokay-native = "0.1" bcdec_rs = "0.2" @@ -172,7 +174,7 @@ trace_iw4 = { path = "crates/trace_iw4" } ui = { path = "crates/ui" } weapon_iw4 = { path = "crates/weapon_iw4" } xmodel_runtime = { path = "crates/xmodel_runtime" } -glam = "0.32.1" +glam = "0.33.2" # Transcribing a retail constant at full precision is the requirement, not a # style slip. `clippy::excessive_precision` fired on diff --git a/THIRD-PARTY-LICENSES.txt b/THIRD-PARTY-LICENSES.txt index 66a4f61..f099635 100644 --- a/THIRD-PARTY-LICENSES.txt +++ b/THIRD-PARTY-LICENSES.txt @@ -919,7 +919,6 @@ Used by: Apache License 2.0 (Apache-2.0) -------------------------------------------------------------------------------- Used by: - codespan-reporting 0.12.0 — https://github.com/brendanzab/codespan codespan-reporting 0.13.1 — https://github.com/brendanzab/codespan cpal 0.17.3 — https://github.com/RustAudio/cpal @@ -1610,7 +1609,7 @@ Used by: updater 0.1.0 weapon_iw4 0.1.0 xmodel_runtime 0.1.0 - accesskit_winit 0.32.2 — https://github.com/AccessKit/accesskit + accesskit_winit 0.33.2 — https://github.com/AccessKit/accesskit blake3 1.8.5 — https://github.com/BLAKE3-team/BLAKE3 constant_time_eq 0.4.2 — https://github.com/cesarb/constant_time_eq ryu 1.0.23 — https://github.com/dtolnay/ryu @@ -1905,135 +1904,6 @@ The copyright notices in the Software and this entire statement, including the a THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -================================================================================ -Creative Commons Zero v1.0 Universal (CC0-1.0) --------------------------------------------------------------------------------- -Used by: - hexf-parse 0.2.1 — https://github.com/lifthrasiir/hexf - -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. - - ================================================================================ ISC License (ISC) -------------------------------------------------------------------------------- @@ -2941,13 +2811,13 @@ MIT License (MIT) Used by: bitflags 1.3.2 — https://github.com/bitflags/bitflags bitflags 2.13.1 — https://github.com/bitflags/bitflags + glob 0.3.4 — https://github.com/rust-lang/glob log 0.4.33 — https://github.com/rust-lang/log num-derive 0.3.3 — https://github.com/rust-num/num-derive num-derive 0.4.2 — https://github.com/rust-num/num-derive num-traits 0.2.19 — https://github.com/rust-num/num-traits regex-automata 0.4.16 — https://github.com/rust-lang/regex regex-syntax 0.8.11 — https://github.com/rust-lang/regex - regex 1.13.1 — https://github.com/rust-lang/regex Copyright (c) 2014 The Rust Project Developers @@ -3110,6 +2980,7 @@ MIT License (MIT) Used by: either 1.17.0 — https://github.com/rayon-rs/either itertools 0.14.0 — https://github.com/rust-itertools/itertools + itertools 0.15.0 — https://github.com/rust-itertools/itertools Copyright (c) 2015 @@ -3171,6 +3042,39 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +MIT License (MIT) +-------------------------------------------------------------------------------- +Used by: + lalrpop-util 0.23.1 — https://github.com/lalrpop/lalrpop + +Copyright (c) 2015 The LALRPOP Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + ================================================================================ MIT License (MIT) -------------------------------------------------------------------------------- @@ -3406,7 +3310,6 @@ DEALINGS IN THE SOFTWARE. MIT License (MIT) -------------------------------------------------------------------------------- Used by: - hashbrown 0.15.5 — https://github.com/rust-lang/hashbrown hashbrown 0.16.1 — https://github.com/rust-lang/hashbrown hashbrown 0.17.1 — https://github.com/rust-lang/hashbrown @@ -3470,6 +3373,39 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +MIT License (MIT) +-------------------------------------------------------------------------------- +Used by: + utf8parse 0.2.2 — https://github.com/alacritty/vte + +Copyright (c) 2016 Joe Wilm + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + ================================================================================ MIT License (MIT) -------------------------------------------------------------------------------- @@ -3931,6 +3867,37 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +MIT License (MIT) +-------------------------------------------------------------------------------- +Used by: + logos-codegen 0.16.1 — https://github.com/maciejhirsz/logos + logos-derive 0.16.1 — https://github.com/maciejhirsz/logos + logos 0.16.1 — https://github.com/maciejhirsz/logos + +Copyright (c) 2018 Maciej Hirsz + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ================================================================================ MIT License (MIT) -------------------------------------------------------------------------------- @@ -4272,41 +4239,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -================================================================================ -MIT License (MIT) --------------------------------------------------------------------------------- -Used by: - font-types 0.11.3 — https://github.com/googlefonts/fontations - read-fonts 0.39.2 — https://github.com/googlefonts/fontations - skrifa 0.42.1 — https://github.com/googlefonts/fontations - -Copyright (c) 2019 Colin Rothfels - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - - ================================================================================ MIT License (MIT) -------------------------------------------------------------------------------- @@ -5197,7 +5129,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI MIT License (MIT) -------------------------------------------------------------------------------- Used by: - bit-set 0.9.1 — https://github.com/contain-rs/bit-set + bit-set 0.10.0 — https://github.com/contain-rs/bit-set Copyright (c) 2026 The Rust Project Developers @@ -5230,13 +5162,23 @@ DEALINGS IN THE SOFTWARE. MIT License (MIT) -------------------------------------------------------------------------------- Used by: - serde_spanned 1.1.1 — https://github.com/toml-rs/toml + annotate-snippets 0.12.16 — https://github.com/rust-lang/annotate-snippets-rs + anstream 1.0.0 — https://github.com/rust-cli/anstyle.git + anstyle-parse 1.0.0 — https://github.com/rust-cli/anstyle.git + anstyle-query 1.1.5 — https://github.com/rust-cli/anstyle.git + anstyle-wincon 3.0.11 — https://github.com/rust-cli/anstyle.git + anstyle 1.0.14 — https://github.com/rust-cli/anstyle.git + colorchoice 1.0.5 — https://github.com/rust-cli/anstyle.git + is_terminal_polyfill 1.70.2 — https://github.com/polyfill-rs/is_terminal_polyfill + once_cell_polyfill 1.70.2 — https://github.com/polyfill-rs/once_cell_polyfill + serde_spanned 1.1.2 — https://github.com/toml-rs/toml toml 0.9.12+spec-1.1.0 — https://github.com/toml-rs/toml + toml 1.1.8+spec-1.1.0 — https://github.com/toml-rs/toml toml_datetime 0.7.5+spec-1.1.0 — https://github.com/toml-rs/toml - toml_datetime 1.1.1+spec-1.1.0 — https://github.com/toml-rs/toml + toml_datetime 1.1.2+spec-1.1.0 — https://github.com/toml-rs/toml toml_edit 0.25.13+spec-1.1.0 — https://github.com/toml-rs/toml - toml_parser 1.1.3+spec-1.1.0 — https://github.com/toml-rs/toml - toml_writer 1.1.2+spec-1.1.0 — https://github.com/toml-rs/toml + toml_parser 1.1.5+spec-1.1.0 — https://github.com/toml-rs/toml + toml_writer 1.1.3+spec-1.1.0 — https://github.com/toml-rs/toml Copyright (c) Individual contributors @@ -5526,8 +5468,8 @@ MIT License (MIT) -------------------------------------------------------------------------------- Used by: parlance 0.1.0 — https://github.com/linebender/parley - parley 0.9.0 — https://github.com/linebender/parley - parley_data 0.9.0 — https://github.com/linebender/parley + parley 0.11.1 — https://github.com/linebender/parley + parley_data 0.11.1 — https://github.com/linebender/parley Copyright 2020 the Parley Authors @@ -5622,7 +5564,7 @@ DEALINGS IN THE SOFTWARE. MIT License (MIT) -------------------------------------------------------------------------------- Used by: - fontique 0.9.0 — https://github.com/linebender/parley + fontique 0.11.1 — https://github.com/linebender/parley Copyright 2024 the Parley Authors @@ -5718,11 +5660,12 @@ SOFTWARE. MIT License (MIT) -------------------------------------------------------------------------------- Used by: - weak-table 0.3.2 — https://github.com/tov/weak-table-rs + weak-table 0.4.0 — https://github.com/tov/weak-table-rs MIT License Copyright (c) 2018 Jesse A. Tov +Copyright 2026 Nick Mathewson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -5820,35 +5763,6 @@ The above copyright notice and this permission notice (including the next paragr THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -================================================================================ -MIT License (MIT) --------------------------------------------------------------------------------- -Used by: - guillotiere 0.6.2 — https://github.com/nical/guillotiere - -MIT License - -Copyright (c) 2019 Nicolas Silva - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ================================================================================ MIT License (MIT) -------------------------------------------------------------------------------- @@ -6259,41 +6173,14 @@ SOFTWARE. MIT License (MIT) -------------------------------------------------------------------------------- Used by: - core_maths 0.1.1 — https://github.com/robertbastian/core_maths - -MIT License - -Copyright (c) 2024 Robert Bastian - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -================================================================================ -MIT License (MIT) --------------------------------------------------------------------------------- -Used by: - naga 29.0.4 — https://github.com/gfx-rs/wgpu - wgpu-core-deps-windows-linux-android 29.0.4 — https://github.com/gfx-rs/wgpu - wgpu-core 29.0.4 — https://github.com/gfx-rs/wgpu - wgpu-hal 29.0.4 — https://github.com/gfx-rs/wgpu - wgpu-naga-bridge 29.0.4 — https://github.com/gfx-rs/wgpu - wgpu-types 29.0.4 — https://github.com/gfx-rs/wgpu - wgpu 29.0.4 — https://github.com/gfx-rs/wgpu + naga-types 30.0.1 — https://github.com/gfx-rs/wgpu + naga 30.0.1 — https://github.com/gfx-rs/wgpu + wgpu-core-deps-windows-linux-android 30.0.1 — https://github.com/gfx-rs/wgpu + wgpu-core 30.0.1 — https://github.com/gfx-rs/wgpu + wgpu-hal 30.0.1 — https://github.com/gfx-rs/wgpu + wgpu-naga-bridge 30.0.1 — https://github.com/gfx-rs/wgpu + wgpu-types 30.0.1 — https://github.com/gfx-rs/wgpu + wgpu 30.0.1 — https://github.com/gfx-rs/wgpu MIT License @@ -6351,8 +6238,8 @@ MIT License (MIT) -------------------------------------------------------------------------------- Used by: accesskit 0.24.1 — https://github.com/AccessKit/accesskit - accesskit_consumer 0.35.0 — https://github.com/AccessKit/accesskit - accesskit_windows 0.32.1 — https://github.com/AccessKit/accesskit + accesskit_consumer 0.38.0 — https://github.com/AccessKit/accesskit + accesskit_windows 0.34.0 — https://github.com/AccessKit/accesskit bcdec_rs 0.2.0 — https://github.com/ScanMountGoat/image_dds binrw 0.15.2 — https://github.com/jam1garner/binrw binrw_derive 0.15.2 — https://github.com/jam1garner/binrw @@ -6362,12 +6249,15 @@ Used by: dpi 0.1.2 — https://github.com/rust-windowing/winit gilrs-core 0.6.8 — https://gitlab.com/gilrs-project/gilrs gilrs 0.11.2 — https://gitlab.com/gilrs-project/gilrs - gpu-descriptor-types 0.2.0 — https://github.com/zakarumych/gpu-descriptor - gpu-descriptor 0.3.2 — https://github.com/zakarumych/gpu-descriptor libm 0.2.16 — https://github.com/rust-lang/compiler-builtins + mutants 0.0.3 — https://github.com/sourcefrog/cargo-mutants profiling 1.0.18 — https://github.com/aclysma/profiling - svg_fmt 0.4.5 — https://github.com/nical/rust_debug - taffy 0.10.1 — https://github.com/DioxusLabs/taffy + taffy 0.14.0 — https://github.com/DioxusLabs/taffy + wesl-core 0.1.0 — https://github.com/webgpu-tools/wesl-rs + wesl-macros 0.6.0 — https://github.com/webgpu-tools/wesl-rs + wesl 0.6.0 — https://github.com/webgpu-tools/wesl-rs + wgsl-parse 0.6.0 — https://github.com/webgpu-tools/wesl-rs + wgsl-types 0.6.0 — https://github.com/webgpu-tools/wesl-rs windows-collections 0.3.2 — https://github.com/microsoft/windows-rs windows-core 0.62.2 — https://github.com/microsoft/windows-rs windows-future 0.3.2 — https://github.com/microsoft/windows-rs @@ -6598,57 +6488,63 @@ MIT License (MIT) -------------------------------------------------------------------------------- Used by: atomicow 1.2.0 — https://github.com/bevyengine/atomicow - bevy 0.19.0 — https://github.com/bevyengine/bevy - bevy_a11y 0.19.0 — https://github.com/bevyengine/bevy - bevy_app 0.19.0 — https://github.com/bevyengine/bevy - bevy_asset 0.19.0 — https://github.com/bevyengine/bevy - bevy_asset_macros 0.19.0 — https://github.com/bevyengine/bevy - bevy_camera 0.19.0 — https://github.com/bevyengine/bevy - bevy_clipboard 0.19.0 — https://github.com/bevyengine/bevy - bevy_color 0.19.0 — https://github.com/bevyengine/bevy - bevy_core_pipeline 0.19.0 — https://github.com/bevyengine/bevy - bevy_derive 0.19.0 — https://github.com/bevyengine/bevy - bevy_diagnostic 0.19.0 — https://github.com/bevyengine/bevy - bevy_ecs 0.19.0 — https://github.com/bevyengine/bevy - bevy_ecs_macro_logic 0.19.0 - bevy_ecs_macros 0.19.0 - bevy_encase_derive 0.19.0 — https://github.com/bevyengine/bevy - bevy_gilrs 0.19.0 — https://github.com/bevyengine/bevy - bevy_image 0.19.0 — https://github.com/bevyengine/bevy - bevy_input 0.19.0 — https://github.com/bevyengine/bevy - bevy_input_focus 0.19.0 — https://github.com/bevyengine/bevy - bevy_internal 0.19.0 — https://github.com/bevyengine/bevy - bevy_light 0.19.0 — https://github.com/bevyengine/bevy - bevy_log 0.19.0 — https://github.com/bevyengine/bevy - bevy_macro_utils 0.19.0 — https://github.com/bevyengine/bevy - bevy_material 0.19.0 — https://github.com/bevyengine/bevy - bevy_material_macros 0.19.0 — https://github.com/bevyengine/bevy - bevy_math 0.19.0 — https://github.com/bevyengine/bevy - bevy_mesh 0.19.0 — https://github.com/bevyengine/bevy - bevy_picking 0.19.0 — https://github.com/bevyengine/bevy - bevy_platform 0.19.0 — https://github.com/bevyengine/bevy - bevy_ptr 0.19.0 — https://github.com/bevyengine/bevy - bevy_reflect 0.19.0 — https://github.com/bevyengine/bevy - bevy_reflect_derive 0.19.0 — https://github.com/bevyengine/bevy - bevy_render 0.19.0 — https://github.com/bevyengine/bevy - bevy_render_macros 0.19.0 — https://github.com/bevyengine/bevy - bevy_shader 0.19.0 — https://github.com/bevyengine/bevy - bevy_sprite 0.19.0 — https://github.com/bevyengine/bevy - bevy_sprite_render 0.19.0 — https://github.com/bevyengine/bevy - bevy_tasks 0.19.0 — https://github.com/bevyengine/bevy - bevy_text 0.19.0 — https://github.com/bevyengine/bevy - bevy_time 0.19.0 — https://github.com/bevyengine/bevy - bevy_transform 0.19.0 — https://github.com/bevyengine/bevy - bevy_ui 0.19.0 — https://github.com/bevyengine/bevy - bevy_ui_render 0.19.0 — https://github.com/bevyengine/bevy - bevy_ui_widgets 0.19.0 — https://github.com/bevyengine/bevy - bevy_utils 0.19.0 — https://github.com/bevyengine/bevy - bevy_window 0.19.0 — https://github.com/bevyengine/bevy - bevy_winit 0.19.0 — https://github.com/bevyengine/bevy + bevy 0.20.0 — https://github.com/bevyengine/bevy + bevy_a11y 0.20.0 — https://github.com/bevyengine/bevy + bevy_app 0.20.0 — https://github.com/bevyengine/bevy + bevy_asset 0.20.0 — https://github.com/bevyengine/bevy + bevy_asset_macros 0.20.0 — https://github.com/bevyengine/bevy + bevy_camera 0.20.0 — https://github.com/bevyengine/bevy + bevy_clipboard 0.20.0 — https://github.com/bevyengine/bevy + bevy_color 0.20.0 — https://github.com/bevyengine/bevy + bevy_core_pipeline 0.20.0 — https://github.com/bevyengine/bevy + bevy_curve 0.20.0 — https://github.com/bevyengine/bevy + bevy_derive 0.20.0 — https://github.com/bevyengine/bevy + bevy_diagnostic 0.20.0 — https://github.com/bevyengine/bevy + bevy_ecs 0.20.0 — https://github.com/bevyengine/bevy + bevy_ecs_macro_logic 0.20.0 + bevy_ecs_macros 0.20.0 + bevy_encase_derive 0.20.0 — https://github.com/bevyengine/bevy + bevy_extract 0.20.0 — https://github.com/bevyengine/bevy + bevy_extract_macros 0.20.0 — https://github.com/bevyengine/bevy + bevy_gilrs 0.20.0 — https://github.com/bevyengine/bevy + bevy_image 0.20.0 — https://github.com/bevyengine/bevy + bevy_input 0.20.0 — https://github.com/bevyengine/bevy + bevy_input_focus 0.20.0 — https://github.com/bevyengine/bevy + bevy_internal 0.20.0 — https://github.com/bevyengine/bevy + bevy_light 0.20.0 — https://github.com/bevyengine/bevy + bevy_log 0.20.0 — https://github.com/bevyengine/bevy + bevy_macro_utils 0.20.0 — https://github.com/bevyengine/bevy + bevy_material 0.20.0 — https://github.com/bevyengine/bevy + bevy_material_macros 0.20.0 — https://github.com/bevyengine/bevy + bevy_math 0.20.0 — https://github.com/bevyengine/bevy + bevy_mesh 0.20.0 — https://github.com/bevyengine/bevy + bevy_picking 0.20.0 — https://github.com/bevyengine/bevy + bevy_platform 0.20.0 — https://github.com/bevyengine/bevy + bevy_ptr 0.20.0 — https://github.com/bevyengine/bevy + bevy_reflect 0.20.0 — https://github.com/bevyengine/bevy + bevy_reflect_derive 0.20.0 — https://github.com/bevyengine/bevy + bevy_render 0.20.0 — https://github.com/bevyengine/bevy + bevy_render_macros 0.20.0 — https://github.com/bevyengine/bevy + bevy_shader 0.20.0 — https://github.com/bevyengine/bevy + bevy_shape 0.20.0 — https://github.com/bevyengine/bevy + bevy_sprite 0.20.0 — https://github.com/bevyengine/bevy + bevy_sprite_render 0.20.0 — https://github.com/bevyengine/bevy + bevy_state 0.20.0 — https://github.com/bevyengine/bevy + bevy_state_macros 0.20.0 + bevy_tasks 0.20.0 — https://github.com/bevyengine/bevy + bevy_text 0.20.0 — https://github.com/bevyengine/bevy + bevy_time 0.20.0 — https://github.com/bevyengine/bevy + bevy_transform 0.20.0 — https://github.com/bevyengine/bevy + bevy_ui 0.20.0 — https://github.com/bevyengine/bevy + bevy_ui_render 0.20.0 — https://github.com/bevyengine/bevy + bevy_ui_widgets 0.20.0 — https://github.com/bevyengine/bevy + bevy_utils 0.20.0 — https://github.com/bevyengine/bevy + bevy_window 0.20.0 — https://github.com/bevyengine/bevy + bevy_winit 0.20.0 — https://github.com/bevyengine/bevy disqualified 1.0.0 — https://github.com/bevyengine/disqualified half 2.7.1 — https://github.com/VoidStarKat/half-rs linebender_resource_handle 0.1.1 — https://github.com/linebender/raw_resource_handle - naga_oil 0.22.0 — https://github.com/bevyengine/naga_oil/ + variadics_please 2.0.0 — https://github.com/bevyengine/variadics_please MIT License @@ -6671,35 +6567,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -================================================================================ -MIT License (MIT) --------------------------------------------------------------------------------- -Used by: - grid 1.0.1 — https://github.com/becheran/grid - -MIT License - -Copyright (c) 2020 Armin Becher - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ================================================================================ MIT License (MIT) -------------------------------------------------------------------------------- @@ -6729,33 +6596,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -================================================================================ -MIT License (MIT) --------------------------------------------------------------------------------- -Used by: - variadics_please 1.1.0 — https://github.com/bevyengine/variadics_please - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ================================================================================ MIT License (MIT) -------------------------------------------------------------------------------- @@ -6806,10 +6646,17 @@ Used by: event-listener 5.4.2 — https://github.com/smol-rs/event-listener fastrand 2.5.0 — https://github.com/smol-rs/fastrand futures-lite 2.6.1 — https://github.com/smol-rs/futures-lite - glam 0.32.1 — https://github.com/bitshifter/glam-rs - hexasphere 18.0.0 — https://github.com/OptimisticPeach/hexasphere.git + glam 0.33.12 — https://github.com/bitshifter/glam-rs + hexasphere 19.0.0 — https://github.com/OptimisticPeach/hexasphere.git inventory 0.3.24 — https://github.com/dtolnay/inventory itoa 1.0.18 — https://github.com/dtolnay/itoa + lexical-core 1.0.6 — https://github.com/Alexhuszagh/rust-lexical + lexical-parse-float 1.0.6 — https://github.com/Alexhuszagh/rust-lexical + lexical-parse-integer 1.0.6 — https://github.com/Alexhuszagh/rust-lexical + lexical-util 1.0.7 — https://github.com/Alexhuszagh/rust-lexical + lexical-write-float 1.0.6 — https://github.com/Alexhuszagh/rust-lexical + lexical-write-integer 1.0.6 — https://github.com/Alexhuszagh/rust-lexical + lexical 7.0.5 — https://github.com/Alexhuszagh/rust-lexical once_cell 1.21.4 — https://github.com/matklad/once_cell parking 2.2.1 — https://github.com/smol-rs/parking pin-project-lite 0.2.17 — https://github.com/taiki-e/pin-project-lite @@ -6831,6 +6678,7 @@ Used by: thiserror 2.0.19 — https://github.com/dtolnay/thiserror typeid 1.0.3 — https://github.com/dtolnay/typeid unicode-ident 1.0.24 — https://github.com/dtolnay/unicode-ident + unsynn 0.3.0 — https://seed.pipapo.org/nodes/seed.pipapo.org/rad:z39WbeupErKS8TwbDS5yU8eZSa3C zmij 1.0.23 — https://github.com/dtolnay/zmij Permission is hereby granted, free of charge, to any @@ -6963,7 +6811,7 @@ SOFTWARE. MIT License (MIT) -------------------------------------------------------------------------------- Used by: - spin 0.10.1 — https://github.com/mvdnes/spin-rs.git + spin 0.12.3 — https://codeberg.org/zesterer/spin The MIT License (MIT) @@ -7113,7 +6961,6 @@ Used by: byteorder-lite 0.1.0 — https://github.com/image-rs/byteorder-lite byteorder 1.5.0 — https://github.com/BurntSushi/byteorder memchr 2.8.3 — https://github.com/BurntSushi/memchr - termcolor 1.4.1 — https://github.com/BurntSushi/termcolor The MIT License (MIT) @@ -7138,36 +6985,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -================================================================================ -MIT License (MIT) --------------------------------------------------------------------------------- -Used by: - data-encoding 2.11.1 — https://github.com/ia0/data-encoding - -The MIT License (MIT) - -Copyright (c) 2015-2020 Julien Cretin -Copyright (c) 2017-2020 Google Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ================================================================================ MIT License (MIT) -------------------------------------------------------------------------------- @@ -7202,29 +7019,28 @@ SOFTWARE. MIT License (MIT) -------------------------------------------------------------------------------- Used by: - winapi-util 0.1.11 — https://github.com/BurntSushi/winapi-util + guillotiere 0.7.0 — https://github.com/nical/guillotiere The MIT License (MIT) -Copyright (c) 2017 Andrew Gallant +Copyright (c) 2019 Nicolas Silva -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ @@ -7326,7 +7142,7 @@ THE SOFTWARE. MIT License (MIT) -------------------------------------------------------------------------------- Used by: - harfrust 0.6.2 — https://github.com/harfbuzz/harfrust + harfrust 0.12.0 — https://github.com/harfbuzz/harfrust The MIT License (MIT) @@ -8094,7 +7910,6 @@ freely, subject to the following restrictions: zlib License (Zlib) -------------------------------------------------------------------------------- Used by: - foldhash 0.1.5 — https://github.com/orlp/foldhash foldhash 0.2.0 — https://github.com/orlp/foldhash Copyright (c) 2024 Orson Peters diff --git a/crates/approved_tests/README.md b/crates/approved_tests/README.md index df91550..f7974cb 100644 --- a/crates/approved_tests/README.md +++ b/crates/approved_tests/README.md @@ -24,6 +24,13 @@ nowhere else: maps, seed rule, scenes, positions, angles, weapons, durations and input. To pin a scene to an owner-chosen position, replace its `Place` there; the runner does not change. +The heavy scenario also plants a claymore, drops a weapon through player death, +restarts the script round in place, and captures entity and resource ownership at gameplay and teardown boundaries. +Both scenarios use isolated accounts and wait for native progression content and run `unlock all` from the menu before +loading their map, so random showcase classes are admitted without altering a +user account. Dumps record slot, payload, script-object and remote-animation counts; the runner +checks entity consistency and complete teardown of these match owners. + The final `quit` phase sends `finish_run`, which uses the normal process-exit boundary after the scenario's pending screenshots finish writing. Interactive `quit` intentionally has only a short write grace period and can leave a @@ -51,7 +58,11 @@ it is a symlink to the repository's cache. A seed resolves to maps and spawn picks once. `run.json` keeps the resolved set — the positions and angles the game actually used — and `--replay` runs that set again rather than the seed, so a change in catalog order or spawn -selection cannot move a replayed scene. +selection cannot move a replayed scene. Replay also restores the source run’s +`iw4l-artifacts/profile/classes.txt` so class indices keep the same loadouts. +The source profile must exist; its contents and path are recorded under +`class_profile` in the new report. Combat outcomes and frame timing can still +vary between runs. ## What a run leaves diff --git a/crates/approved_tests/src/main.rs b/crates/approved_tests/src/main.rs index 057a0a8..a11aa57 100644 --- a/crates/approved_tests/src/main.rs +++ b/crates/approved_tests/src/main.rs @@ -306,12 +306,27 @@ fn run(root: &Path, args: &Args) -> Result { let scenes_b = resolve_scenes("b", &map_b, 1, seed, replay.as_ref()); let phases = hgl::phases(&map_b, &scenes_a, &scenes_b); let script = scenario::script(&phases); - let child_args = vec![ - "map".to_owned(), - SCENARIO.map_a.to_owned(), - "--cmds".to_owned(), - script.clone(), - ]; + let child_args = vec!["menu".to_owned(), "--cmds".to_owned(), script.clone()]; + + if let Some(replay) = &replay { + let source = replay + .source + .parent() + .ok_or("replay: no parent directory")? + .join("iw4l-artifacts/profile/classes.txt"); + let contents = std::fs::read_to_string(&source) + .map_err(|e| format!("replay class profile {}: {e}", source.display()))?; + let destination = run_dir.join("iw4l-artifacts/profile/classes.txt"); + std::fs::create_dir_all(destination.parent().expect("has parent")) + .map_err(|e| format!("create replay profile directory: {e}"))?; + std::fs::write(&destination, &contents) + .map_err(|e| format!("write replay profile {}: {e}", destination.display()))?; + manifest["class_profile"] = json!({ + "source": source.display().to_string(), + "path": destination.display().to_string(), + "contents": contents, + }); + } let cache = prepare_cache(root, &run_dir, args.cache)?; manifest["cache"] = cache; @@ -339,6 +354,14 @@ fn run(root: &Path, args: &Args) -> Result { env: vec![ ("IW4L_PERF", "1".into()), ("IW4L_GAMETYPE", SCENARIO.gametype.into()), + ( + "IW4L_SETTINGS_PATH", + run_dir.join("settings.cfg").display().to_string(), + ), + ( + "IW4L_ACCOUNT_PATH", + run_dir.join("account.dat").display().to_string(), + ), ], phase_timeout: Duration::from_secs(SCENARIO.phase_timeout_secs), quit_timeout: Duration::from_secs(SCENARIO.quit_timeout_secs), diff --git a/crates/approved_tests/src/report.rs b/crates/approved_tests/src/report.rs index 9792239..301588f 100644 --- a/crates/approved_tests/src/report.rs +++ b/crates/approved_tests/src/report.rs @@ -683,11 +683,62 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: & png_ok, png_to.display().to_string(), ); + let lifetime_text = std::fs::read_to_string(&dump_to).unwrap_or_default(); + let lifetimes: BTreeMap<_, _> = lifetime_text + .lines() + .filter_map(|line| line.split_once(" = ")) + .filter(|(key, _)| { + matches!( + *key, + "occupied_slots" + | "event_holds" + | "projectile_payloads" + | "item_payloads" + | "mover_payloads" + | "script_objects" + | "script_defined_entities" + | "entity_consistency" + | "retirement_serial" + | "retired_resources" + | "scope_resources_live" + | "remote_tree_bindings" + | "remote_tree_storage_bytes" + ) + }) + .map(|(key, value)| (key.to_owned(), value.to_owned())) + .collect(); + asserts.check( + &format!("lifetime.{capture}.consistent"), + lifetimes + .get("entity_consistency") + .is_some_and(|value| value == "Ok(())"), + json!(lifetimes), + ); + if capture == "02_after_disconnect" { + for key in [ + "occupied_slots", + "event_holds", + "projectile_payloads", + "item_payloads", + "mover_payloads", + "script_objects", + "remote_tree_bindings", + "scope_resources_live", + ] { + asserts.check( + &format!("lifetime.teardown.{key}"), + lifetimes.get(key).is_some_and(|value| value == "0"), + json!(lifetimes.get(key)), + ); + } + } captures.insert( capture.to_owned(), json!({ "screenshot": png_ok.then(|| png_to.display().to_string()), "dump": dump_ok.then(|| dump_to.display().to_string()), + "lifetimes": lifetimes, + "scope_rows": lifetime_text.lines().filter(|line| line.starts_with("resource = ")).collect::>(), }), ); } diff --git a/crates/approved_tests/src/scenarios/heavy_gameplay_lifecycle.rs b/crates/approved_tests/src/scenarios/heavy_gameplay_lifecycle.rs index 118fe1d..e9bf1f3 100644 --- a/crates/approved_tests/src/scenarios/heavy_gameplay_lifecycle.rs +++ b/crates/approved_tests/src/scenarios/heavy_gameplay_lifecycle.rs @@ -59,6 +59,15 @@ pub fn phases(map_b: &str, scenes_a: &[ResolvedScene], scenes_b: &[ResolvedScene ] }; let mut out = vec![ + Phase::new( + "account_fixture", + vec![ + "wait progression".into(), + "unlock all".into(), + format!("map {}", s.map_a), + "wait world".into(), + ], + ), Phase::new("a.cold_load", vec!["wait world".into()]), captured("capture_00", "00_cold_load"), Phase::new("a.populate", populate()), @@ -68,7 +77,35 @@ pub fn phases(map_b: &str, scenes_a: &[ResolvedScene], scenes_b: &[ResolvedScene .iter() .map(|scene| Phase::new(scene.phase(), scene.commands.clone())), ); + out.push(Phase::new( + "a.entity_lifetimes", + vec![ + "force_spawn random 75".into(), + "wait 2t".into(), + "give weapon/claymore".into(), + "wait 2t".into(), + "press +frag".into(), + "wait 80t".into(), + "dump 01a_equipment_live".into(), + "kill".into(), + "wait 10t".into(), + "dump 01b_dropped_weapon".into(), + "force_spawn random 79".into(), + "wait 2t".into(), + ], + )); out.push(captured("capture_01", "01_overgrown_gameplay")); + out.push(Phase::new( + "a.round_restart", + vec![ + "round_restart".into(), + "wait 40t".into(), + "spawn 0".into(), + "force_match_start".into(), + "wait 40t".into(), + ], + )); + out.push(captured("capture_round", "01c_after_round_restart")); out.push(Phase::new( "disconnect", vec!["disconnect".into(), "wait torn".into()], diff --git a/crates/approved_tests/src/scenarios/master_duo_chaos.rs b/crates/approved_tests/src/scenarios/master_duo_chaos.rs index c4a2f29..150629d 100644 --- a/crates/approved_tests/src/scenarios/master_duo_chaos.rs +++ b/crates/approved_tests/src/scenarios/master_duo_chaos.rs @@ -267,14 +267,14 @@ fn execute( ) -> Result<()> { let mut host = launch(binary, cwd, run, games, descriptor, "host")?; host.send( - "set ui_mapname iw4:mp_boneyard; set ui_gametype dm; ui_create_lobby; ui_lobby_privacy", + "wait progression; unlock all; set ui_mapname iw4:mp_boneyard; set ui_gametype dm; ui_create_lobby; ui_lobby_privacy", )?; let status = host.wait(|s, _| field(s, "state") == Some("hosting"))?; let room = field(&status, "room").ok_or("missing room ID")?.to_owned(); manifest["room"] = room.clone().into(); let mut client = launch(binary, cwd, run, games, descriptor, "client")?; client.send(&format!( - "set ui_mapname iw4:mp_boneyard; set ui_gametype dm; ui_join_lobby_id {room}" + "wait progression; unlock all; set ui_mapname iw4:mp_boneyard; set ui_gametype dm; ui_join_lobby_id {room}" ))?; client.wait(|s, _| field(s, "state") == Some("joined"))?; host.wait(|s, _| field(s, "members") == Some("2"))?; diff --git a/crates/asset_core/src/lib.rs b/crates/asset_core/src/lib.rs index 6284210..9d328ed 100644 --- a/crates/asset_core/src/lib.rs +++ b/crates/asset_core/src/lib.rs @@ -1,3 +1,5 @@ +pub mod map_content; +pub use map_content::{GlassContent, GlassDefinition, GlassPaneBasis, MapContentDefinition}; pub mod family; pub use family::{Family, FamilyId, Iw4, Iw5, T5, T6}; pub mod asset_key; diff --git a/crates/asset_core/src/map_content.rs b/crates/asset_core/src/map_content.rs new file mode 100644 index 0000000..36cdb47 --- /dev/null +++ b/crates/asset_core/src/map_content.rs @@ -0,0 +1,90 @@ +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct GlassPaneBasis { + pub origin: [f32; 3], + pub axis_s: [f32; 3], + pub axis_t: [f32; 3], +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct MapContentDefinition { + pub glass: GlassContent, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum GlassContent { + Absent, + #[default] + Unsupported, + Invalid(String), + Prepared(GlassDefinition), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct GlassDefinition { + panes: Vec, + names: BTreeMap>, + collision_pieces: BTreeSet, +} + +impl GlassDefinition { + pub fn checked( + panes: Vec, + names: Vec<(String, Vec)>, + collision_pieces: impl IntoIterator, + ) -> Result { + for (id, pane) in panes.iter().enumerate() { + if !pane + .origin + .iter() + .chain(&pane.axis_s) + .chain(&pane.axis_t) + .all(|value| value.is_finite()) + { + return Err(format!("glass piece {id} has non-finite geometry")); + } + let norm = |axis: [f32; 3]| axis.iter().map(|v| v * v).sum::(); + if norm(pane.axis_s) < 0.5 || norm(pane.axis_t) < 0.5 { + return Err(format!("glass piece {id} has a degenerate plane")); + } + } + let collision_pieces: BTreeSet<_> = collision_pieces.into_iter().collect(); + for id in &collision_pieces { + if *id as usize >= panes.len() { + return Err(format!("glass collision piece {id} has no visual plane")); + } + } + let mut named = BTreeMap::new(); + for (name, pieces) in names { + if name.is_empty() || named.contains_key(&name) { + return Err(format!("invalid or duplicate glass name {name:?}")); + } + let mut seen = BTreeSet::new(); + for id in &pieces { + if *id as usize >= panes.len() || !seen.insert(*id) { + return Err(format!("glass set {name:?} has invalid piece {id}")); + } + } + named.insert(name, pieces); + } + Ok(Self { + panes, + names: named, + collision_pieces, + }) + } + + pub fn panes(&self) -> &[GlassPaneBasis] { + &self.panes + } + pub fn named(&self, name: &str) -> &[u32] { + self.names.get(name).map_or(&[], Vec::as_slice) + } + pub fn names(&self) -> &BTreeMap> { + &self.names + } + pub fn collision_pieces(&self) -> &BTreeSet { + &self.collision_pieces + } +} diff --git a/crates/asset_game/Cargo.toml b/crates/asset_game/Cargo.toml index c202374..354df61 100644 --- a/crates/asset_game/Cargo.toml +++ b/crates/asset_game/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true description = "Weapons, attachments, penetration, localization, menus, teams, and stats." [dependencies] +gamemode_iw4.workspace = true structured_data_iw4.workspace = true asset_core.workspace = true asset_transport.workspace = true diff --git a/crates/asset_game/src/menu_catalog.rs b/crates/asset_game/src/menu_catalog.rs index 4c2b929..e964b31 100644 --- a/crates/asset_game/src/menu_catalog.rs +++ b/crates/asset_game/src/menu_catalog.rs @@ -1724,3 +1724,12 @@ impl AssetLinkSink for MenuSink { Ok(()) } } + +impl gamemode_iw4::progression::ProgressionTable for CapturedStringTable { + fn rows(&self) -> usize { + self.rows + } + fn cell(&self, row: usize, column: usize) -> &str { + self.cell(row as i32, column as i32) + } +} diff --git a/crates/asset_game/src/weapon_catalog.rs b/crates/asset_game/src/weapon_catalog.rs index 256dec3..4e20779 100644 --- a/crates/asset_game/src/weapon_catalog.rs +++ b/crates/asset_game/src/weapon_catalog.rs @@ -1569,6 +1569,8 @@ pub struct WeaponRegistry { world_catalog_identity: u64, fpv_catalog_identity: u64, family_tables: Arc<[(crate::AssetNamespace, crate::CapturedStringTable)]>, + shared_ranks: std::sync::OnceLock>, + shared_icons: std::sync::OnceLock>, iw5_attachments: HashMap, diff --git a/crates/asset_game/src/weapon_catalog/editor.rs b/crates/asset_game/src/weapon_catalog/editor.rs index 8544bcc..9c86a32 100644 --- a/crates/asset_game/src/weapon_catalog/editor.rs +++ b/crates/asset_game/src/weapon_catalog/editor.rs @@ -63,6 +63,25 @@ impl FamilyContent for EditorWeaponCatalog { } impl EditorWeaponCatalog { + pub fn shared_rank_table(&self) -> Result<&crate::CapturedStringTable, &'static str> { + self.registry.shared_rank_table() + } + pub fn shared_rank_icons(&self) -> Result<&crate::CapturedStringTable, &'static str> { + self.registry.shared_rank_icons() + } + pub fn rank_progression( + &self, + ) -> Result { + self.registry.rank_progression() + } + pub fn item_unlock_requirement( + &self, + namespace: crate::AssetNamespace, + name: &str, + ) -> Result { + self.registry.item_unlock_requirement(namespace, name) + } + pub fn len(&self) -> usize { self.registry.len() } diff --git a/crates/asset_game/src/weapon_catalog/presentation.rs b/crates/asset_game/src/weapon_catalog/presentation.rs index 0d994e5..6b502cc 100644 --- a/crates/asset_game/src/weapon_catalog/presentation.rs +++ b/crates/asset_game/src/weapon_catalog/presentation.rs @@ -21,6 +21,7 @@ pub struct WeaponHudFacts { pub ammo_counter_clip: i32, pub ammo_index: i32, pub can_hold_breath: bool, + pub cooks_grenade: bool, pub scope_zoom: weapon_iw4::ScopeZoom, pub clip_index: i32, pub clip_only: bool, @@ -68,6 +69,7 @@ impl WeaponHudFacts { ammo_counter_clip: f.ammo_counter_clip, ammo_index: f.ammo_index, can_hold_breath: f.can_hold_breath, + cooks_grenade: f.weap_type == weapon_iw4::WEAPTYPE_GRENADE && f.cook_off_hold, scope_zoom: f.scope_zoom, clip_index: f.clip_index, clip_only: f.clip_only, diff --git a/crates/asset_game/src/weapon_catalog/publication.rs b/crates/asset_game/src/weapon_catalog/publication.rs index 05d516d..da6a765 100644 --- a/crates/asset_game/src/weapon_catalog/publication.rs +++ b/crates/asset_game/src/weapon_catalog/publication.rs @@ -463,6 +463,8 @@ impl WeaponBuild { world_catalog_identity: 0, fpv_catalog_identity: 0, family_tables: Arc::default(), + shared_ranks: Default::default(), + shared_icons: Default::default(), iw5_attachments, configurations: HashMap::new(), by_name: index_of, diff --git a/crates/asset_game/src/weapon_catalog/registry.rs b/crates/asset_game/src/weapon_catalog/registry.rs index 7e2b20c..d443e7b 100644 --- a/crates/asset_game/src/weapon_catalog/registry.rs +++ b/crates/asset_game/src/weapon_catalog/registry.rs @@ -1,6 +1,239 @@ use super::*; impl WeaponRegistry { + pub fn shared_rank_table(&self) -> Result<&crate::CapturedStringTable, &'static str> { + self.shared_ranks + .get_or_init(|| self.capture_shared_ranks()) + .as_ref() + .map_err(|error| *error) + } + + fn capture_shared_ranks(&self) -> Result { + let host = self + .family_tables + .iter() + .rev() + .find(|(ns, table)| { + *ns == crate::AssetNamespace::Iw4 + && table.name.eq_ignore_ascii_case("mp/rankTable.csv") + }) + .map(|(_, table)| table) + .ok_or("rank.missing_table")?; + let maximum = |table: &crate::CapturedStringTable| { + (0..table.rows as i32) + .filter_map(|row| table.cell(row, 0).parse::().ok()) + .max() + }; + let host_max = maximum(host).ok_or("rank.invalid_table")?; + let donor = self + .family_tables + .iter() + .filter(|(_, table)| table.name.eq_ignore_ascii_case("mp/rankTable.csv")) + .filter_map(|(_, table)| maximum(table).map(|rank| (rank, table))) + .max_by_key(|(rank, _)| *rank) + .ok_or("rank.missing_table")?; + let mut shared = host.clone(); + if donor.0 > host_max { + let last = host + .lookup_row_in_col(0, &host_max.to_string()) + .ok_or("rank.invalid_table")?; + let first = donor + .1 + .lookup_row_in_col(0, &(host_max + 1).to_string()) + .ok_or("rank.invalid_table")?; + let next_xp = host + .cell(last, 7) + .parse::() + .map_err(|_| "rank.invalid_threshold")?; + let donor_xp = donor + .1 + .cell(first, 2) + .parse::() + .map_err(|_| "rank.invalid_threshold")?; + let offset = next_xp + .checked_sub(donor_xp) + .ok_or("rank.invalid_threshold")?; + for rank in host_max + 1..=donor.0 { + let row = donor + .1 + .lookup_row_in_col(0, &rank.to_string()) + .ok_or("rank.invalid_table")?; + let mut cells = (0..host.columns as i32) + .map(|column| donor.1.cell(row, column).to_owned()) + .collect::>(); + for column in [2, 7] { + cells[column] = cells[column] + .parse::() + .ok() + .and_then(|xp| xp.checked_add(offset)) + .ok_or("rank.invalid_threshold")? + .to_string(); + } + cells[6] = host.cell(last, 6).to_owned(); + shared.cells.extend(cells); + shared.rows += 1; + } + let max_row = shared + .lookup_row_in_col(0, "maxrank") + .ok_or("rank.invalid_table")?; + shared.cells[max_row as usize * shared.columns + 1] = donor.0.to_string(); + } + gamemode_iw4::progression::RankProgression::capture(&shared)?; + Ok(shared) + } + + pub fn shared_rank_icons(&self) -> Result<&crate::CapturedStringTable, &'static str> { + self.shared_icons + .get_or_init(|| self.capture_shared_icons()) + .as_ref() + .map_err(|error| *error) + } + + fn capture_shared_icons(&self) -> Result { + let mut icons = self + .family_tables + .iter() + .rev() + .find(|(ns, table)| { + *ns == crate::AssetNamespace::Iw4 + && table.name.eq_ignore_ascii_case("mp/rankIconTable.csv") + }) + .map(|(_, table)| table.clone()) + .ok_or("rank.missing_icons")?; + let max = (0..icons.rows as i32) + .filter_map(|row| icons.cell(row, 0).parse::().ok()) + .max() + .ok_or("rank.invalid_icons")?; + let shared = self.shared_rank_table()?; + let shared_max = shared + .lookup_col("maxrank", 1) + .parse::() + .map_err(|_| "rank.invalid_table")?; + let last = icons + .lookup_row_in_col(0, &max.to_string()) + .ok_or("rank.invalid_icons")?; + let last = (0..icons.columns as i32) + .map(|column| icons.cell(last, column).to_owned()) + .collect::>(); + for rank in max + 1..=shared_max { + let mut row = last.clone(); + row[0] = rank.to_string(); + icons.cells.extend(row); + icons.rows += 1; + } + Ok(icons) + } + + pub fn rank_progression( + &self, + ) -> Result { + gamemode_iw4::progression::RankProgression::capture(self.shared_rank_table()?) + } + + pub fn item_unlock_requirement( + &self, + namespace: crate::AssetNamespace, + name: &str, + ) -> Result { + let infinity = matches!( + namespace, + crate::AssetNamespace::Iw4 | crate::AssetNamespace::Iw5 + ); + let table_name = if infinity { + "mp/unlockTable.csv" + } else { + "mp/statsTable.csv" + }; + let table = self + .family_tables + .iter() + .rev() + .find(|(ns, t)| *ns == namespace && t.name.eq_ignore_ascii_case(table_name)) + .ok_or("unlock.missing_table")?; + let column = if infinity { 0 } else { 4 }; + let row = table + .1 + .lookup_row_in_col(column, name) + .or_else(|| table.1.lookup_row_in_col(column, &format!("{name}_mp"))) + .or_else(|| { + infinity + .then(|| { + table.1.lookup_row_in_col( + column, + &format!("specialty_{}", name.replace('_', "")), + ) + }) + .flatten() + }); + let Some(row) = row else { + let native_perk = infinity + && self.family_tables.iter().any(|(ns, table)| { + *ns == namespace + && table.name.eq_ignore_ascii_case("mp/perkTable.csv") + && table.lookup_row_in_col(1, name).is_some() + }); + return if native_perk { + Ok(Default::default()) + } else { + Err("unlock.missing_item") + }; + }; + if infinity { + return gamemode_iw4::progression::UnlockRequirement::capture( + table.1.cell(row, 2), + table.1.cell(row, 3), + ); + } + let native = &table.1; + let rank = native.cell(row, 10); + let mut requirement = gamemode_iw4::progression::UnlockRequirement::capture( + if rank.is_empty() { "0" } else { rank }, + "", + )?; + if namespace == crate::AssetNamespace::T5 { + let classified = native.cell(row, 15); + let purchases = if classified.is_empty() { + 0 + } else { + classified + .parse::() + .map_err(|_| "unlock.invalid_classified")? + }; + if purchases > 0 { + let group = native.cell(row, 2); + let ranks = (0..native.rows as i32) + .filter(|&other| native.cell(other, 2) == group) + .filter(|&other| !native.cell(other, 10).is_empty()) + .map(|other| { + native + .cell(other, 10) + .parse::() + .map_err(|_| "unlock.invalid_rank") + }) + .collect::, _>>()?; + requirement.rank = ranks + .into_iter() + .max() + .ok_or("unlock.missing_classified_group")?; + } + } + Ok(requirement) + } + + pub fn weapon_unlock_requirement( + &self, + id: u32, + ) -> Result { + if id == 0 { + return Ok(Default::default()); + } + let selection = self + .describe_configuration(id) + .ok_or("unlock.missing_family")?; + let family = selection.family.as_ref().ok_or("unlock.missing_family")?; + self.item_unlock_requirement(family.namespace, &family.base) + } + pub fn vehicle_compass(&self) -> impl Iterator { self.vehicle_compass .iter() diff --git a/crates/asset_material/src/material_compile/technique.rs b/crates/asset_material/src/material_compile/technique.rs index 49eea84..5987184 100644 --- a/crates/asset_material/src/material_compile/technique.rs +++ b/crates/asset_material/src/material_compile/technique.rs @@ -15,6 +15,9 @@ pub(super) trait MaterialCompiler { fn compile_state(&self, words: [u32; 2]) -> render_material::CompiledPassState; fn color_space(&self, slot: u8) -> PassColorSpace; fn hardware_shadow_compare(&self) -> bool; + fn depth_to_colour(&self, _slot: u8) -> bool { + false + } } pub(super) fn compile_technique( @@ -160,6 +163,7 @@ pub(super) fn compile_technique( arguments, color_space: compiler.color_space(slot), hardware_shadow_compare: compiler.hardware_shadow_compare(), + depth_to_colour: compiler.depth_to_colour(slot), }; sort_pass_args(&mut runtime_pass); diff --git a/crates/asset_material/src/material_compile/technique/t5.rs b/crates/asset_material/src/material_compile/technique/t5.rs index 2170424..9aba29a 100644 --- a/crates/asset_material/src/material_compile/technique/t5.rs +++ b/crates/asset_material/src/material_compile/technique/t5.rs @@ -39,4 +39,7 @@ impl MaterialCompiler for T5Compiler { fn hardware_shadow_compare(&self) -> bool { true } + fn depth_to_colour(&self, slot: u8) -> bool { + slot == 3 + } } diff --git a/crates/asset_material/src/material_images/materialization.rs b/crates/asset_material/src/material_images/materialization.rs index 19d85c0..a6fb047 100644 --- a/crates/asset_material/src/material_images/materialization.rs +++ b/crates/asset_material/src/material_images/materialization.rs @@ -201,7 +201,17 @@ pub(super) fn wrap_payload(payload: &PreparedPayload, wrap: WrapRecipe) -> Arc { let mut state = state.lock().expect("prepared mip cache is not poisoned"); - if let Some((_, image)) = state.images.iter().find(|(recipe, _)| *recipe == wrap) { + if let Some((_, image)) = state.images.iter().find(|(recipe, image)| { + *recipe == wrap + || (image.texture_descriptor.format + == texture_format(state.mips.layout().storage, wrap.linear()) + && image.sampler + == ImageSampler::Descriptor(sampler_from_iw4( + wrap.sampler_state, + state.mips.layout().levels, + wrap.alpha_test_color, + ))) + }) { return Arc::clone(image); } if state.images.is_empty() { diff --git a/crates/asset_material/src/t5_tech_map.rs b/crates/asset_material/src/t5_tech_map.rs index b5f8963..669b56f 100644 --- a/crates/asset_material/src/t5_tech_map.rs +++ b/crates/asset_material/src/t5_tech_map.rs @@ -517,6 +517,7 @@ pub fn iw4_slot_to_t5(iw4_slot: usize) -> Option { return None; } let iw4_slot = match iw4_slot { + 3 => 2, 6 | 8 => iw4_slot - 1, 10..=36 if iw4_slot % 2 == 0 => iw4_slot - 1, _ => iw4_slot, diff --git a/crates/asset_material/src/t6_techset.rs b/crates/asset_material/src/t6_techset.rs index 56573f4..68cc071 100644 --- a/crates/asset_material/src/t6_techset.rs +++ b/crates/asset_material/src/t6_techset.rs @@ -1,6 +1,6 @@ mod material; pub use material::T6MaterialRefusal; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use dxbc_sm5::wgsl::{ConstantRow, Shader, TextureDimension, TextureSlot}; use dxbc_sm5::{BindingKind, Reflection}; @@ -842,25 +842,29 @@ impl MaterialCatalog { let mut gaps = BTreeSet::new(); let mut slots = vec![None; IW4_TECHNIQUE_TYPE_COUNT]; let mut table = TechniqueTable::default(); + let mut compiled = BTreeMap::new(); for (iw4_slot, iw4_name) in IW4_TECHNIQUE_TYPE_NAMES.iter().enumerate() { let Some(selection) = select_t6_technique(set, draw, iw4_name) else { continue; }; - let technique = set.techniques[usize::from(selection.slot)] - .as_ref() - .unwrap(); - let passes: Result, String> = (0u8..) - .zip(&technique.passes) - .map(|(index, pass)| { - let layer_routing = if layered { - &pass.layer_routing[..] - } else { - &[] - }; - let vertex_decl = self.link_vertex_decl(t6_vertex_decl(layer_routing)); - self.t6_pass(pass, index, vertex_decl, &mut gaps, namespace) - }) - .collect(); + let passes: &Result, String> = + compiled.entry(selection.slot).or_insert_with(|| { + let technique = set.techniques[usize::from(selection.slot)] + .as_ref() + .unwrap(); + (0u8..) + .zip(&technique.passes) + .map(|(index, pass)| { + let layer_routing = if layered { + &pass.layer_routing[..] + } else { + &[] + }; + let vertex_decl = self.link_vertex_decl(t6_vertex_decl(layer_routing)); + self.t6_pass(pass, index, vertex_decl, &mut gaps, namespace) + }) + .collect() + }); match passes { Ok(passes) => { table.slots |= 1 << iw4_slot; @@ -874,7 +878,7 @@ impl MaterialCatalog { } else { 0 }, - passes, + passes: passes.clone(), body_scanned: true, }); } diff --git a/crates/asset_world/src/glass_catalog.rs b/crates/asset_world/src/glass_catalog.rs index 914175f..e2ee6ed 100644 --- a/crates/asset_world/src/glass_catalog.rs +++ b/crates/asset_world/src/glass_catalog.rs @@ -152,8 +152,6 @@ pub struct FxGlassReset { pub piece_limit: usize, pub geo_data_limit: usize, - - pub names: Vec<(String, Vec)>, } impl FxGlassReset { @@ -265,34 +263,112 @@ pub fn build_fx_glass_reset(stream: &ZoneStream<'_>) -> Option { geo_cursor, piece_limit: g.piece_limit, geo_data_limit: g.geo_data_limit, - names: glass_names(stream), }) } -fn glass_names(s: &ZoneStream<'_>) -> Vec<(String, Vec)> { - let Some(g) = s.glass_data() else { - return Vec::new(); - }; - let Some(rows) = g.names else { - return Vec::new(); - }; - let mut out = Vec::with_capacity(g.name_count); - for i in 0..g.name_count { - let row = rows.at(i * s.layout(sz::G_GLASS_NAME, 24)); - let Some(name) = cstr_field(s, row, 0).filter(|name| !name.is_empty()) else { - continue; - }; - let count = s.u16_at(row, s.layout(6, 10)).unwrap_or(0) as usize; - let pieces = match s.ptr_at(row, s.layout(8, 16)) { - Ok(ZonePtr::Offset(q)) => { - let q = s.resolve_alias(q); - (0..count).map_while(|n| s.u16_at(q, n * 2).ok()).collect() +pub fn build_map_content( + s: &ZoneStream<'_>, + visual: Option<&FxGlassReset>, + clip: Option<&ClipCollision>, +) -> asset_core::MapContentDefinition { + let prepare = || -> Result { + let collision: Vec<_> = clip + .into_iter() + .flat_map(|clip| &clip.brushes) + .filter_map(|brush| brush.glass_encoded.checked_sub(1)) + .map(u32::from) + .collect(); + let Some(visual) = visual else { + if s.fx_world().is_some() || s.glass_data().is_some() || !collision.is_empty() { + return Err("glass data has no complete visual geometry".into()); } - _ => Vec::new(), + return Ok(asset_core::GlassContent::Absent); }; - out.push((name, pieces)); + let panes: Vec<_> = (0..visual.piece_places.len()) + .map(|id| { + let (origin, axis_s, axis_t) = visual + .pane_basis(id) + .ok_or_else(|| format!("glass piece {id} has no plane"))?; + Ok(asset_core::GlassPaneBasis { + origin, + axis_s, + axis_t, + }) + }) + .collect::>()?; + if let Some(clip) = clip { + for id in &collision { + let pane = panes + .get(*id as usize) + .ok_or_else(|| format!("glass collision piece {id} has no visual plane"))?; + let aligned = clip + .brushes + .iter() + .filter(|brush| u32::from(brush.glass_encoded) == id + 1) + .any(|brush| { + brush.planes.iter().all(|plane| { + plane.iter().all(|value| value.is_finite()) + && pane.origin[0] * plane[0] + + pane.origin[1] * plane[1] + + pane.origin[2] * plane[2] + - plane[3] + <= 1.0 + }) + }); + if !aligned { + return Err(format!( + "glass piece {id} visual origin is outside its collision brushes" + )); + } + } + } + let mut names = Vec::new(); + if let Some(g) = s.glass_data() { + if g.piece_count != panes.len() { + return Err(format!( + "game glass pieces {} != visual pieces {}", + g.piece_count, + panes.len() + )); + } + if g.name_count > 0 { + let rows = g.names.ok_or("glass name rows absent")?; + for i in 0..g.name_count { + let row = rows.at(i * s.layout(sz::G_GLASS_NAME, 24)); + let name = + cstr_field(s, row, 0).ok_or_else(|| format!("glass name {i} invalid"))?; + let count = + s.u16_at(row, s.layout(6, 10)) + .map_err(|error| error.to_string())? as usize; + let pieces = if count == 0 { + Vec::new() + } else { + let q = match s + .ptr_at(row, s.layout(8, 16)) + .map_err(|error| error.to_string())? + { + ZonePtr::Offset(q) => s.resolve_alias(q), + _ => return Err(format!("glass set {name:?} has no piece indices")), + }; + (0..count) + .map(|n| { + s.u16_at(q, n * 2) + .map(u32::from) + .map_err(|error| error.to_string()) + }) + .collect::, _>>()? + }; + names.push((name, pieces)); + } + } + } + Ok(asset_core::GlassContent::Prepared( + asset_core::GlassDefinition::checked(panes, names, collision)?, + )) + }; + asset_core::MapContentDefinition { + glass: prepare().unwrap_or_else(asset_core::GlassContent::Invalid), } - out } fn glass_material_edge( diff --git a/crates/assets/src/lane/iw4.rs b/crates/assets/src/lane/iw4.rs index 30834da..6939e18 100644 --- a/crates/assets/src/lane/iw4.rs +++ b/crates/assets/src/lane/iw4.rs @@ -361,6 +361,11 @@ impl ZoneLane for Iw4Lane { }); report.extend(image_report.into_iter().flatten()); report.extend(clip_report); + let map_content = std::sync::Arc::new(asset_world::build_map_content( + &stream, + fx_glass.as_ref(), + clip.as_ref(), + )); let fx_glass = fx_glass.map(|mut g| { g.resolve_material_edges(&materials); g @@ -414,6 +419,7 @@ impl ZoneLane for Iw4Lane { fx: std::mem::take(&mut fx), fx_models, fx_glass, + map_content, impact_fx: impact_fx.take(), dyn_ents, exp_fog, @@ -652,6 +658,7 @@ impl ZoneLane for Iw4Lane { fx, fx_models, fx_glass, + map_content, impact_fx, reflection_probe_images, intermission_view, @@ -699,6 +706,7 @@ impl ZoneLane for Iw4Lane { fx, fx_models, fx_glass, + map_content, impact_fx, dyn_ents, exp_fog, diff --git a/crates/assets/src/lane/sink.rs b/crates/assets/src/lane/sink.rs index f1003a6..3825494 100644 --- a/crates/assets/src/lane/sink.rs +++ b/crates/assets/src/lane/sink.rs @@ -9,6 +9,11 @@ use super::helpers::MapXModelCatalog; fn is_cac_table(name: &str) -> bool { asset_game::is_stats_table_name(name) + || name.eq_ignore_ascii_case("mp/perkTable.csv") + || name.eq_ignore_ascii_case("mp/rankTable.csv") + || name.eq_ignore_ascii_case("mp/rankIconTable.csv") + || name.eq_ignore_ascii_case("mp/unlockTable.csv") + || name.eq_ignore_ascii_case("mp/unlockTableMP.csv") || name.eq_ignore_ascii_case("mp/attachmentTable.csv") || name.eq_ignore_ascii_case("mp/attachmentCombos.csv") || name.eq_ignore_ascii_case("mp/weaponoptions.csv") diff --git a/crates/assets/src/lane/t5.rs b/crates/assets/src/lane/t5.rs index cec58d6..c66613b 100644 --- a/crates/assets/src/lane/t5.rs +++ b/crates/assets/src/lane/t5.rs @@ -452,6 +452,7 @@ impl ZoneLane for T5Lane { fx_models: Default::default(), fx_glass: None, + map_content: Default::default(), impact_fx: sink.impact_fx.take_table(), reflection_probe_images, intermission_view, diff --git a/crates/assets/src/lane/t6.rs b/crates/assets/src/lane/t6.rs index ec21307..52e68e7 100644 --- a/crates/assets/src/lane/t6.rs +++ b/crates/assets/src/lane/t6.rs @@ -1160,7 +1160,37 @@ fn capture_content( content.melee = melee_weapon(load); let (mut failed, mut decoded, mut missing) = (0usize, 0usize, 0usize); let mut native_textures = DecodedTextures::new(); - for (name, (view, hands)) in wanted { + let wanted: Vec<_> = wanted.into_iter().collect(); + let jobs: Vec<_> = wanted + .iter() + .map(|(name, _)| { + let source = copies + .get(name) + .map_or(name.as_str(), |(model, _)| model.as_str()); + let &(load, asset) = models.get(source)?; + asset_model::T6Model::new(load, asset) + }) + .collect(); + let pool = crate::session_load::load_pool(); + let chunk_size = jobs.len().div_ceil(pool.thread_num().max(1) * 16).max(1); + let mut captured = pool.scope(|scope| { + for (index, chunk) in jobs.chunks(chunk_size).enumerate() { + scope.spawn(async move { + let skels: Vec<_> = chunk + .iter() + .map(|model| { + model.and_then(|model| asset_model::capture_model_skel_t6(model, |_| None)) + }) + .collect(); + (index, skels) + }); + } + }); + // Complete pure captures in parallel, then retain the input mutation + // and refusal order regardless of which task finished first. + captured.sort_unstable_by_key(|(index, _)| *index); + let captured = captured.into_iter().flat_map(|(_, skels)| skels); + for ((name, (view, hands)), skel) in wanted.into_iter().zip(captured) { let copy = copies.get(&name); let source = copy.map_or(name.as_str(), |(model, _)| model.as_str()); let Some(&(load, asset)) = models.get(source) else { @@ -1172,7 +1202,7 @@ fn capture_content( let Some(model) = asset_model::T6Model::new(load, asset) else { continue; }; - let Some(mut skel) = asset_model::capture_model_skel_t6(model, |_| None) else { + let Some(mut skel) = skel else { failed += 1; content .report diff --git a/crates/assets/src/lane/t6/common_compile/models.rs b/crates/assets/src/lane/t6/common_compile/models.rs index f0d641b..c42c740 100644 --- a/crates/assets/src/lane/t6/common_compile/models.rs +++ b/crates/assets/src/lane/t6/common_compile/models.rs @@ -10,6 +10,7 @@ pub(super) fn bind_native_materials( refusals: &mut Vec, ) -> std::collections::BTreeMap { let mut bound = std::collections::BTreeMap::new(); + let mut linked_techsets = std::collections::BTreeSet::new(); for (name, capture) in captures { let result = (|| { let native = capture @@ -33,7 +34,9 @@ pub(super) fn bind_native_materials( asset_material::t6_techset::T6Draw::Lit }; let mut report = Vec::new(); - materials.link_t6_technique_set(set, draw, &mut report); + if linked_techsets.insert(draw.technique_set_name(&set.name)) { + materials.link_t6_technique_set(set, draw, &mut report); + } let definition = super::super::native_material_definition( path, name, diff --git a/crates/assets/src/lib.rs b/crates/assets/src/lib.rs index 1747f9a..9cf23d1 100644 --- a/crates/assets/src/lib.rs +++ b/crates/assets/src/lib.rs @@ -11,7 +11,6 @@ mod match_load; pub mod plugin; pub mod prepared; pub mod session_load; -mod teardown; pub use artifact_cache::{cache_flight, cache_get, cache_put, fnv1a64, fnv1a64_more}; pub(crate) use asset_graph::stamp_match_destructible_death; diff --git a/crates/assets/src/match_load.rs b/crates/assets/src/match_load.rs index bd1f4fb..5bb2303 100644 --- a/crates/assets/src/match_load.rs +++ b/crates/assets/src/match_load.rs @@ -200,15 +200,27 @@ fn start_match_load( } }); inflight.0 = true; - commands.insert_resource(MatchLoadAccepted { - request_id, - load_key, - zone: zone_accepted, + commands.queue(move |world: &mut World| { + frame::scope::insert( + world, + MatchLoadAccepted { + request_id, + load_key, + zone: zone_accepted, + }, + frame::MatchScope::Loading, + ); }); - commands.insert_resource(MatchLoadTask { - task, - request_id, - progress: cancel_handle, + commands.queue(move |world: &mut World| { + frame::scope::insert( + world, + MatchLoadTask { + task, + request_id, + progress: cancel_handle, + }, + frame::MatchScope::Loading, + ); }); commands.remove_resource::(); } @@ -337,7 +349,7 @@ fn poll_match_load( diag::error!(World, "prepared match has no family"); return; }; - commands.insert_resource(PreparedMatchSound { + let map_sound = PreparedMatchSound { namespace, load_key: ready.load_key, zone: ready.zone.clone(), @@ -348,8 +360,13 @@ fn poll_match_load( .sound .take() .unwrap_or_else(|| Err("the map zone never opened".to_owned())), + }; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, map_sound, frame::MatchScope::Loading); + }); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, ready, frame::MatchScope::Loading); }); - commands.insert_resource(ready); } fn expire_match_load_abort( @@ -369,12 +386,19 @@ fn expire_match_load_abort( } pub fn register_match_load_systems(app: &mut App) { + if !app.is_plugin_added::() { + app.add_plugins(frame::ScopePlugin); + } + app.add_systems( + OnExit(frame::MatchScope::Loading), + cancel_loading.in_set(frame::ScopeSet::Release), + ); app.init_resource::().add_systems( Update, ( approve_map_load.in_set(MapLoadApproval), ( - start_match_load, + start_match_load.run_if(in_state(frame::MatchScope::Loading)), poll_match_load.after(start_match_load), expire_match_load_abort.after(poll_match_load), ) @@ -385,3 +409,10 @@ pub fn register_match_load_systems(app: &mut App) { .in_set(ClientSet::Load), ); } + +fn cancel_loading(task: Option>, mut busy: ResMut) { + if let Some(task) = task { + task.progress.cancel(); + } + busy.0 = false; +} diff --git a/crates/assets/src/plugin.rs b/crates/assets/src/plugin.rs index 9bd8b1e..ba57e64 100644 --- a/crates/assets/src/plugin.rs +++ b/crates/assets/src/plugin.rs @@ -1,13 +1,11 @@ use bevy::prelude::*; use crate::match_load::register_match_load_systems; -use crate::teardown::register_match_teardown; pub struct AssetPlugin; impl Plugin for AssetPlugin { fn build(&self, app: &mut App) { register_match_load_systems(app); - register_match_teardown(app); } } diff --git a/crates/assets/src/prepared.rs b/crates/assets/src/prepared.rs index 723578d..47e265c 100644 --- a/crates/assets/src/prepared.rs +++ b/crates/assets/src/prepared.rs @@ -50,6 +50,7 @@ pub struct MapFacts { #[derive(Clone, Debug, Default, PartialEq)] pub struct PreparedMap { + pub content: std::sync::Arc, pub zone: String, pub namespace: Option, @@ -66,7 +67,7 @@ pub struct PreparedKillstreaks(pub Vec); pub struct PreparedWeapons( std::sync::Arc, Option, - frame::WorldGeneration, + frame::WorldStamp, ); #[derive(Clone, Copy)] @@ -91,7 +92,7 @@ impl PreparedWeapons { Self( registry, Some(key.match_key.match_epoch), - frame::WorldGeneration::from_install(key.local_load_request_id), + frame::WorldStamp::from_install(key.local_load_request_id), ) } @@ -107,7 +108,7 @@ impl PreparedWeapons { pub fn for_event( &self, - generation: frame::WorldGeneration, + generation: frame::WorldStamp, ) -> Result, asset_game::WeaponBindingRefusal> { if generation.0.is_none() || generation != self.2 { return Err(asset_game::WeaponBindingRefusal::StaleSnapshot); diff --git a/crates/assets/src/session_load/common_cache.rs b/crates/assets/src/session_load/common_cache.rs index 188f2ee..fea767f 100644 --- a/crates/assets/src/session_load/common_cache.rs +++ b/crates/assets/src/session_load/common_cache.rs @@ -118,7 +118,9 @@ pub struct CommonSet { pub(super) id: u64, pub(super) key: CommonKey, pub(super) products: CommonProducts, + editor_weapons: asset_game::EditorWeaponCatalog, donor_images: async_lock::OnceCell>, + donor_plans: std::sync::Mutex>>, pub(super) fpv_plan: Option, pub(super) retained: std::sync::Mutex, pub(super) cac_tables: Vec<(asset_core::AssetNamespace, asset_game::CapturedStringTable)>, @@ -127,6 +129,49 @@ pub struct CommonSet { } impl CommonSet { + pub(super) fn start_donor_images(self: &Arc) { + let Some(plans) = self + .donor_plans + .lock() + .unwrap_or_else(|poison| poison.into_inner()) + .take() + else { + return; + }; + // A canceled match must not poison the shared image publication. + let progress = LoadProgress::default(); + let pending: Vec<_> = plans + .into_iter() + .map(|(namespace, plan)| (namespace, plan.enqueue(&progress))) + .collect(); + let keeping = Arc::clone(self); + load_pool() + .spawn(async move { + let mut kept = Vec::new(); + for (namespace, pending) in pending { + let job = pending.job; + let (label, batch) = pending.join().await; + let batch = batch.into_kept(); + keeping.retain(&batch); + kept.push(KeptImages { + label, + namespace, + batch, + job, + }); + } + diag::info!( + World, + "common set: {} donor image batches kept, {} payloads ({:.1}MiB)", + kept.len(), + keeping.retained_payloads(), + keeping.retained_bytes() as f64 / (1024.0 * 1024.0), + ); + let _ = keeping.donor_images.set(kept).await; + }) + .detach(); + } + pub(super) async fn donor_images(&self) -> &[KeptImages] { self.donor_images.wait().await } @@ -231,6 +276,9 @@ pub(super) async fn ensure_common(key: CommonKey) -> (Arc, &'static s } pub struct ShellCommon { + pub player_defaults_config: Option, + pub challenges: Option, + pub player_schemas: std::collections::BTreeMap>, pub ui_images: asset_material::UiImagePublication, pub weapons: asset_game::EditorWeaponCatalog, pub strings: LocalizeCatalog, @@ -242,7 +290,7 @@ pub async fn load_shell_common(games: asset_transport::GamesRoot) -> ShellCommon let mut report = Vec::new(); let key = CommonKey::shell(&games, &mut report); let (common, reach) = ensure_common(key).await; - let weapons = common.products.weapons.clone().publish_for_editor(); + let weapons = common.editor_weapons.clone(); report.push(format!( "CAC: {reach} common set {}; weapons={} (iw4={} iw5={} t5={} t6={}) tables={}", @@ -285,6 +333,18 @@ pub async fn load_shell_common(games: asset_transport::GamesRoot) -> ShellCommon )); } ShellCommon { + player_defaults_config: common + .products + .scripts + .config("mp/stats_init.cfg") + .map(str::to_owned), + challenges: common + .products + .scripts + .tables() + .get("mp/allchallengestable.csv") + .cloned(), + player_schemas: common.products.scripts.schemas().clone(), ui_images: common.ui_images.clone(), weapons, strings: common.products.strings.clone(), @@ -741,8 +801,7 @@ async fn prepare_common(key: CommonKey) -> Arc { load_pool(), ); stage.done(); - camouflage_images = hold_image_plan("T5 camouflage", Some(plan), job) - .map(|held| held.enqueue(&progress)); + camouflage_images = hold_image_plan("T5 camouflage", Some(plan), job); } } @@ -779,7 +838,7 @@ async fn prepare_common(key: CommonKey) -> Arc { let prepared_ms = started.elapsed().as_secs_f32() * 1000.0; diag::info!( World, - "common set: {key} prepared in {prepared_ms:.0}ms; donor images still decoding" + "common set: {key} prepared in {prepared_ms:.0}ms; donor images deferred until a match" ); material_seed.mark_images_common_owned(); @@ -803,8 +862,9 @@ async fn prepare_common(key: CommonKey) -> Arc { ui_images.preview_fallback(family.key.namespace, &family.image, image); } } - let set = Arc::new(CommonSet { + Arc::new(CommonSet { ui_images: ui_images.publish(), + editor_weapons: preview_weapons, id: NEXT_COMMON_PROFILE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed), key, products: CommonProducts { @@ -853,48 +913,23 @@ async fn prepare_common(key: CommonKey) -> Arc { localize_report, }, donor_images: async_lock::OnceCell::new(), + donor_plans: std::sync::Mutex::new(Some( + [ + ("t5", t5_images), + ("t5", camouflage_images), + ("iw5", foreign_images), + ("iw5", bundle_images), + ] + .into_iter() + .filter_map(|(namespace, plan)| Some((namespace, plan?))) + .collect(), + )), fpv_plan, retained: std::sync::Mutex::new(Default::default()), cac_tables, prepared_ms, ready_at: std::time::Instant::now(), - }); - - let keeping = Arc::clone(&set); - load_pool() - .spawn(async move { - let mut kept = Vec::new(); - for (namespace, pending) in [ - ("t5", t5_images), - ("t5", camouflage_images), - ("iw5", foreign_images), - ("iw5", bundle_images), - ] { - let Some(pending) = pending else { - continue; - }; - let job = pending.job; - let (label, batch) = pending.join().await; - let batch = batch.into_kept(); - keeping.retain(&batch); - kept.push(KeptImages { - label, - namespace, - batch, - job, - }); - } - diag::info!( - World, - "common set: {} donor image batches kept, {} payloads ({:.1}MiB)", - kept.len(), - keeping.retained_payloads(), - keeping.retained_bytes() as f64 / (1024.0 * 1024.0), - ); - let _ = keeping.donor_images.set(kept).await; - }) - .detach(); - set + }) } fn walk_iw5_startup(donor: &Path, progress: &LoadProgress) -> crate::lane::MaterialPopulation { diff --git a/crates/assets/src/session_load/common_walks.rs b/crates/assets/src/session_load/common_walks.rs index 50cda01..bacdd88 100644 --- a/crates/assets/src/session_load/common_walks.rs +++ b/crates/assets/src/session_load/common_walks.rs @@ -279,7 +279,7 @@ pub(super) fn walk_foreign_material_common( donor: &Path, progress: &LoadProgress, job: load_jobs::Job, -) -> (MaterialCatalog, Option, Vec) { +) -> (MaterialCatalog, Option, Vec) { let mut report = Vec::new(); let Some(mut census) = capture_common_zone( donor, @@ -290,8 +290,7 @@ pub(super) fn walk_foreign_material_common( ) else { return (MaterialCatalog::default(), None, report); }; - let pending_images = hold_image_plan("IW5 common_mp", census.pending_images.take(), job) - .map(|held| held.enqueue(progress)); + let held_images = hold_image_plan("IW5 common_mp", census.pending_images.take(), job); let materials = census.material_population; report.extend(census.report); report.push(format!( @@ -300,7 +299,7 @@ pub(super) fn walk_foreign_material_common( materials.materials.len(), materials.technique_set_facts().len(), )); - (materials, pending_images, report) + (materials, held_images, report) } fn capture_common_zone( @@ -532,11 +531,11 @@ pub(super) enum ForeignCommonWork { bevy::tasks::Task<( MaterialCatalog, Iw5WeaponBundle, - Option, + Option, Vec, )>, ), - Split(Option, Vec)>>), + Split(Option, Vec)>>), } #[derive(Default)] @@ -582,7 +581,7 @@ pub(super) fn walk_iw5_weapon_bundle( donor: &Path, progress: &LoadProgress, job: load_jobs::Job, -) -> (Iw5WeaponBundle, Option, Vec) { +) -> (Iw5WeaponBundle, Option, Vec) { let mut report = Vec::new(); let Some(mut census) = capture_common_zone( donor, @@ -593,11 +592,7 @@ pub(super) fn walk_iw5_weapon_bundle( ) else { return (Iw5WeaponBundle::default(), None, report); }; - // The producer enqueues the bundle's images itself. Left for the consumer, - // they wait out the synchronous `common_mp` walk and the unpacking of this - // tuple: seconds of ready decode work with nobody holding it. - let pending_images = hold_image_plan("IW5 weapon bundle", census.pending_images.take(), job) - .map(|held| held.enqueue(progress)); + let held_images = hold_image_plan("IW5 weapon bundle", census.pending_images.take(), job); report.extend(census.report); report.push(format!( "iw5 weapons: path={} ids={} gun_named={} fpv={} world_guns={} xanims={} materials={}", @@ -621,7 +616,7 @@ pub(super) fn walk_iw5_weapon_bundle( scene_models: census.scene_models, shared_surfaces: census.shared_surfaces, }, - pending_images, + held_images, report, ) } @@ -633,7 +628,7 @@ pub(super) fn walk_shared_iw5_common( ) -> ( MaterialCatalog, Iw5WeaponBundle, - Option, + Option, Vec, ) { let mut report = Vec::new(); @@ -654,8 +649,7 @@ pub(super) fn walk_shared_iw5_common( // One capture serves the material seed and the weapon bundle, so there is // one plan here, not two: the donor is walked once and its images are // claimed once. - let pending_images = hold_image_plan("IW5 common_mp", census.pending_images.take(), job) - .map(|held| held.enqueue(progress)); + let held_images = hold_image_plan("IW5 common_mp", census.pending_images.take(), job); report.extend(census.report); let materials = census.material_population; report.push(format!( @@ -683,7 +677,7 @@ pub(super) fn walk_shared_iw5_common( scene_models: census.scene_models, shared_surfaces: census.shared_surfaces, }, - pending_images, + held_images, report, ) } @@ -751,7 +745,7 @@ pub(super) struct T5WeaponCommon { pub(super) projectiles: asset_model::ProjectileMeshBuild, pub(super) teamsets: std::collections::HashMap, pub(super) scene_models: asset_world::MapXModelSceneCatalog, - pub(super) images: Option, + pub(super) images: Option, pub(super) stats_tables: ( Vec, Vec, @@ -815,8 +809,7 @@ pub(super) fn walk_t5_weapon_common( } }; let mut census = lane(image.game).load_common_mp(&donor, &image, progress, true, material_seed); - let images = hold_image_plan("T5 common_mp", census.pending_images.take(), job) - .map(|held| held.enqueue(progress)); + let images = hold_image_plan("T5 common_mp", census.pending_images.take(), job); report.extend(census.report); report.push(format!( "t5 weapon common: path={} weapons={} fpv={} world_guns={} materials={} xanims={} fx={}", @@ -882,6 +875,8 @@ fn t6_class_tables( asset_game::is_stats_table_name(&table.name) || table.name.eq_ignore_ascii_case("mp/attachmentTable.csv") || table.name.eq_ignore_ascii_case("mp/mapstable.csv") + || table.name.eq_ignore_ascii_case("mp/rankTable.csv") + || table.name.eq_ignore_ascii_case("mp/unlockTable.csv") }) .collect(); report.push(format!( diff --git a/crates/assets/src/session_load/match_walk.rs b/crates/assets/src/session_load/match_walk.rs index e5891ea..ba60f74 100644 --- a/crates/assets/src/session_load/match_walk.rs +++ b/crates/assets/src/session_load/match_walk.rs @@ -62,6 +62,8 @@ pub(super) async fn walk_prepared_match( return (MatchLoadOutcome::Canceled, None); } + common.start_donor_images(); + let cloning = std::time::Instant::now(); let CommonProducts { scripts: mut iw4_scripts, @@ -727,6 +729,7 @@ pub(super) async fn walk_prepared_match( )); let prepared_map = PreparedMap { + content: Arc::clone(&world.map_content), zone: zone_name, namespace: map_namespace, spawns: dm_spawns, diff --git a/crates/assets/src/session_load/mod.rs b/crates/assets/src/session_load/mod.rs index f53291a..c06c023 100644 --- a/crates/assets/src/session_load/mod.rs +++ b/crates/assets/src/session_load/mod.rs @@ -126,6 +126,8 @@ pub struct PreparedWorld { pub fx_glass: Option, + pub map_content: Arc, + pub impact_fx: Option, pub reflection_probe_images: Vec>, pub intermission_view: Option, @@ -211,6 +213,7 @@ impl PreparedWorld { fx: Default::default(), fx_models: Default::default(), fx_glass: Default::default(), + map_content: Default::default(), impact_fx: Default::default(), reflection_probe_images: Default::default(), intermission_view: Default::default(), diff --git a/crates/assets/src/teardown.rs b/crates/assets/src/teardown.rs deleted file mode 100644 index 4c77d51..0000000 --- a/crates/assets/src/teardown.rs +++ /dev/null @@ -1,58 +0,0 @@ -use bevy::prelude::*; -use frame::{ClientSet, MatchTornDown, ReturnedToMenu, SessionSwapApplied}; - -use crate::{ - MatchType10SoundHints, PreparedBodies, PreparedBodyClips, PreparedDestructibleDeath, - PreparedFpvMeshes, PreparedLocalizedStrings, PreparedProjectileMeshes, PreparedWeapons, - PreparedWorldWeapons, PreparedXAnims, PreparedXModelWalkCensus, SessionCompass, -}; -use asset_anim::PlayerAnimSources; -use asset_audio::SessionMapScriptSound; -use asset_game::SessionTeamSettings; -use asset_material::AssetRefDumpCensus; -use asset_world::MapXModelSceneCatalog; - -pub(crate) fn drop_match_catalogs_on_teardown( - mut torn: MessageReader, - mut returned: MessageReader, - mut commands: Commands, -) { - if torn.read().count() == 0 && returned.read().count() == 0 { - return; - } - commands.queue(retire_match_catalogs); -} - -fn retire_match_catalogs(world: &mut World) { - frame::retire::retire_resources(world, |batch| { - batch - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::(); - }); -} - -pub(crate) fn register_match_teardown(app: &mut App) { - app.add_message::().add_systems( - Update, - drop_match_catalogs_on_teardown - .after(SessionSwapApplied) - .in_set(ClientSet::Load), - ); -} diff --git a/crates/audio/src/ambient.rs b/crates/audio/src/ambient.rs index 773ecd5..85e1fff 100644 --- a/crates/audio/src/ambient.rs +++ b/crates/audio/src/ambient.rs @@ -8,11 +8,11 @@ use bevy::{ prelude::*, tasks::{AsyncComputeTaskPool, Task, TaskPool, futures_lite::future}, }; -use frame::{MatchTornDown, ReturnedToMenu}; -use crate::backend::{AudioScope, MatchEpoch}; +use crate::backend::AudioScope; use crate::playback::SoundBank; use crate::sources::{DesiredSource, SourceCueRequest, SourceKey, SourceRenderGroup}; +use frame::{MatchScope, ScopeEpoch}; pub(crate) const MAP_BED_SLOT: u32 = 2; const MAP_EMITTER_SLOT: u32 = 3; @@ -145,62 +145,14 @@ struct ResidentComposed { #[derive(Resource)] pub(crate) struct SoundBankNamespace { - pub(crate) generation: frame::WorldGeneration, + pub(crate) generation: frame::WorldStamp, pub(crate) zone: String, pub(crate) namespace: AssetNamespace, } -pub(crate) fn stop_map_ambient_on_match_end( - mut torn: MessageReader, - mut returned: MessageReader, - mut sources: ResMut, - mut booted: ResMut, - mut attempted: ResMut, - mut epoch: ResMut, - compose: Option>, - accepted: Option>, - mut commands: Commands, -) { - let retired: Vec<_> = torn.read().map(|fact| fact.world_generation).collect(); - let left_session = returned.read().count() > 0; - if retired.is_empty() && !left_session { - return; - } - epoch.bump(); - sources.desired.clear(); - booted.0 = false; - commands.remove_resource::(); - - let compose_is_for_the_incoming_map = !left_session - && compose - .as_ref() - .zip(accepted.as_ref()) - .is_some_and(|(compose, accepted)| { - compose.load_key == accepted.load_key - && !retired.contains(&frame::WorldGeneration::from_install( - compose.load_key.local_load_request_id, - )) - }); - if !compose_is_for_the_incoming_map { - commands.remove_resource::(); - commands.remove_resource::(); - attempted.0 = false; - } - commands.queue(|world: &mut bevy::prelude::World| { - frame::retire::retire_resources(world, |batch| { - batch - .resource::() - .resource::() - .resource::() - .resource::(); - }); - }); - perf::ambient_hold(i64::from(booted.0)); - diag::info!(Audio, "audio: map ambient stopped"); -} - pub(crate) fn start_sound_bank_compose( mut attempted: ResMut, + mut status: ResMut, accepted: Option>, abort: Option>, identity: Option>, @@ -212,6 +164,7 @@ pub(crate) fn start_sound_bank_compose( return; } if silent.is_some() { + *status = LoadingAudioStatus::Ready; attempted.0 = true; return; } @@ -248,26 +201,33 @@ pub(crate) fn start_sound_bank_compose( })) } }; - commands.insert_resource(SoundBankCompose { - load_key: accepted.load_key, - zone, - iwd, - bank: None, - common_profile_id: 0, - products_id: 0, - started: std::time::Instant::now(), - wait: None, + let load_key = accepted.load_key; + commands.queue(move |world: &mut World| { + frame::scope::insert( + world, + SoundBankCompose { + load_key, + zone, + iwd, + bank: None, + common_profile_id: 0, + products_id: 0, + started: std::time::Instant::now(), + wait: None, + }, + frame::MatchScope::Loading, + ); }); } pub(crate) fn install_sound_bank( + mut status: ResMut, mut compose: Option>, identity: Option>, accepted: Option>, abort: Option>, mut map_sound: Option>, process: Option>, - mut epoch: ResMut, resident_clips: Res, mut resident: ResMut, mut commands: Commands, @@ -384,7 +344,7 @@ pub(crate) fn install_sound_bank( } = composed; match loaded { Ok((bank, gaps)) => { - epoch.bump(); + *status = LoadingAudioStatus::Ready; if iwd.is_empty() { diag::warn!( Audio, @@ -392,7 +352,8 @@ pub(crate) fn install_sound_bank( compose.zone ); } - commands.insert_resource(SoundIwd(Arc::clone(&iwd))); + let sound_iwd = SoundIwd(Arc::clone(&iwd)); + resident.0 = Some(ResidentBank { zone: compose.zone.clone(), iwd: Arc::clone(&iwd), @@ -411,14 +372,7 @@ pub(crate) fn install_sound_bank( compose.common_profile_id, Some(resident_clips.clone()), ); - commands.queue(move |world: &mut World| { - frame::retire::retire_resources(world, |batch| { - batch.resource::(); - }); - world.insert_resource(new_clips); - }); - commands.insert_resource(crate::clip_store::CueFeedback::default()); - commands.insert_resource(SoundBank(bank)); + diag::info!( Audio, "audio: sound bank {} for {} ({} zone gaps) in {:.0}ms", @@ -427,15 +381,26 @@ pub(crate) fn install_sound_bank( gaps, compose.started.elapsed().as_secs_f32() * 1000.0 ); - commands.insert_resource(SoundBankNamespace { - generation: frame::WorldGeneration::from_install( - compose.load_key.local_load_request_id, - ), + let sound_namespace = SoundBankNamespace { + generation: frame::WorldStamp::from_install(compose.load_key.local_load_request_id), zone: compose.zone.clone(), namespace, + }; + commands.queue(move |world: &mut World| { + frame::scope::insert( + world, + PreparedMatchAudio { + iwd: sound_iwd, + clips: new_clips, + bank: SoundBank(bank), + namespace: sound_namespace, + }, + frame::MatchScope::Loading, + ); }); } Err(e) => { + *status = LoadingAudioStatus::Failed(e.to_string()); diag::warn!( Audio, "audio: sound bank load failed for {}: {e}", @@ -453,7 +418,7 @@ pub(crate) fn boot_map_ambient_once( identity: Option>, script_sound: Option>, namespace: Option>, - epoch: Res, + epoch: Res>, ready: Res, generation: Res, runtime: Res, @@ -467,7 +432,7 @@ pub(crate) fn boot_map_ambient_once( if loading.is_some_and(|screen| !screen.is_complete()) { return; } - if !ready.0.ready_for(*generation) { + if !ready.0.ready_for(generation.stamp()) { return; } let Some(bank) = bank else { @@ -553,3 +518,28 @@ pub(crate) fn boot_map_ambient_once( identity.zone ); } + +#[derive(Resource, Default)] +pub enum LoadingAudioStatus { + #[default] + Pending, + Ready, + Failed(String), +} + +#[derive(Resource)] +struct PreparedMatchAudio { + iwd: SoundIwd, + clips: crate::ClipStore, + bank: SoundBank, + namespace: SoundBankNamespace, +} + +pub fn stage_prepared_match_audio(world: &mut World) { + if let Some(prepared) = world.remove_resource::() { + frame::scope::stage(world, prepared.iwd); + frame::scope::stage(world, prepared.clips); + frame::scope::stage(world, prepared.bank); + frame::scope::stage(world, prepared.namespace); + } +} diff --git a/crates/audio/src/backend.rs b/crates/audio/src/backend.rs index 6bafd4e..994ddeb 100644 --- a/crates/audio/src/backend.rs +++ b/crates/audio/src/backend.rs @@ -4,23 +4,26 @@ use frame::ClientSet; pub(crate) use crate::render_core::AudioScope; use crate::runtime::AudioRuntime; -#[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct MatchEpoch(pub u64); - -impl MatchEpoch { - pub fn bump(&mut self) { - self.0 = self.0.wrapping_add(1); - } -} +use frame::{MatchScope, ScopeEpoch}; pub(crate) fn register(app: &mut App) { + app.add_systems( + StateTransition, + sync_scope_epoch + .in_set(bevy::state::state::StateTransitionSystems::TransitionSchedules) + .after(frame::scope::ScopeRetired), + ); + app.add_systems( + OnExit(MatchScope::Live), + release_audio.in_set(frame::ScopeSet::Release), + ); app.init_resource::(); let verdicts = app.world().resource::().clone(); app.init_resource::(); app.world() .resource::() .set_fire_verdicts(verdicts); - app.init_resource::() + app.init_resource::>() .add_systems( Update, publish_audio_context @@ -37,10 +40,11 @@ pub(crate) fn register(app: &mut App) { } pub(crate) fn publish_audio_context( - epoch: Res, + epoch: Res>, runtime: Res, verdicts: Option>, clips: Option>, + frontend: Option>, mut mix: Option>, mut channels: Option>, listeners: Query<&Transform, With>, @@ -98,7 +102,12 @@ pub(crate) fn publish_audio_context( .map(|latest| latest.tick.0), }), ); - runtime.set_media_service(clips.as_ref().map(|clips| clips.service())); + runtime.set_media_service( + clips + .as_ref() + .map(|clips| clips.service()) + .or_else(|| frontend.as_ref().map(|frontend| frontend.clips.service())), + ); if let Some(mix) = mix.as_mut() { mix.reset_epoch(epoch.0); } @@ -159,7 +168,7 @@ fn cancel_audio_on_exit(mut exit: MessageReader, runtime: Res, - epoch: Res, + epoch: Res>, settings: Option>, listeners: Query<&Transform, With>, destructibles: Option>, @@ -197,3 +206,14 @@ fn submit_presented_audio( } runtime.set_sources(desired); } + +fn sync_scope_epoch(epoch: Res>, runtime: Res) { + runtime.set_match_epoch(epoch.0); +} + +fn release_audio(mut runtime: ResMut) { + runtime.set_media_service(None); + runtime.set_event_context(None); + runtime.set_cue_mix(None); + runtime.set_sources(Vec::new()); +} diff --git a/crates/audio/src/breath.rs b/crates/audio/src/breath.rs index 16e9171..47e8679 100644 --- a/crates/audio/src/breath.rs +++ b/crates/audio/src/breath.rs @@ -29,12 +29,11 @@ pub(crate) fn update( view: Option>, screen: Option>, bank: Option>, - mut torn: MessageReader, mut died: MessageReader, mut state: Local, mut sources: ResMut, runtime: Res, - epoch: Res, + epoch: Res>, mut play: MessageWriter, ) { let client = local.as_ref().map(|local| local.0); @@ -61,15 +60,13 @@ pub(crate) fn update( sources.context = context; *state = BreathAudio::default(); } - let torn_down = torn.read().count() != 0; let died = died.read().any(|event| { client.is_some_and(|client| event.client == client.0) && life == Some(event.life) }); let ps = client .and_then(|client| presented.as_ref()?.alive_player(client)) .filter(|_| !view.as_ref().is_some_and(|view| view.in_killcam())); - if torn_down - || died + if died || !screen.is_some_and(|screen| matches!(*screen, frame::AppScreen::InGame)) || ps.is_none() { diff --git a/crates/audio/src/clip_store.rs b/crates/audio/src/clip_store.rs index 1d69265..ba2582b 100644 --- a/crates/audio/src/clip_store.rs +++ b/crates/audio/src/clip_store.rs @@ -662,9 +662,6 @@ impl CueFeedback { } self.cues.push(CueFeedbackEntry { handle }); } - pub fn clear(&mut self) { - self.cues.clear(); - } } pub(crate) fn clip_keys_for_alias( diff --git a/crates/audio/src/cue.rs b/crates/audio/src/cue.rs index 7a8ddd2..54b1aa1 100644 --- a/crates/audio/src/cue.rs +++ b/crates/audio/src/cue.rs @@ -114,12 +114,17 @@ fn alias_admission( pub(crate) struct CueState { pub(crate) release: Arc, - result: OnceLock>, + result: OnceLock>, completion: OnceLock, pub(crate) playback: OnceLock>, children: Mutex)>>, } +struct ResolvedCueInfo { + bank_revision: u64, + looping: asset_audio::LoopingPolicy, +} + pub(crate) struct CueHandle(pub Arc); impl CueHandle { @@ -138,31 +143,24 @@ impl CueHandle { pub(crate) fn completion(&self) -> Option { let mut decision = self.0.completion()?; - if let Some(Ok(cue)) = self.result() { + if let Some(Ok(cue)) = self.0.result.get() { decision.detail = Some(self.0.playback.get().map_or_else( || { format!( "bank_revision={} looping_policy={:?}", - cue.bank.revision(), - cue.policy.looping + cue.bank_revision, cue.looping ) }, |instance| { format!( "instance={} bank_revision={} looping_policy={:?}", - instance.id, - cue.bank.revision(), - cue.policy.looping + instance.id, cue.bank_revision, cue.looping ) }, )); } Some(decision) } - - pub fn result(&self) -> Option> { - self.0.result.get().cloned() - } } impl CueState { @@ -226,7 +224,10 @@ impl CueState { let _ = self.completion.set(decision); } pub(crate) fn resolved(&self, result: Result) { - let _ = self.result.set(result); + let _ = self.result.set(result.map(|cue| ResolvedCueInfo { + bank_revision: cue.bank.revision(), + looping: cue.policy.looping, + })); } } @@ -241,6 +242,7 @@ pub(crate) struct CueRequest { pub scope: AudioScope, pub epoch: u64, pub pitch_scale: f32, + pub volume_scale: f32, } impl CueRequest { @@ -307,7 +309,14 @@ impl CueResolver { .map_err(|_| CueFailure::MissingPolicy)?; let policy = bound.policy(); let clip = clip_key_for_sound(bound).map_err(|_| CueFailure::InvalidMediaBinding)?; - let volume = policy.volume(unit_random(&mut self.lcg)); + let scale = if request.volume_scale.is_finite() { + request.volume_scale.max(0.0) + } else { + 1.0 + }; + let volume = policy + .volume(unit_random(&mut self.lcg)) + .map(|volume| volume * scale); let pitch = policy.pitch(unit_random(&mut self.lcg)); let scale = if request.pitch_scale.is_finite() && request.pitch_scale > 0.0 { request.pitch_scale diff --git a/crates/audio/src/cue_execution.rs b/crates/audio/src/cue_execution.rs index 7f02ebc..3e421f4 100644 --- a/crates/audio/src/cue_execution.rs +++ b/crates/audio/src/cue_execution.rs @@ -113,6 +113,7 @@ pub(crate) struct CueTrigger { pub class: SoundClass, pub epoch: u64, pub pitch_scale: f32, + pub volume_scale: f32, pub fallbacks: Vec, } @@ -444,6 +445,7 @@ impl CueWork { scope: self.request.scope, epoch: self.request.epoch, pitch_scale: 1.0, + volume_scale: self.request.volume_scale, execution: CueIntent { event: intent.event, origin_inches: intent.origin_inches, diff --git a/crates/audio/src/destructible_loops.rs b/crates/audio/src/destructible_loops.rs index 29af2c6..b79b61d 100644 --- a/crates/audio/src/destructible_loops.rs +++ b/crates/audio/src/destructible_loops.rs @@ -5,10 +5,11 @@ use bevy::prelude::*; use net::PresentedSnapshot; use crate::ambient::SoundBankNamespace; -use crate::backend::{AudioScope, MatchEpoch}; +use crate::backend::AudioScope; use crate::playback::SoundBank; use crate::runtime::AudioRuntime; use crate::sources::{DesiredSource, SourceKey}; +use frame::{MatchScope, ScopeEpoch}; struct PresentedLoop { alias: String, @@ -29,7 +30,7 @@ pub(crate) fn update( presented: Res, bank: Option>, namespace: Option>, - epoch: Res, + epoch: Res>, runtime: Res, mut sources: ResMut, ) { diff --git a/crates/audio/src/emit.rs b/crates/audio/src/emit.rs index e6f28f6..ba991cc 100644 --- a/crates/audio/src/emit.rs +++ b/crates/audio/src/emit.rs @@ -42,6 +42,7 @@ pub fn emit_footstep_on_bob_wrap( } let (alias, fallback) = footstep_aliases(gait, surface_flags, local_player, quieter); footsteps.write(Footstep { + volume_scale: 1.0, event: None, alias, fallback, diff --git a/crates/audio/src/entity_events.rs b/crates/audio/src/entity_events.rs index 7700804..efc5c59 100644 --- a/crates/audio/src/entity_events.rs +++ b/crates/audio/src/entity_events.rs @@ -117,7 +117,7 @@ fn entity_event_sound( play_cs_sound_alias( &sound.event, Some(crate::AudioEvent::from_entity( - *generation, + generation.stamp(), sound.entity, &sound.event, 0, @@ -226,7 +226,7 @@ fn entity_event_sound( }; output.write(WeaponSound { event: Some(crate::AudioEvent::from_entity( - *generation, + generation.stamp(), sound.entity, &sound.event, 0, @@ -308,13 +308,35 @@ fn movement_sound( .client() .and_then(|id| presented.player(id)) .is_some_and(|ps| (ps.perks[0] & playerstate_iw4::PERK_QUIETER) != 0); + let selective_hearing = presented + .player(local.0) + .is_some_and(|ps| ps.perks[0] & playerstate_iw4::PERK_SELECTIVEHEARING != 0); + let allied = presented.snapshot().is_some_and(|snapshot| { + let listener = snapshot + .meta + .for_client(local.0) + .map(|meta| meta.client_state_team); + let emitter = identity + .client() + .and_then(|id| snapshot.meta.for_client(id)) + .map(|meta| meta.client_state_team); + listener.is_some_and(|team| team != 0 && emitter == Some(team)) + }); + let volume_scale = if !selective_hearing { + 1.0 + } else if player_view || allied { + 0.25 + } else { + 4.0 + }; let origin_inches = (!player_view).then_some(sound.event.payload.origin); if let Some(index) = sound.event.event.landing_surface_index() { let surface_flags = (index as u32) << 20; let (alias, fallback) = land_aliases(surface_flags, player_view, quieter); land.write(LandSound { + volume_scale, event: Some(crate::AudioEvent::from_entity( - *generation, + generation.stamp(), sound.entity, &sound.event, 0, @@ -337,7 +359,7 @@ fn movement_sound( let fallback = player_view.then(|| gear_alias(false).to_owned()); play.write(crate::AliasCommand::Play(PlayAlias { event: Some(crate::AudioEvent::from_entity( - *generation, + generation.stamp(), sound.entity, &sound.event, 0, @@ -363,8 +385,9 @@ fn movement_sound( let surface_flags = u32::from(sound.event.payload.surf_type) << 20; let (alias, fallback) = footstep_aliases(gait, surface_flags, player_view, quieter); footsteps.write(Footstep { + volume_scale, event: Some(crate::AudioEvent::from_entity( - *generation, + generation.stamp(), sound.entity, &sound.event, 0, @@ -376,7 +399,7 @@ fn movement_sound( }); gear.write(WeaponSound { event: Some(crate::AudioEvent::from_entity( - *generation, + generation.stamp(), sound.entity, &sound.event, 1, @@ -585,7 +608,7 @@ pub(crate) fn play_viewmodel_notetrack_messages( .filter(|(bank, weapons)| table.owns(&bank.0, &weapons.registry())) .map(|(bank, _)| Arc::clone(&bank.0)); for batch in notes.read() { - if batch.generation != *generation + if batch.generation != generation.stamp() || batch.client != local.0 || !presented .snapshot() @@ -729,7 +752,7 @@ fn grenade_contact( }; output.write(WeaponSound { event: Some(crate::AudioEvent::from_entity( - *generation, + generation.stamp(), contact.entity, &contact.event, 0, diff --git a/crates/audio/src/event.rs b/crates/audio/src/event.rs index 082a38d..7b10ecf 100644 --- a/crates/audio/src/event.rs +++ b/crates/audio/src/event.rs @@ -47,7 +47,7 @@ pub struct AudioEvent { impl AudioEvent { pub fn from_entity( - generation: frame::WorldGeneration, + generation: frame::WorldStamp, entity: bevy::prelude::Entity, event: &net::DispatchedEntityEvent, ordinal: u16, @@ -75,7 +75,7 @@ impl AudioEvent { } pub fn from_animation( - generation: frame::WorldGeneration, + generation: frame::WorldStamp, timeline: u64, client: sim::ClientId, tick: sim::Tick, @@ -217,7 +217,7 @@ impl EventJournal { .. } ) && event.fire_cause.is_some_and(|(cause, _)| { - verdicts.status(frame::WorldGeneration(event.id.world), cause) + verdicts.status(frame::WorldStamp(event.id.world), cause) == net::PredictedFireStatus::Refused }) }) diff --git a/crates/audio/src/frontend.rs b/crates/audio/src/frontend.rs index cd7e71f..01f972b 100644 --- a/crates/audio/src/frontend.rs +++ b/crates/audio/src/frontend.rs @@ -8,22 +8,20 @@ use bevy::{ tasks::{AsyncComputeTaskPool, Task, TaskPool, futures_lite::future}, }; use frame::{ - ClientSet, LaunchIdentity, ReturnedToMenu, UiPlayMusic, UiPlaySound, UiStopMusic, - register_ui_contracts, + ClientSet, LaunchIdentity, UiPlayMusic, UiPlaySound, UiStopMusic, register_ui_contracts, }; use crate::{ ClipStore, SoundClass, - ambient::SoundIwd, clip_store::CueFeedback, playback::{MissingAliasGaps, SoundBank, play_alias_oneshot}, start::StartDecisions, }; -#[derive(Resource, Clone)] +#[derive(Resource)] pub struct FrontendAudio { pub bank: Arc, - pub iwd: Arc, + pub clips: ClipStore, } const FRONTEND_SOUND_ZONE: &str = "iw4:code_post_gfx_mp"; @@ -47,15 +45,12 @@ pub(crate) struct MenuSources { pub(crate) fn register_frontend_audio(app: &mut App) { register_ui_contracts(app); app.init_resource::() - .add_message::() + .init_resource::() .add_systems( Update, ( start_frontend_audio_prepare, install_frontend_audio_prepare.after(start_frontend_audio_prepare), - restore_frontend_audio_on_menu - .after(install_frontend_audio_prepare) - .after(crate::ambient::stop_map_ambient_on_match_end), ) .in_set(ClientSet::Load), ) @@ -143,53 +138,29 @@ fn install_frontend_audio_prepare( ); return; }; - commands.insert_resource(FrontendAudio { - bank, - iwd: walked.iwd, - }); + let clips = ClipStore::start(Arc::clone(&bank), Some(walked.iwd)); + commands.insert_resource(FrontendAudio { bank, clips }); diag::info!(Audio, "audio: frontend sound resident"); } -pub(crate) fn restore_frontend_audio_on_menu( - mut returned: MessageReader, - frontend: Option>, - live: Option>, - silent: Option>, - mut commands: Commands, -) { - if returned.read().count() == 0 || silent.is_some() { - return; - } - let Some(frontend) = frontend else { - diag::warn!( - Audio, - "audio: no resident frontend bank — the menu runs without sound (typed gap)" - ); - return; - }; - if live.is_some_and(|live| Arc::ptr_eq(&live.0, &frontend.bank)) { - return; - } - commands.insert_resource(CueFeedback::default()); - commands.insert_resource(SoundBank(Arc::clone(&frontend.bank))); - commands.insert_resource(SoundIwd(Arc::clone(&frontend.iwd))); - commands.insert_resource(ClipStore::start( - Arc::clone(&frontend.bank), - Some(Arc::clone(&frontend.iwd)), - )); - diag::info!(Audio, "audio: frontend sound restored"); -} +#[derive(Resource, Default)] +struct FrontendCueFeedback(CueFeedback); fn play_ui_sound_messages( mut events: MessageReader, runtime: Res, mut gaps: ResMut, - mut pending: ResMut, + mut pending: ResMut, mut decisions: ResMut, bank: Option>, - epoch: Res, + frontend: Option>, + epoch: Res>, namespace: Option>, ) { + let bank = bank + .as_ref() + .map(|bank| &bank.0) + .or_else(|| frontend.as_ref().map(|frontend| &frontend.bank)); let Some(bank) = bank else { for event in events.read().filter(|_| !crate::AudioSilent::active()) { diag::warn!( @@ -211,12 +182,12 @@ fn play_ui_sound_messages( &event.alias, ); let outcome = play_alias_oneshot( - &bank.0, + bank, ns, alias, None, &runtime, - &mut pending, + &mut pending.0, &mut decisions, Some(crate::SND_ENT_LOCAL), SoundClass::Ui, @@ -233,6 +204,7 @@ fn play_ui_music_messages( mut stop_events: MessageReader, mut desired: ResMut, bank: Option>, + frontend: Option>, runtime: Res, ) { if stop_events.read().count() > 0 { @@ -255,11 +227,15 @@ fn play_ui_music_messages( return; } desired.source = None; + let bank = bank + .as_ref() + .map(|bank| &bank.0) + .or_else(|| frontend.as_ref().map(|frontend| &frontend.bank)); let Some(bank) = bank else { return; }; let Some(cue) = runtime.source_cue(crate::sources::SourceCueRequest { - bank: bank.0.clone(), + bank: bank.clone(), namespace: asset_core::AssetNamespace::Iw4, alias, emitter: None, diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index 006c013..76ffb5a 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -24,7 +24,7 @@ pub use aliases::{ land_aliases, quiet_surface_alias, select_cg_fire_alias, select_fire_alias, step_prefix, surface_alias_candidates, world_surface_alias, }; -pub use ambient::{MapAmbientBooted, SoundIwd}; +pub use ambient::{MapAmbientBooted, SoundIwd, stage_prepared_match_audio}; pub use clip_store::{ClipPath, ClipPathCost, ClipPrepCost, ClipStore, clip_prep_cost}; pub use emit::{BobCycleTracker, emit_footstep_on_bob_wrap, emit_weapon_fire}; pub use frontend::FrontendAudio; @@ -80,3 +80,5 @@ mod spatial; mod cue_execution; mod event; pub use event::{AnimationMarkerId, AudioEvent, AudioEventId, AudioOccurrence}; + +pub use ambient::LoadingAudioStatus; diff --git a/crates/audio/src/match_set.rs b/crates/audio/src/match_set.rs index a624301..490aab2 100644 --- a/crates/audio/src/match_set.rs +++ b/crates/audio/src/match_set.rs @@ -1,3 +1,4 @@ +use frame::ScopeApp; use std::collections::{BTreeSet, HashSet}; use asset_audio::SoundCatalog; @@ -6,10 +7,10 @@ use asset_game::WeaponRegistry; use assets::{MapLoadProcess, MatchType10SoundHints, PreparedWeapons}; use bevy::ecs::system::SystemParam; use bevy::prelude::*; -use frame::{ClientSet, LaunchIdentity, MatchTornDown, ReturnedToMenu}; +use frame::{ClientSet, LaunchIdentity}; use crate::aliases::movement_prepare_names; -use crate::ambient::{SoundBankCompose, SoundBankLoadAttempted, SoundBankNamespace}; +use crate::ambient::{SoundBankCompose, SoundBankNamespace}; use crate::clip_store::{ClipKey, ClipStore, clip_keys_for_alias}; use crate::playback::SoundBank; @@ -46,7 +47,7 @@ struct MatchRequests { #[derive(Resource, Default)] struct MatchClipPrep { - generation: frame::WorldGeneration, + generation: frame::WorldStamp, submitted: bool, capacity_failure: bool, required: HashSet, @@ -67,13 +68,13 @@ struct AudioLoadScope<'w> { } impl AudioLoadScope<'_> { - fn generation(&self) -> frame::WorldGeneration { + fn generation(&self) -> frame::WorldStamp { self.accepted .as_ref() .map(|accepted| accepted.load_key) .or_else(|| self.incoming.as_ref().map(|incoming| incoming.load_key)) - .map(|key| frame::WorldGeneration::from_install(key.local_load_request_id)) - .unwrap_or(*self.installed) + .map(|key| frame::WorldStamp::from_install(key.local_load_request_id)) + .unwrap_or(self.installed.stamp()) } } @@ -90,46 +91,24 @@ pub(crate) fn register(app: &mut App) { diag::info!(Audio, "audio: Silent (IW4L_SOUND=off)"); app.insert_resource(AudioSilent); } - app.init_resource::() - .init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, ( - queue_match_clips.after(crate::ambient::install_sound_bank), - poll_match_audio_ready.after(queue_match_clips), - reset_match_audio_on_match_end, + queue_match_clips + .in_set(frame::InMatch) + .after(crate::ambient::install_sound_bank), + poll_match_audio_ready + .in_set(frame::InMatch) + .after(queue_match_clips), ) .in_set(ClientSet::Load), ); } -fn reset_match_audio_on_match_end( - mut torn: MessageReader, - mut returned: MessageReader, - mut ready: ResMut, - mut prep: ResMut, - mut announcer: ResMut, -) { - let retired = torn.read().fold(false, |retired, event| { - retired || event.world_generation == prep.generation - }); - let menu = returned.read().fold(false, |menu, event| { - menu || (event.had_world && prep.generation.0.is_none()) - }); - if !retired && !menu { - return; - } - *ready = AudioReady::default(); - if let Some(stage) = prep.stage.take() { - stage.cancel(); - } - *prep = MatchClipPrep::default(); - *announcer = Default::default(); -} - fn queue_match_clips( - attempted: Res, walk: Option>, mut clips: Option>, weapons: Option>, @@ -173,9 +152,6 @@ fn queue_match_clips( diag::info!(Audio, "audio: AudioReady — Silent, no clips prepared"); return; } - if !attempted.0 { - return; - } if walk.is_some() { return; } @@ -193,7 +169,7 @@ fn queue_match_clips( ); return; }; - if *load_scope.installed != scope { + if load_scope.installed.stamp() != scope { return; } let Some(weapons) = weapons else { diff --git a/crates/audio/src/messages.rs b/crates/audio/src/messages.rs index ba6aa6b..48e8ce4 100644 --- a/crates/audio/src/messages.rs +++ b/crates/audio/src/messages.rs @@ -35,6 +35,7 @@ pub enum AliasCommand { #[derive(Message, Clone, Debug)] pub struct Footstep { pub event: Option, + pub volume_scale: f32, pub alias: &'static str, pub fallback: &'static str, pub origin_inches: Option<[f32; 3]>, @@ -67,7 +68,7 @@ pub struct ViewmodelNotetrack { #[derive(Message, Clone, Debug)] pub struct ViewmodelNotetracks { - pub generation: frame::WorldGeneration, + pub generation: frame::WorldStamp, pub client: sim::ClientId, pub life: sim::LifeSequence, pub weapon: u32, @@ -78,6 +79,7 @@ pub struct ViewmodelNotetracks { #[derive(Message, Clone, Debug)] pub struct LandSound { pub event: Option, + pub volume_scale: f32, pub alias: &'static str, pub fallback: &'static str, pub origin_inches: Option<[f32; 3]>, diff --git a/crates/audio/src/playback.rs b/crates/audio/src/playback.rs index af8e051..1f9d861 100644 --- a/crates/audio/src/playback.rs +++ b/crates/audio/src/playback.rs @@ -1,19 +1,20 @@ +use frame::ScopeApp; use std::sync::Arc; use crate::runtime::AudioRuntime; use asset_audio::SoundCatalog; use asset_core::AssetNamespace; use bevy::prelude::*; -use frame::{ClientSet, FxSoundPublished, MatchTornDown, SessionSwapApplied}; +use frame::{ClientSet, FxSoundPublished}; use net::{LastAdoptedSnapshot, SvcLocalSound}; -use crate::backend::MatchEpoch; use crate::clip_store::CueFeedback; use crate::messages::{ AliasCommand, BoundWeaponSound, Footstep, LandSound, PlayAlias, SND_ENT_LOCAL, ViewmodelNotetracks, WeaponSound, }; use crate::start::{SoundClass, StartDecision, StartDecisions, StartFailure, StartOutcome}; +use frame::{MatchScope, ScopeEpoch}; #[derive(Component, Default)] pub struct AmbientListener; @@ -47,29 +48,31 @@ pub(crate) struct PlayerSoundPlugin; impl Plugin for PlayerSoundPlugin { fn build(&self, app: &mut App) { - app.init_resource::() + app.scoped::(frame::MatchScope::Loading) + .init_resource::() .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .add_message::() - .add_message::() - .add_message::() - .add_message::() - .add_message::() - .init_resource::() - .add_message::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Loading) + .scoped::(frame::MatchScope::Loading) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) .add_systems( Update, play_weapon_sound_messages + .in_set(frame::InMatch) .in_set(ClientSet::Predict) .after(frame::OwnerEventsPublished) .after(crate::backend::publish_audio_context), @@ -77,25 +80,37 @@ impl Plugin for PlayerSoundPlugin { .add_systems( Update, ( - crate::ambient::boot_map_ambient_once, + crate::ambient::boot_map_ambient_once.in_set(frame::InMatch), crate::script_ambient::update_script_ambient + .in_set(frame::InMatch) .after(crate::ambient::boot_map_ambient_once), collect_cue_decisions + .in_set(frame::InMatch) .after(play_alias_messages) .before(play_footstep_messages) .before(play_land_sound_messages), apply_svc_local_sound + .in_set(frame::InMatch) .before(play_alias_messages) .run_if(resource_exists::), - crate::shellshock::update_shellshock_tinnitus.before(play_alias_messages), - crate::breath::update.before(play_alias_messages), - play_alias_messages.after(FxSoundPublished), - play_footstep_messages, + crate::shellshock::update_shellshock_tinnitus + .in_set(frame::InMatch) + .before(play_alias_messages), + crate::breath::update + .in_set(frame::InMatch) + .before(play_alias_messages), + play_alias_messages + .in_set(frame::InMatch) + .after(FxSoundPublished), + play_footstep_messages.in_set(frame::InMatch), crate::entity_events::play_viewmodel_notetrack_messages + .in_set(frame::InMatch) .before(play_bound_weapon_sounds), - play_bound_weapon_sounds.after(collect_cue_decisions), - play_land_sound_messages, - crate::destructible_loops::update, + play_bound_weapon_sounds + .in_set(frame::InMatch) + .after(collect_cue_decisions), + play_land_sound_messages.in_set(frame::InMatch), + crate::destructible_loops::update.in_set(frame::InMatch), ) .in_set(ClientSet::Effects), ) @@ -103,29 +118,19 @@ impl Plugin for PlayerSoundPlugin { Update, ( crate::ambient::start_sound_bank_compose - .after(crate::ambient::stop_map_ambient_on_match_end), + .run_if(in_state(frame::MatchScope::Loading)), crate::ambient::install_sound_bank + .run_if(in_state(frame::MatchScope::Loading)) .after(crate::ambient::start_sound_bank_compose), crate::entity_events::bind_notetrack_sounds + .in_set(frame::InMatch) .after(crate::ambient::install_sound_bank), - crate::ambient::stop_map_ambient_on_match_end.after(SessionSwapApplied), - reset_clip_prep_on_match_torn_down, ) .in_set(ClientSet::Load), ); } } -fn reset_clip_prep_on_match_torn_down( - mut torn: MessageReader, - mut pending: ResMut, -) { - if torn.read().count() == 0 { - return; - } - pending.clear(); -} - fn apply_svc_local_sound( mut cmds: MessageReader, adopted: Res, @@ -188,7 +193,7 @@ fn play_alias_messages( mut pending: ResMut, mut decisions: ResMut, bank: Option>, - epoch: Res, + epoch: Res>, local: Res, ) { for command in events.read() { @@ -229,6 +234,7 @@ fn play_alias_messages( SoundClass::World, epoch.0, pitch_scale, + 1.0, event.fallback.iter().cloned().collect(), event.event, ); @@ -242,7 +248,7 @@ fn play_footstep_messages( mut pending: ResMut, mut decisions: ResMut, bank: Option>, - epoch: Res, + epoch: Res>, ) { let Some(bank) = bank else { drop_without_bank(events.read().map(|e| (e.alias, e.event)), &mut decisions); @@ -261,6 +267,7 @@ fn play_footstep_messages( SoundClass::World, epoch.0, event.event, + event.volume_scale, ); if outcome.allows_binding_fallback() { gaps.record(event.alias); @@ -276,7 +283,7 @@ fn play_weapon_sound_messages( mut pending: ResMut, mut decisions: ResMut, bank: Option>, - epoch: Res, + epoch: Res>, ) { let Some(bank) = bank else { drop_without_bank( @@ -299,6 +306,7 @@ fn play_weapon_sound_messages( SoundClass::Weapon, epoch.0, 1.0, + 1.0, Vec::new(), event.event, ); @@ -315,7 +323,7 @@ fn play_bound_weapon_sounds( mut pending: ResMut, mut decisions: ResMut, bank: Option>, - epoch: Res, + epoch: Res>, local: Res, ) { let Some(bank) = bank else { @@ -345,6 +353,7 @@ fn play_bound_weapon_sounds( SoundClass::Weapon, epoch.0, 1.0, + 1.0, Vec::new(), event.event, ); @@ -361,7 +370,7 @@ fn play_land_sound_messages( mut pending: ResMut, mut decisions: ResMut, bank: Option>, - epoch: Res, + epoch: Res>, ) { let Some(bank) = bank else { drop_without_bank(events.read().map(|e| (e.alias, e.event)), &mut decisions); @@ -380,6 +389,7 @@ fn play_land_sound_messages( SoundClass::World, epoch.0, event.event, + event.volume_scale, ); if outcome.allows_binding_fallback() { gaps.record(event.alias); @@ -424,6 +434,7 @@ fn play_surface_alias_chain( class: SoundClass, epoch: u64, event: Option, + volume_scale: f32, ) -> StartOutcome { let candidates = crate::aliases::surface_alias_candidates(alias, fallback); let Some(first) = candidates.first() else { @@ -442,6 +453,7 @@ fn play_surface_alias_chain( class, epoch, 1.0, + volume_scale, candidates[1..] .iter() .map(|name| (*name).to_owned()) @@ -514,6 +526,7 @@ pub(crate) fn play_alias_oneshot( class, epoch, 1.0, + 1.0, Vec::new(), None, ) @@ -533,6 +546,7 @@ fn play_oneshot_recorded( class: SoundClass, epoch: u64, pitch_scale: f32, + volume_scale: f32, fallbacks: Vec, event: Option, ) -> StartOutcome { @@ -547,6 +561,7 @@ fn play_oneshot_recorded( class, epoch, pitch_scale, + volume_scale, fallbacks, }); let outcome = StartOutcome::Pending; diff --git a/crates/audio/src/rumble.rs b/crates/audio/src/rumble.rs index 9e024cc..42a75ce 100644 --- a/crates/audio/src/rumble.rs +++ b/crates/audio/src/rumble.rs @@ -167,12 +167,7 @@ struct ActiveRumble { #[derive(Default)] struct RumblePlayback { - owner: Option<( - frame::WorldGeneration, - sim::ClientId, - sim::LifeSequence, - u64, - )>, + owner: Option<(frame::WorldStamp, sim::ClientId, sim::LifeSequence, u64)>, last_time: i32, active: Vec, output: Option<(Entity, GamepadRumbleIntensity)>, @@ -298,6 +293,7 @@ pub(crate) fn register(app: &mut App) { .add_systems( Update, update + .in_set(frame::InMatch) .in_set(net::ClientSet::Effects) .after(crate::entity_events::play_viewmodel_notetrack_messages), ); @@ -345,7 +341,7 @@ fn update( let owner = bank.as_ref().and_then(|bank| { let meta = presented.snapshot()?.meta.for_client(local.0)?; (meta.lifecycle == sim::ClientLifecycle::Alive).then_some(( - *generation, + generation.stamp(), local.0, meta.life_sequence, bank.0.revision(), diff --git a/crates/audio/src/runtime.rs b/crates/audio/src/runtime.rs index 393cd2c..c8804e2 100644 --- a/crates/audio/src/runtime.rs +++ b/crates/audio/src/runtime.rs @@ -338,6 +338,7 @@ impl AudioRuntime { class, epoch, pitch_scale, + volume_scale, fallbacks, } = request; if fallbacks.len() > 16 { @@ -374,6 +375,7 @@ impl AudioRuntime { scope: class.scope(), epoch, pitch_scale, + volume_scale, execution: CueIntent { event, origin_inches, @@ -1006,6 +1008,7 @@ fn control( scope: source.key.scope, epoch: source.key.epoch, pitch_scale: 1.0, + volume_scale: 1.0, execution: CueIntent { event: None, origin_inches: source.origin_inches, diff --git a/crates/audio/src/script_ambient.rs b/crates/audio/src/script_ambient.rs index 5dbef6d..ee5f71d 100644 --- a/crates/audio/src/script_ambient.rs +++ b/crates/audio/src/script_ambient.rs @@ -14,7 +14,7 @@ pub(crate) struct ScriptAmbientPlayback { } pub(crate) fn update_script_ambient( - epoch: Res, + epoch: Res>, ready: Res, generation: Res, loading: Option>, @@ -34,7 +34,8 @@ pub(crate) fn update_script_ambient( playback.voices.clear(); } playback.voices.retain(CueHandle::active); - if !ready.0.ready_for(*generation) || loading.is_some_and(|screen| !screen.is_complete()) { + if !ready.0.ready_for(generation.stamp()) || loading.is_some_and(|screen| !screen.is_complete()) + { return; } let Some(presented) = presented else { return }; @@ -110,6 +111,7 @@ pub(crate) fn update_script_ambient( class: SoundClass::Ambience, epoch: epoch.0, pitch_scale: 1.0, + volume_scale: 1.0, fallbacks: Vec::new(), }, frames, diff --git a/crates/audio/src/script_mix.rs b/crates/audio/src/script_mix.rs index 53f94fa..adb081c 100644 --- a/crates/audio/src/script_mix.rs +++ b/crates/audio/src/script_mix.rs @@ -1,8 +1,9 @@ -use crate::backend::MatchEpoch; use crate::media::LiveGain; use asset_audio::ChannelKey; use asset_core::AssetNamespace; use bevy::prelude::*; +use frame::ScopeApp; +use frame::{MatchScope, ScopeEpoch}; use std::{collections::HashMap, sync::Arc}; #[derive(Resource, Default)] @@ -165,7 +166,7 @@ fn update_channel_mix( bank: Option>, namespace: Option>, local: Option>, - epoch: Res, + epoch: Res>, runtime: Res, ) { let now = runtime.audio_frame(); @@ -236,7 +237,12 @@ fn update_channel_mix( } pub(crate) fn register(app: &mut App) { - app.init_resource::() - .init_resource::() - .add_systems(Update, update_channel_mix.in_set(net::ClientSet::Effects)); + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .add_systems( + Update, + update_channel_mix + .in_set(frame::InMatch) + .in_set(net::ClientSet::Effects), + ); } diff --git a/crates/audio/src/script_music.rs b/crates/audio/src/script_music.rs index 2bc615b..aa44fc3 100644 --- a/crates/audio/src/script_music.rs +++ b/crates/audio/src/script_music.rs @@ -3,16 +3,16 @@ use crate::cue::CueHandle; use crate::cue_execution::CueTrigger; use crate::{SoundBank, SoundClass}; use bevy::prelude::*; +use frame::ScopeApp; #[derive(Resource, Default)] pub(crate) struct ScriptMusicPlayback { - epoch: u64, pending: Option, active: Option, } pub(crate) fn update_script_music( - epoch: Res, + epoch: Res>, ready: Res, generation: Res, runtime: Res, @@ -24,11 +24,6 @@ pub(crate) fn update_script_music( mut feedback: ResMut, ) { mix.reset_epoch(epoch.0); - if playback.epoch != epoch.0 { - playback.epoch = epoch.0; - playback.pending = None; - playback.active = None; - } if playback .active .as_ref() @@ -68,7 +63,8 @@ pub(crate) fn update_script_music( } } } - if !ready.0.ready_for(*generation) || loading.is_some_and(|screen| !screen.is_complete()) { + if !ready.0.ready_for(generation.stamp()) || loading.is_some_and(|screen| !screen.is_complete()) + { return; } let Some(bank) = bank else { return }; @@ -87,6 +83,7 @@ pub(crate) fn update_script_music( class: SoundClass::Music, epoch: epoch.0, pitch_scale: 1.0, + volume_scale: 1.0, fallbacks: Vec::new(), }); feedback.push(CueHandle(handle.0.clone())); @@ -94,6 +91,11 @@ pub(crate) fn update_script_music( } pub(crate) fn register(app: &mut App) { - app.init_resource::() - .add_systems(Update, update_script_music.in_set(net::ClientSet::Effects)); + app.scoped::(frame::MatchScope::Live) + .add_systems( + Update, + update_script_music + .in_set(frame::InMatch) + .in_set(net::ClientSet::Effects), + ); } diff --git a/crates/audio/src/shellshock.rs b/crates/audio/src/shellshock.rs index 2f02616..66aac1f 100644 --- a/crates/audio/src/shellshock.rs +++ b/crates/audio/src/shellshock.rs @@ -1,6 +1,6 @@ use asset_core::AssetNamespace; use bevy::prelude::*; -use frame::{AppScreen, LifeEnded, MatchTornDown}; +use frame::{AppScreen, LifeEnded}; use hud_iw4::shellshock_remaining_ms; use net::{FrameClock, LocalPresentClient, PresentedSnapshot}; @@ -16,7 +16,6 @@ pub(crate) struct ShellshockSources { } pub(crate) fn update_shellshock_tinnitus( - mut torn: MessageReader, mut died: MessageReader, screen: Option>, cg_clock: Option>, @@ -26,9 +25,8 @@ pub(crate) fn update_shellshock_tinnitus( runtime: Res, mut sources: ResMut, mut play: MessageWriter, - epoch: Res, + epoch: Res>, ) { - let torn = torn.read().next().is_some(); let local_id = local.as_ref().map(|l| l.0.0); let life = local.as_ref().and_then(|local| { presented @@ -41,7 +39,7 @@ pub(crate) fn update_shellshock_tinnitus( let died = died .read() .any(|ev| local_id == Some(ev.client) && life == Some(ev.life)); - if torn || !screen.is_some_and(|s| matches!(*s, AppScreen::InGame)) { + if !screen.is_some_and(|s| matches!(*s, AppScreen::InGame)) { sources.source = None; sources.context = None; sources.was_active = false; diff --git a/crates/bootstrap/src/bench/facts.rs b/crates/bootstrap/src/bench/facts.rs index 3e3de82..7706cc5 100644 --- a/crates/bootstrap/src/bench/facts.rs +++ b/crates/bootstrap/src/bench/facts.rs @@ -70,11 +70,11 @@ pub(crate) fn collect( demo: None, role: None, adapter: adapter.map(|adapter| AdapterFacts { - name: adapter.0.name.clone(), - backend: format!("{:?}", adapter.0.backend), - device_type: format!("{:?}", adapter.0.device_type), - driver: adapter.0.driver.clone(), - driver_info: adapter.0.driver_info.clone(), + name: adapter.name.clone(), + backend: format!("{:?}", adapter.backend), + device_type: format!("{:?}", adapter.device_type), + driver: adapter.driver.clone(), + driver_info: adapter.driver_info.clone(), // Whether the *device* was created with the feature, not whether // the adapter could have offered it: a pass can only be timed if // the device we actually hold can resolve the query. @@ -145,8 +145,8 @@ pub(crate) fn announce( "runtime: backend={} adapter={:?} window={} target={}x{} present={}", adapter .as_ref() - .map_or_else(|| "unknown".to_owned(), |a| format!("{:?}", a.0.backend)), - adapter.as_ref().map_or("unknown", |a| a.0.name.as_str()), + .map_or_else(|| "unknown".to_owned(), |a| format!("{:?}", a.backend)), + adapter.as_ref().map_or("unknown", |a| a.name.as_str()), window.map_or_else( || "unknown".to_owned(), |w| format!( diff --git a/crates/bootstrap/src/bench/milestones.rs b/crates/bootstrap/src/bench/milestones.rs index 0c7a88f..07cd03c 100644 --- a/crates/bootstrap/src/bench/milestones.rs +++ b/crates/bootstrap/src/bench/milestones.rs @@ -9,7 +9,7 @@ use asset_transport::{LoadLaneTiming, LoadProgress}; use assets::{LoadingScreen, MatchLoadBusy, MatchLoadRequest}; use audio::MapAmbientBooted; use bevy::prelude::*; -use frame::MatchInstalled; +use frame::InstalledMatch; use render_frontend::prepare::scene::cull::DpvsFrameStats; use render_frontend::prepare::scene::world::WorldScene; use render_gpu::ColourWorkingSet; @@ -142,7 +142,7 @@ fn sanitize(zone: &str) -> String { pub(crate) fn poll( request: Option>, busy: Option>, - mut installed: MessageReader, + installed: Option>, scene: Option>, loading: Option>, screen: Option>, @@ -152,7 +152,7 @@ pub(crate) fn poll( ) { let now = Instant::now(); let saw_request = request.is_some() || busy.is_some_and(|busy| busy.0); - let saw_installed = installed.read().next().is_some(); + let saw_installed = installed.is_some_and(|installed| installed.is_added()); let spawned_now = scene.as_deref().is_some_and(|scene| scene.spawned); let overlay_now = loading.is_some(); let ingame_now = screen.is_some_and(|screen| matches!(*screen, ui::AppScreen::InGame)); diff --git a/crates/bootstrap/src/launch.rs b/crates/bootstrap/src/launch.rs index 8a2f748..c9294a0 100644 --- a/crates/bootstrap/src/launch.rs +++ b/crates/bootstrap/src/launch.rs @@ -6,14 +6,12 @@ use asset_transport::{LoadProgress, find_runtime_common_mp, find_zone_file, list use assets::{ LoadingPreviewSource, LoadingScreen, MatchLoadRequest, NamespaceSoundIwd, NamespaceTrees, }; -use audio::{SoundBank, SoundIwd}; use bevy::prelude::*; use bevy::window::PresentMode; use render::diag::acceptance::{ ACCEPTANCE_HEIGHT, ACCEPTANCE_PRESENT_MODE, ACCEPTANCE_WIDTH, AcceptanceRun, }; use render::diag::capture::{CaptureQueue, CaptureRequest}; -use render_frontend::prepare::scene::world::WorldScene; use replay::{Playback, ReplayPlayback}; use session::StartupCommands; use ui::{ @@ -90,6 +88,8 @@ fn install_class_catalog( shell: Option>, mut strings: ResMut, menus: Res, + account: Option>, + mut player_data: ResMut, ) { use bevy::tasks::futures_lite::future; let Some(mut shell) = shell else { @@ -101,6 +101,63 @@ fn install_class_catalog( for line in &common.report { diag::info!(Launch, "{line}"); } + match ( + common.player_schemas.get("mp/playerdata.def"), + common.weapons.rank_progression(), + ) { + (Some(definitions), Ok(ranks)) => { + if let Err(error) = player_data.install( + definitions, + ranks, + account + .as_ref() + .and_then(|account| account.snapshot.as_ref()), + ) { + diag::warn!(Launch, "progression: cannot read account: {error:?}"); + } + } + (None, _) => diag::warn!(Launch, "progression: shell player data schema is missing"), + (_, Err(error)) => diag::warn!(Launch, "progression: shell ranks unavailable: {error}"), + } + player_data.install_unlock_data( + common + .player_defaults_config + .as_ref() + .map(|config| sim::PlayerDataDefaults { + config: config.clone(), + class_names: std::array::from_fn(|index| { + let key = format!("CLASS_SLOT{}", index + 1); + common + .strings + .raw_text(&key) + .filter(|bytes| bytes.first().is_some_and(|byte| *byte != 0)) + .map_or_else(|| key.into_bytes(), <[u8]>::to_vec) + }), + }), + common + .challenges + .as_ref() + .map(|table| sim::script::StringTable { + columns: table.columns, + rows: table.rows, + cells: table.cells.clone(), + }), + ); + for (name, table) in [ + ("mp/ranktable.csv", common.weapons.shared_rank_table()), + ("mp/rankicontable.csv", common.weapons.shared_rank_icons()), + ] { + if let Ok(table) = table { + player_data.install_rank_table( + name, + sim::script::StringTable { + columns: table.columns, + rows: table.rows, + cells: table.cells.clone(), + }, + ); + } + } commands.insert_resource(ui::frontend::maps::MapPresentation::from_tables( &common.tables, common.ui_images.clone(), @@ -400,7 +457,6 @@ fn run_menu( diag::warn!(Launch, "menu: {line}"); } let bank = std::sync::Arc::new(loaded.catalog); - app.insert_resource(SoundBank(std::sync::Arc::clone(&bank))); Some(bank) } Err(error) => { @@ -414,16 +470,9 @@ fn run_menu( diag::info!(Launch, "menu: {line}"); } let menu_iwd = std::sync::Arc::new(menu_iwd); - app.insert_resource(SoundIwd(std::sync::Arc::clone(&menu_iwd))); if let Some(bank) = menu_bank { - app.insert_resource(audio::ClipStore::start( - std::sync::Arc::clone(&bank), - Some(std::sync::Arc::clone(&menu_iwd)), - )); - app.insert_resource(audio::FrontendAudio { - bank, - iwd: menu_iwd, - }); + let clips = audio::ClipStore::start(std::sync::Arc::clone(&bank), Some(menu_iwd)); + app.insert_resource(audio::FrontendAudio { bank, clips }); } app.insert_resource(launch_identity(&config)) .insert_resource(cheats) @@ -433,7 +482,6 @@ fn run_menu( .insert_resource(StartupCommands { lines: console::startup_commands(), }) - .insert_resource(WorldScene::default()) .insert_resource(ClearColor(Color::srgb(0.04, 0.045, 0.06))) .insert_resource({ let mut layers = UiLayers::default(); @@ -450,8 +498,22 @@ fn run_menu( }, ); } + bench::announce_runtime(&mut app); + if bench::enabled() { + bench::insert( + &mut app, + "menu-session", + None, + "menu", + &config.artifacts, + asset_transport::LoadProgress::default(), + ); + } app.run(); - let _ = flush_perf(); + let trace = flush_perf(); + if bench::enabled() { + bench::finish(&config.artifacts, trace); + } } fn run_play( @@ -625,7 +687,6 @@ fn run_map( .insert_resource(StartupCommands { lines: console::startup_commands(), }) - .insert_resource(WorldScene::default()) .insert_resource(ClearColor(Color::BLACK)) .insert_resource(AppScreen::Loading) .insert_resource({ @@ -652,6 +713,9 @@ fn run_map( Role::Listen | Role::Dedicated => add_runtime_plugins(&mut app), Role::Client => add_runtime_plugins_with_role(&mut app, frame::RuntimeRole::Client), } + app.world_mut() + .resource_mut::() + .begin_load(); // The demo, before the playback moves into the world: it names the workload // in the bench manifest, and "same demo" is what makes two runs comparable // at all. The whole path, not a stem — a clip is `clips//clip.iw4ldemo`, diff --git a/crates/bootstrap/src/plugins.rs b/crates/bootstrap/src/plugins.rs index 71cf212..b841a59 100644 --- a/crates/bootstrap/src/plugins.rs +++ b/crates/bootstrap/src/plugins.rs @@ -47,24 +47,9 @@ pub fn add_runtime_plugins_with_role(app: &mut App, role: RuntimeRole) { .add_plugins(RenderPlugin) .add_plugins(SessionPlugin); - app.edit_schedule(Update, |schedule| { - schedule.set_executor(bevy::ecs::schedule::SingleThreadedExecutor::new()); - }); - - app.edit_schedule(First, |schedule| { - schedule.set_executor(bevy::ecs::schedule::SingleThreadedExecutor::new()); - }); - app.edit_schedule(PreUpdate, |schedule| { - schedule.set_executor(bevy::ecs::schedule::SingleThreadedExecutor::new()); - }); - app.edit_schedule(PostUpdate, |schedule| { - schedule.set_executor(bevy::ecs::schedule::SingleThreadedExecutor::new()); - }); - app.edit_schedule(Last, |schedule| { - schedule.set_executor(bevy::ecs::schedule::SingleThreadedExecutor::new()); - }); - if let Some(render_app) = app.get_sub_app_mut(bevy::render::RenderApp) { + // Measured: multithreaded render schedules compete with camera prepare's own + // ComputeTaskPool lanes for the same workers and lose ~5% fps. render_app.edit_schedule(bevy::render::renderer::RenderGraph, |schedule| { schedule.set_executor(bevy::ecs::schedule::SingleThreadedExecutor::new()); }); @@ -138,7 +123,6 @@ const PIPELINED_RENDERING_ENV: &str = "IW4L_PIPELINED_RENDERING"; /// boundary; the bounded render channel permits one outstanding frame. /// Set IW4L_PIPELINED_RENDERING=0 for synchronous presentation. /// -/// Off by default on macOS. fn pipelined_rendering() -> bool { match std::env::var_os(PIPELINED_RENDERING_ENV) { None => !cfg!(target_os = "macos"), diff --git a/crates/bots/src/plugin.rs b/crates/bots/src/plugin.rs index fcba285..0bff8a4 100644 --- a/crates/bots/src/plugin.rs +++ b/crates/bots/src/plugin.rs @@ -1,6 +1,7 @@ use bevy::ecs::system::SystemParam; use bevy::prelude::*; use bevy::tasks::{Task, futures_lite::future}; +use frame::ScopeApp; use net::{ AUTHORITY_MS, AuthorityClock, AuthorityWorld, ClientActionInbox, ClientCommandInbox, LocalPresentClient, ReliableEventHub, authority_should_tick, look_angles_from_degrees, @@ -14,7 +15,7 @@ use crate::roster::{ default_class_index, }; use crate::sensor; -use frame::{AuthoritySet, BotNavigationReady, ClientSet, HasWorld, MatchTornDown, RuntimeRole}; +use frame::{AuthoritySet, BotNavigationReady, ClientSet, RuntimeRole}; const VIEW_PITCH_DOWN: f32 = 85.0; const TRACE_QUOTA: u32 = 96; @@ -99,35 +100,39 @@ pub struct BotsPlugin; impl Plugin for BotsPlugin { fn build(&self, app: &mut App) { - app.init_resource::() + app.scoped::(frame::MatchScope::Live) .init_resource::() .init_resource::() .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, ( - fill_bots_from_rules, - drain_bot_add_queue, - evict_bots_claiming_local_client, - boot_bots, - apply_bot_tp, + fill_bots_from_rules.in_set(frame::InMatch), + drain_bot_add_queue.in_set(frame::InMatch), + evict_bots_claiming_local_client.in_set(frame::InMatch), + boot_bots.in_set(frame::InMatch), + apply_bot_tp.in_set(frame::InMatch), ) .chain() - .after(ClientSet::Load), + .after(ClientSet::Load) + .before(ClientSet::Receive), ) .add_systems( Update, - (reset_roster_on_match_torn_down, prepare_navigation) + (prepare_navigation.in_set(frame::InMatch)) .chain() .in_set(ClientSet::Load), ) .add_systems( FixedUpdate, - (evict_bots_claiming_local_client, think_bots) + ( + evict_bots_claiming_local_client.in_set(frame::InMatch), + think_bots.in_set(frame::InMatch), + ) .chain() .in_set(AuthoritySet::Ingress) .run_if(authority_should_tick), @@ -135,28 +140,16 @@ impl Plugin for BotsPlugin { } } -fn reset_roster_on_match_torn_down( - mut torn: MessageReader, - mut roster: ResMut, - mut nav: ResMut, - mut ready: ResMut, -) { - if torn.read().len() == 0 { - return; - } - *roster = BotRoster::default(); - *nav = BotNav::default(); - *ready = BotNavigationReady::default(); -} - fn fill_bots_from_rules( mut roster: ResMut, mut queue: ResMut, - installed: Option>, + installed: Option>>, rules: Option>, world: Option>, ) { - if roster.rules_filled || !installed.is_some_and(|installed| installed.0) { + if roster.rules_filled + || !installed.is_some_and(|installed| *installed.get() == frame::MatchScope::Live) + { return; } let Some(world) = world else { @@ -246,14 +239,14 @@ fn boot_bots( mut roster: ResMut, mut actions: ResMut, mut request_ids: ResMut, - installed: Option>, + installed: Option>>, world: Option>, local: Res, ) { let Some(mut world) = world else { return; }; - if !installed.is_some_and(|installed| installed.0) { + if !installed.is_some_and(|installed| *installed.get() == frame::MatchScope::Live) { return; } let seed = roster.seed; @@ -588,19 +581,19 @@ fn think_bots(mut p: ThinkBots) { fn prepare_navigation( world: Option>, - installed: Option>, + installed: Option>>, role: Res, generation: Res, mut nav: ResMut, mut ready: ResMut, load: Option>, ) { - ready.0 = frame::WorldReadiness::new(*generation, frame::ReadinessState::Pending); + ready.0 = frame::WorldReadiness::new(generation.stamp(), frame::ReadinessState::Pending); if !matches!(*role, RuntimeRole::Listen | RuntimeRole::Dedicated) { ready.0.state = frame::ReadinessState::Ready; return; } - if !installed.is_some_and(|installed| installed.0) { + if !installed.is_some_and(|installed| *installed.get() == frame::MatchScope::Live) { return; } let Some(world) = world else { diff --git a/crates/clipmap_iw4/src/world_trace.rs b/crates/clipmap_iw4/src/world_trace.rs index 0561c87..69c9922 100644 --- a/crates/clipmap_iw4/src/world_trace.rs +++ b/crates/clipmap_iw4/src/world_trace.rs @@ -89,6 +89,7 @@ pub fn trace_brush_and_mesh_with_census( return (best, winner, census); } + let has_leaf_aabbs = !no_mesh && leaves_have_coll_aabb(map.leaves); let frac = core::cell::Cell::new(1.0_f32); let mut scratch = Vec::new(); let walk = walk_clip_tree(map, ext, &|| frac.get(), &mut |leaf| { @@ -115,7 +116,7 @@ pub fn trace_brush_and_mesh_with_census( mesh, leaf, ext, - map.leaves, + has_leaf_aabbs, &mut best, &mut census.mesh, &mut scratch, @@ -130,7 +131,7 @@ pub fn trace_brush_and_mesh_with_census( if !no_mesh { finish_mesh_forest_fallback( mesh, - map.leaves, + has_leaf_aabbs, ext, &mut best, &mut winner, @@ -169,12 +170,12 @@ pub fn trace_leaf_mesh_into( mesh: &ClipMeshRef<'_>, leaf: &ClipLeaf, ext: &TraceExtents, - leaves: &[ClipLeaf], + has_leaf_aabbs: bool, best: &mut Trace, census: &mut MeshWalkCensus, scratch: &mut Vec, ) { - if !leaves_have_coll_aabb(leaves) { + if !has_leaf_aabbs { return; } scratch.clear(); @@ -197,13 +198,13 @@ pub fn trace_leaf_mesh_into( pub fn finish_mesh_forest_fallback( mesh: &ClipMeshRef<'_>, - leaves: &[ClipLeaf], + has_leaf_aabbs: bool, ext: &TraceExtents, best: &mut Trace, winner: &mut ClipWorldWinner, census: &mut MeshWalkCensus, ) { - if leaves_have_coll_aabb(leaves) { + if has_leaf_aabbs { return; } if mesh.tri_indices.len() < 3 { diff --git a/crates/console/Cargo.toml b/crates/console/Cargo.toml index 0664349..c6b9c06 100644 --- a/crates/console/Cargo.toml +++ b/crates/console/Cargo.toml @@ -22,6 +22,7 @@ input_iw4.workspace = true net = { path = "../net" } render = { path = "../render" } render_frontend.workspace = true +render_anim.workspace = true session = { path = "../session" } render_fx.workspace = true replay = { path = "../replay" } diff --git a/crates/console/src/barracks_menu.rs b/crates/console/src/barracks_menu.rs index eddf485..33fa382 100644 --- a/crates/console/src/barracks_menu.rs +++ b/crates/console/src/barracks_menu.rs @@ -287,16 +287,16 @@ pub(crate) fn sync_profile( profile: Res, (generation, has_world, role): ( Res, - Res, + Res>, Res, ), local: Option>, link: Option>, mut inbox: Option>, mut seq: ResMut, - mut sent: Local>, + mut sent: Local>, ) { - if !has_world.0 || *role == frame::RuntimeRole::Replay { + if !(*has_world.get() == frame::MatchScope::Live) || *role == frame::RuntimeRole::Replay { *sent = None; return; } @@ -315,7 +315,7 @@ pub(crate) fn sync_profile( return; } let selection = profile.selection; - let next = (*generation, local.0, selection); + let next = (generation.stamp(), local.0, selection); if sent.as_ref() == Some(&next) { return; } diff --git a/crates/console/src/class_dispatch.rs b/crates/console/src/class_dispatch.rs index 21b74e2..541d433 100644 --- a/crates/console/src/class_dispatch.rs +++ b/crates/console/src/class_dispatch.rs @@ -252,9 +252,8 @@ fn describe_slot(store: &SessionClassStore, index: usize) -> String { fn class_listing(store: &SessionClassStore, highlight: usize) -> Vec { let mut lines = Vec::with_capacity(store.slots.len() + 1); lines.push(format!( - "class: {} presets, selected {highlight}, equipped {:?}", - store.slots.len(), - store.equipped + "class: {} presets, selected {highlight}", + store.slots.len() )); for index in 0..store.slots.len() { let marker = if index == highlight { '>' } else { ' ' }; diff --git a/crates/console/src/class_menu.rs b/crates/console/src/class_menu.rs index 522f8dd..f5aae2f 100644 --- a/crates/console/src/class_menu.rs +++ b/crates/console/src/class_menu.rs @@ -16,6 +16,7 @@ pub(crate) struct ClassMenuState { page: usize, attachments: bool, camo: bool, + samples: bool, hover: usize, } @@ -155,6 +156,12 @@ impl ClassMenuState { } fn choices(&self, catalog: &ClassLoadoutCatalog, store: &SessionClassStore) -> Vec { + if self.samples { + return ui::showcase_classes() + .iter() + .map(|preset| preset.name.to_owned()) + .collect(); + } let Some(row) = self.row else { return Vec::new(); }; @@ -187,6 +194,8 @@ impl ClassMenuState { pub(crate) fn register(registry: &mut ConsoleRegistry) { for name in [ + "ui_class_setup", + "ui_class_samples", "ui_class_slot", "ui_class_preview_slot", "ui_class_edit", @@ -217,6 +226,7 @@ pub(crate) fn route( mut state: Local, mut store: ResMut, catalog: Res, + player_data: Res, loc: ( Res, Option>, @@ -234,7 +244,34 @@ pub(crate) fn route( .filter(|command| command.name.starts_with("ui_class_")) { let result = (|| -> Result<(), String> { + if !matches!( + command.name.as_str(), + "ui_class_hover" | "ui_class_preview_slot" + ) { + if let Some(reason) = catalog.progression_lock("cac", &player_data) { + return Err(reason); + } + if !matches!(command.name.as_str(), "ui_class_setup" | "ui_class_slot") + && !player_data.custom_class_available(store.selected) + { + return Err("Custom class slot is locked".into()); + } + } match command.name.as_str() { + "ui_class_setup" => { + if !player_data.custom_class_available(store.selected) { + store.selected = 0; + } + *state = ClassMenuState::default(); + menus.write(UiMenuRequest::Open("class_setup".into())); + } + "ui_class_samples" => { + *state = ClassMenuState { + samples: true, + ..Default::default() + }; + menus.write(UiMenuRequest::Open("class_samples_picker".into())); + } "ui_class_hover" => state.hover = index(command)?.min(PAGE_SIZE - 1), "ui_class_preview_slot" => { let selected = index(command)?; @@ -243,6 +280,9 @@ pub(crate) fn route( } "ui_class_slot" => { let selected = index(command)?; + if !player_data.custom_class_available(selected) { + return Err("Custom class slot is locked".into()); + } let slot = store.slots.get(selected).ok_or("Class is unavailable")?; dvars.set("ui_class_name", &slot.name); store.selected = selected; @@ -261,6 +301,7 @@ pub(crate) fn route( state.hover = 0; state.attachments = command.name == "ui_class_attachments"; state.camo = false; + state.samples = false; if state.attachments && !matches!(row, ClassEditRow::Primary | ClassEditRow::Secondary) { @@ -331,6 +372,26 @@ pub(crate) fn route( .saturating_add_signed(delta) .min(len.saturating_sub(1) / PAGE_SIZE); } + "ui_class_pick" if state.samples => { + let at = index(command)?; + if at >= PAGE_SIZE { + return Err("Invalid sample row".into()); + } + let preset = ui::showcase_classes() + .get(state.page * PAGE_SIZE + at) + .ok_or("Sample is unavailable")?; + let candidate = ui::ClassSlotState::from_preset(preset); + catalog.validate_class(&candidate)?; + catalog.validate_progression(&candidate, &player_data)?; + let selected = store.selected; + *store + .slots + .get_mut(selected) + .ok_or("Class is unavailable")? = candidate; + dvars.set("ui_class_name", &store.slots[selected].name); + menus.write(UiMenuRequest::Close("class_samples_picker".into())); + *state = ClassMenuState::default(); + } "ui_class_pick" => { let row = state.row.ok_or("No loadout slot selected")?; let at = index(command)?; @@ -400,6 +461,18 @@ pub(crate) fn route( } } catalog.validate_edit(&candidate, row)?; + if let Some(reason) = + catalog.progression_lock(candidate.row_value(row), &player_data) + { + return Err(reason); + } + if candidate.perk1 == "specialty_bling" + && candidate.secondary_attachments.len() > 1 + && let Some(reason) = + catalog.progression_lock("specialty_secondarybling", &player_data) + { + return Err(reason); + } candidate.lock_reason = catalog.validate_class(&candidate).err(); if slot.lock_reason.is_none() && let Some(reason) = &candidate.lock_reason @@ -469,6 +542,7 @@ pub(crate) fn route( default }; catalog.validate_class(&candidate)?; + catalog.validate_progression(&candidate, &player_data)?; *slot = candidate; dvars.set("ui_class_name", &slot.name); } @@ -510,12 +584,29 @@ pub(crate) fn route( echo.write(format!("menu: {error}")); } } + let feature_lock = catalog.progression_lock("cac", &player_data); + dvars.set( + "ui_class_available", + if feature_lock.is_none() { "1" } else { "0" }, + ); + dvars.set( + "ui_class_feature_lock", + feature_lock.clone().unwrap_or_default(), + ); + dvars.set( + "ui_class_feature_label", + feature_lock.map_or_else( + || "CLASSES".to_owned(), + |reason| format!("CLASSES — {reason}"), + ), + ); for at in 0..10 { dvars.set( &format!("ui_class_slot_{at}"), store .slots .get(at) + .filter(|_| player_data.custom_class_available(at)) .map(|slot| class_name(&slot.name)) .unwrap_or_default(), ); @@ -702,6 +793,26 @@ pub(crate) fn route( ); state.page = state.page.min(choices.len().saturating_sub(1) / PAGE_SIZE); for at in 0..PAGE_SIZE { + let choice_lock = choices.get(state.page * PAGE_SIZE + at).and_then(|value| { + if state.samples { + let preset = ui::showcase_classes() + .iter() + .find(|preset| preset.name == value)?; + let candidate = ui::ClassSlotState::from_preset(preset); + catalog + .validate_class(&candidate) + .and_then(|()| catalog.validate_progression(&candidate, &player_data)) + .err() + } else if !state.attachments && !state.camo { + catalog.progression_lock(value, &player_data) + } else { + None + } + }); + dvars.set( + &format!("ui_class_choice_unavailable_{at}"), + choice_lock.unwrap_or_default(), + ); dvars.set( &format!("ui_class_game_{at}"), games @@ -722,6 +833,22 @@ pub(crate) fn route( .get(state.page * PAGE_SIZE + at) .map(|value| { let slot = store.slots.get(store.selected); + if state.samples { + let preset = ui::showcase_classes() + .iter() + .find(|preset| preset.name == value) + .expect("sample choice exists"); + let candidate = ui::ClassSlotState::from_preset(preset); + let reason = catalog + .validate_class(&candidate) + .and_then(|()| catalog.validate_progression(&candidate, &player_data)) + .err(); + return format!( + "{}{}", + class_name(value), + reason.map_or_else(String::new, |reason| format!(" — {reason}")) + ); + } if state.camo { let selected = slot .zip(state.row.and_then(camo_slot)) @@ -761,10 +888,14 @@ pub(crate) fn route( slot.row_value(row) == value } }); + let lock = (!state.attachments) + .then(|| catalog.progression_lock(value, &player_data)) + .flatten(); format!( - "{}{}", + "{}{}{}", if selected { "* " } else { "" }, - label(&key, &catalog, loc) + label(&key, &catalog, loc), + lock.map_or_else(String::new, |reason| format!(" — {reason}")) ) }) .unwrap_or_default(), @@ -791,7 +922,13 @@ pub(crate) fn route( .get(state.page * PAGE_SIZE + state.hover) .filter(|_| !state.camo) .and_then(|key| { - let lookup = if state.attachments { + let lookup = if state.samples { + ui::showcase_classes() + .iter() + .find(|preset| preset.name == key)? + .primary + .to_owned() + } else if state.attachments { let slot = store.slots.get(store.selected)?; format!("{}+{key}", slot.row_value(state.row?)) } else { @@ -854,7 +991,9 @@ pub(crate) fn route( .get(store.selected) .zip(state.row) .map_or("", |(slot, row)| slot.row_value(row)); - if state.camo { + if state.samples { + class_name(key) + } else if state.camo { localized_camo_label(&catalog, loc, weapon, key) } else if state.attachments && !key.is_empty() { label(&format!("{weapon}+{key}"), &catalog, loc) @@ -882,9 +1021,35 @@ pub(crate) fn route( } dvars.set( "ui_class_description", - preview - .and_then(|(_, preview)| loc.text(preview.desc_key.trim_start_matches('@'))) - .unwrap_or_default(), + if state.samples { + choices + .get(state.page * PAGE_SIZE + state.hover) + .and_then(|name| { + ui::showcase_classes() + .iter() + .find(|preset| preset.name == name) + }) + .map(|preset| { + let slot = ui::ClassSlotState::from_preset(preset); + ClassEditRow::ALL + .into_iter() + .map(|row| { + format!( + "{}: {}", + localized(loc, row.loc_key(), row.label()), + label(slot.row_value(row), &catalog, loc) + ) + }) + .collect::>() + .join("\n") + }) + .unwrap_or_default() + } else { + preview + .and_then(|(_, preview)| loc.text(preview.desc_key.trim_start_matches('@'))) + .unwrap_or_default() + .to_owned() + }, ); dvars.set( "ui_class_page", diff --git a/crates/console/src/diagnostics.rs b/crates/console/src/diagnostics.rs index e039b25..0c8d8a1 100644 --- a/crates/console/src/diagnostics.rs +++ b/crates/console/src/diagnostics.rs @@ -97,7 +97,7 @@ pub(crate) fn register_diagnostics_mirror(app: &mut App) { mirror_runtime_diagnostics, drain_pending_console_lines, finish_replay_playback, - update_showpos_overlay, + update_showpos_overlay.in_set(frame::InMatch), ) .in_set(ClientSet::Diag), ); diff --git a/crates/console/src/feature_dispatch/mod.rs b/crates/console/src/feature_dispatch/mod.rs index 2795d70..f6d85da 100644 --- a/crates/console/src/feature_dispatch/mod.rs +++ b/crates/console/src/feature_dispatch/mod.rs @@ -225,6 +225,10 @@ pub fn register_feature_commands(registry: &mut crate::ConsoleRegistry, maps: &[ "clip", "clip — save last ≤45s available to this client as iw4l-artifacts/clips//{clip.iw4ldemo, dump.txt} (always-on ring)", ), + ( + "round_restart", + "round_restart — restart the host's script round and retain match content (needs cheats)", + ), ( "map_restart", "map_restart — a new match on the current map, on what the last one prepared", @@ -299,7 +303,7 @@ pub fn register_feature_commands(registry: &mut crate::ConsoleRegistry, maps: &[ ), ( "wait", - "wait [seconds|t|world|spawn|torn|ambient] — pause the console FIFO; t = n authority ticks; world = scene.spawned; spawn = AppScreen::InGame; torn = HasWorld false and scene.spawned false (hold after MatchTornDown); ambient = MapAmbientBooted (overlay finished, CreateFX sources published)", + "wait [seconds|t|world|spawn|torn|ambient|progression] — pause the console FIFO; t = n authority ticks; world = scene.spawned; spawn = AppScreen::InGame; torn = match scope absent and scene.spawned false (after match retirement); ambient = MapAmbientBooted; progression = native account content ready", ), ] { if registry.resolve(name).is_none() { diff --git a/crates/console/src/feature_dispatch/replay.rs b/crates/console/src/feature_dispatch/replay.rs index bb31710..a0a6472 100644 --- a/crates/console/src/feature_dispatch/replay.rs +++ b/crates/console/src/feature_dispatch/replay.rs @@ -42,9 +42,9 @@ pub(crate) fn route_replay_commands( ResMut, ), clip: ( - Res, + Option>, Res, - Res, + Option>, Option>, Option>, ), @@ -212,6 +212,10 @@ pub(crate) fn route_replay_commands( echo("usage: clip".into(), console, line); continue; } + let (Some(ring), Some(prediction)) = (ring.as_ref(), prediction.as_ref()) else { + echo("clip: no live match".into(), console, line); + continue; + }; if ring.is_empty() { echo("clip: ring empty (0 ticks)".into(), console, line); continue; diff --git a/crates/console/src/feature_dispatch/session.rs b/crates/console/src/feature_dispatch/session.rs index 0ef7991..5ff79a4 100644 --- a/crates/console/src/feature_dispatch/session.rs +++ b/crates/console/src/feature_dispatch/session.rs @@ -1,5 +1,4 @@ use bevy::prelude::*; -use frame::HasWorld; use crate::{ConsoleCommand, ConsoleDispatch}; @@ -9,17 +8,33 @@ pub(crate) fn route_session_commands( mut events: MessageReader, mut echo: ConsoleEcho, mut transition: ResMut<::session::SessionSwapRequest>, - has_world: Res, + has_world: Res>, playback: Option>, bridge: Option>, manifest: Option>, mut dispatch: ResMut, + mut authority: Option>, ) { for cmd in events.read() { match cmd.name.as_str() { + "round_restart" => { + let result = if !cmd.args.is_empty() { + Err("usage: round_restart".into()) + } else if let Some(authority) = authority.as_deref_mut() { + authority.0.request_round_restart() + } else { + Err("round restart needs the match host".into()) + }; + match result { + Ok(()) => echo.write("round_restart: requested script round restart"), + Err(error) => echo.write(format!("round_restart: {error}")), + } + } "map_restart" => { let zone = manifest.as_ref().and_then(|manifest| match &manifest.map { - ::session::ManifestFact::Known(map) if has_world.0 => { + ::session::ManifestFact::Known(map) + if (*has_world.get() == frame::MatchScope::Live) => + { Some(format!("{}:{}", map.namespace.as_str(), map.name)) } _ => None, @@ -58,7 +73,7 @@ pub(crate) fn route_session_commands( } }, "disconnect" => { - let in_session = has_world.0 + let in_session = (*has_world.get() == frame::MatchScope::Live) || playback.is_some() || transition.dump_id().is_some() || bridge.is_some(); diff --git a/crates/console/src/feature_dispatch/state_dump.rs b/crates/console/src/feature_dispatch/state_dump.rs index 0058dde..fdc1aa7 100644 --- a/crates/console/src/feature_dispatch/state_dump.rs +++ b/crates/console/src/feature_dispatch/state_dump.rs @@ -21,6 +21,14 @@ pub(crate) fn route_state_dump_commands( Option>, Option>, ), + lifetime: ( + Option>, + Option>, + ), + fpv: ( + Option>, + Option>, + ), (audio_ready, decisions, gaps, clips, runtime): ( Option>, Option>, @@ -40,13 +48,56 @@ pub(crate) fn route_state_dump_commands( continue; } }; - let audio = audio_dump_section( + let mut audio = audio_dump_section( audio_ready.as_deref(), decisions.as_deref(), gaps.as_deref(), clips.as_deref(), runtime.as_deref(), ); + audio.push_str("\n[client_lifetimes]\n"); + if let Some(owners) = lifetime.0.as_ref() { + audio.push_str(&format!( + "retirement_serial = {}\nretired_resources = {}\n", + owners.retirement_serial, owners.retired_resources + )); + let mut live_resources = 0; + for (name, scope, kind, live) in owners.lifetimes() { + if matches!( + kind, + frame::scope::ScopeKind::Resource | frame::scope::ScopeKind::Staged + ) { + live_resources += usize::from(live); + } + audio.push_str(&format!( + "resource = {name:?} scope={scope:?} kind={kind:?} live={}\n", + usize::from(live) + )); + } + audio.push_str(&format!("scope_resources_live = {live_resources}\n")); + } + if let Some(trees) = lifetime.1.as_ref() { + audio.push_str(&format!( + "remote_tree_bindings = {}\nremote_tree_storage_bytes = {}\n", + trees.retained_bindings(), + trees.tree_storage_bytes() + )); + } else { + audio.push_str("remote_tree_bindings = 0\nremote_tree_storage_bytes = 0\n"); + } + audio.push_str("\n[viewmodel]\n"); + if let Some(status) = fpv.0.as_ref() { + audio.push_str(&format!("fpv_state = {:?}\n", status.0)); + } + if let Some(plan) = fpv.1.as_ref() { + audio.push_str(&format!( + "fpv_visible = {}\nfpv_draws = {}\nfpv_vertices = {}\nfpv_transform = {:?}\n", + plan.visible, + plan.draws().len(), + plan.decoded_n(), + plan.world_from_local + )); + } match write_current_state_dump( identity.as_deref(), &name, @@ -195,6 +246,9 @@ pub(super) fn state_dump_body( } None => "[hitvol]\nUnavailable { reason: \"AuthorityWorld resource absent\" }\n".to_owned(), }; + let lifetimes = authority + .map(|world| world.0.entity_lifetime_summary()) + .unwrap_or_else(|| "authority_world_present = 0\noccupied_slots = 0\nevent_holds = 0\nprojectile_payloads = 0\nitem_payloads = 0\nmover_payloads = 0\nscript_objects = 0\nscript_defined_entities = 0\nentity_consistency = Ok(())\n".into()); format!( "format = \"iw4l-state-dump-1\"\n\ captured_unix_ns = {captured_unix_ns}\n\ @@ -203,7 +257,7 @@ pub(super) fn state_dump_body( authority_clock = {authority_clock:#?}\n\ \n[authority_snapshot]\n{authority_snapshot}\n\ \n[presented_snapshot]\n{presented_snapshot}\n\ - \n{hitvol}\n{audio}", + \n[entity_lifetimes]\n{lifetimes}\n{hitvol}\n{audio}", identity.role_label, identity.zone, ) } diff --git a/crates/console/src/feature_dispatch/ui.rs b/crates/console/src/feature_dispatch/ui.rs index e0f2201..7fa2f6e 100644 --- a/crates/console/src/feature_dispatch/ui.rs +++ b/crates/console/src/feature_dispatch/ui.rs @@ -16,7 +16,7 @@ pub(crate) fn route_ui_commands( ), mut ui_draw: ResMut, mut game_settings: ResMut, - menus: Res, + menus: hud::MenuState<'_>, identity: Option>, (authority, authority_clock, presented): ( Option>, diff --git a/crates/console/src/frontend.rs b/crates/console/src/frontend.rs index 4db2cdb..e8de9e4 100644 --- a/crates/console/src/frontend.rs +++ b/crates/console/src/frontend.rs @@ -33,7 +33,7 @@ pub(crate) struct LobbyServices<'w> { browser: Option>, bridge: Option>, action: Option>, - menus: Option>, + menus: hud::MenuState<'w>, } impl LobbyServices<'_> { @@ -99,7 +99,10 @@ fn change_page(page: &mut usize, command: &ConsoleCommand, len: usize) { pub(crate) fn route( mut events: MessageReader, - mut returned: MessageReader, + (mut returned, scope): ( + MessageReader>, + Res, + ), mut commands: Commands, mut dvars: ResMut, mut party: ResMut, @@ -122,13 +125,16 @@ pub(crate) fn route( let mut returned_in_menu = false; let mut match_ended = true; let mut left_session = false; - for fact in returned.read() { - returned_from_world |= fact.had_world; - returned_in_menu |= !fact.had_world; - if fact.had_world { - match_ended &= fact.reason == Some(frame::TeardownReason::MatchEnded); + for fact in returned + .read() + .filter(|event| event.exited.is_some() && event.entered == Some(frame::MatchScope::Absent)) + { + returned_from_world |= fact.exited == Some(frame::MatchScope::Live); + returned_in_menu |= !(fact.exited == Some(frame::MatchScope::Live)); + if fact.exited == Some(frame::MatchScope::Live) { + match_ended &= scope.exit_reason() == Some(frame::TeardownReason::MatchEnded); } - left_session |= fact.reason == Some(frame::TeardownReason::Disconnect); + left_session |= scope.exit_reason() == Some(frame::TeardownReason::Disconnect); } if returned_from_world { commands.remove_resource::(); diff --git a/crates/console/src/gamepad.rs b/crates/console/src/gamepad.rs index de83681..abf53b1 100644 --- a/crates/console/src/gamepad.rs +++ b/crates/console/src/gamepad.rs @@ -181,7 +181,7 @@ const REPEAT_EVERY: f32 = 0.12; pub(crate) fn drive_menus_with_pad( gamepads: Query<&Gamepad>, active: Res, - script_menus: Option>, + script_menus: hud::MenuState<'_>, (devices, console, capture): ( Res, Res, diff --git a/crates/console/src/local_account.rs b/crates/console/src/local_account.rs index 58f90e6..7240f99 100644 --- a/crates/console/src/local_account.rs +++ b/crates/console/src/local_account.rs @@ -66,6 +66,7 @@ pub(crate) fn save( account: Option>, mut persistence: ResMut, mut receipt: Option>, + mut player_data: ResMut, ) { if !matches!( *role, @@ -87,6 +88,7 @@ pub(crate) fn save( let Some(snapshot) = snapshot else { return; }; + let _ = player_data.refresh(Some(&snapshot)); if persistence.saved.as_ref() == Some(&snapshot) { persistence.retry_at = None; persistence.last_error = None; @@ -148,6 +150,97 @@ pub(crate) fn save( } } +pub(crate) fn route_unlock_commands( + mut commands: MessageReader, + mut echo: crate::feature_dispatch::ConsoleEcho, + (screen, world, role): ( + Res, + Res>, + Res, + ), + (account, mut authority): ( + Option>, + Option>, + ), + mut persistence: ResMut, + mut receipt: Option>, + mut player_data: ResMut, +) { + let mut account = account; + for command in commands.read().filter(|command| command.name == "unlock") { + let unlock = match command.args.as_slice() { + [mode] if mode == "all" => true, + [mode] if mode == "reset" => false, + _ => { + echo.write("usage: unlock "); + continue; + } + }; + if *screen != frame::AppScreen::MainMenu + || *world.get() == frame::MatchScope::Live + || !matches!( + *role, + frame::RuntimeRole::Listen | frame::RuntimeRole::Client + ) + { + echo.write("unlock: disconnect to the main menu first"); + continue; + } + let (Some(account), Some(path)) = (account.as_mut(), persistence.path.as_ref()) else { + echo.write("unlock: saved account is unavailable"); + continue; + }; + if !player_data.unlock_data_ready() { + echo.write("unlock: progression content is loading"); + continue; + } + let result = (|| -> Result<_, String> { + let snapshot = player_data + .unlock_snapshot(account.id, account.snapshot.as_ref(), unlock) + .map_err(|error| format!("cannot edit progression: {error:?}"))?; + let store = authority + .as_ref() + .filter(|authority| authority.0.persistent_data().snapshot(account.id).is_some()) + .map(|authority| { + let mut store = authority.0.persistent_data().for_new_match(); + store.replace_unbound(snapshot.clone())?; + Ok::<_, sim::PersistentDataError>(store) + }) + .transpose() + .map_err(|error| format!("cannot replace account: {error:?}"))?; + let next = LocalAccount { + id: account.id, + key: account.key.clone(), + snapshot: Some(snapshot.clone()), + }; + save_current(path, &next, persistence.saved.as_ref()) + .map_err(|error| format!("cannot save account: {error}"))?; + Ok((next, snapshot, store)) + })(); + match result { + Ok((next, snapshot, store)) => { + if let (Some(authority), Some(store)) = (authority.as_mut(), store) { + authority.0.set_persistent_data(store); + } + let _ = player_data.refresh(Some(&snapshot)); + persistence.saved = Some(snapshot.clone()); + persistence.retry_at = None; + persistence.last_error = None; + if let Some(receipt) = receipt.as_mut() { + receipt.0 = Some(snapshot); + } + **account = next; + echo.write(if unlock { + "unlock all: maximum level and native challenges saved; Pro perks unlocked" + } else { + "unlock reset: level 1 and initial challenge progress saved" + }); + } + Err(error) => echo.write(format!("unlock: {error}")), + } + } +} + fn invalid(message: &str) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, message) } diff --git a/crates/console/src/plugin/dispatch_state.rs b/crates/console/src/plugin/dispatch_state.rs index 1c7f80d..5c73b52 100644 --- a/crates/console/src/plugin/dispatch_state.rs +++ b/crates/console/src/plugin/dispatch_state.rs @@ -17,6 +17,9 @@ pub struct ConsoleDispatch { pub wait_remaining: f32, + pub wait_progression: bool, + pub wait_progression_elapsed: f32, + pub wait_world: bool, pub wait_world_elapsed: f32, @@ -55,6 +58,7 @@ impl ConsoleDispatch { self.paused = false; self.wait_remaining = 0.0; self.wait_world = false; + self.wait_progression = false; self.wait_spawn = false; self.wait_spawn_admit = false; self.pending_spawn_class = None; @@ -81,6 +85,7 @@ pub(crate) enum WaitKind { Seconds(f32), Ticks(u32), World, + Progression, Spawn, Torn, Ambient, @@ -88,6 +93,7 @@ pub(crate) enum WaitKind { pub(crate) fn parse_wait_args(args: &[String]) -> WaitKind { match args.first().map(String::as_str) { + Some(s) if s.eq_ignore_ascii_case("progression") => WaitKind::Progression, Some(s) if s.eq_ignore_ascii_case("world") => WaitKind::World, Some(s) if s.eq_ignore_ascii_case("spawn") => WaitKind::Spawn, Some(s) if s.eq_ignore_ascii_case("torn") => WaitKind::Torn, diff --git a/crates/console/src/plugin/mod.rs b/crates/console/src/plugin/mod.rs index f8740f7..4c8adb2 100644 --- a/crates/console/src/plugin/mod.rs +++ b/crates/console/src/plugin/mod.rs @@ -5,15 +5,19 @@ use bevy::{ input::mouse::{MouseButton, MouseMotion, MouseWheel}, input_focus::{FocusCause, InputFocus, InputFocusSystems}, picking::{ - events::{Drag, Pointer, Press, Release}, + events::{PointerDrag, PointerPress, PointerRelease}, pointer::PointerButton, }, prelude::*, - text::{EditableText, FontCx, LayoutCx, TextCursorStyle, TextEdit, TextLayoutInfo}, + text::{ + EditableText, FontCx, LayoutCx, TextCursorStyle, TextEdit, TextLayoutInfo, + TextReadWriteMode, + }, ui::{ComputedUiRenderTargetInfo, UiGlobalTransform}, window::{CursorEntered, CursorGrabMode, CursorOptions, PrimaryWindow, WindowFocused}, }; -use frame::{AppScreen, HasWorld}; +use frame::AppScreen; +use frame::ScopeApp; use input_iw4::{SCRIPT_KEYNUM, command_names, input_cmd, key_event, key_up_command_id}; use net::{ClientActionInput, ClientSet, PresentedSnapshot, frame_time_msec, key_frame_msec}; use render_frontend::prepare::scene::world::WorldScene; @@ -90,7 +94,7 @@ impl Plugin for ConsolePlugin { .init_resource::() .init_resource::() .init_resource::() - .init_resource::() + .published::(frame::MatchScope::Live) .init_resource::() .init_resource::() .init_resource::() @@ -98,6 +102,7 @@ impl Plugin for ConsolePlugin { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .add_message::() .add_message::() .add_systems( @@ -137,7 +142,7 @@ impl Plugin for ConsolePlugin { copy_console_selection_on_release, isolate_gameplay_input, expire_pressed_inputs, - publish_client_action_input, + publish_client_action_input.in_set(frame::InMatch), sync_cursor_grab, ) .chain() @@ -154,7 +159,11 @@ impl Plugin for ConsolePlugin { dispatch_menu_commands, handle_input_commands, apply_console_os_paste, - crate::feature_dispatch::route_replay_commands, + ( + crate::local_account::route_unlock_commands, + crate::feature_dispatch::route_replay_commands, + ) + .chain(), crate::feature_dispatch::route_ui_commands, ( crate::frontend::route, @@ -165,17 +174,18 @@ impl Plugin for ConsolePlugin { crate::feature_dispatch::route_capture_commands, crate::feature_dispatch::route_state_dump_commands, crate::feature_dispatch::route_hitvol_commands, - crate::feature_dispatch::route_debug_feature_commands, + crate::feature_dispatch::route_debug_feature_commands + .in_set(frame::InMatch), crate::feature_dispatch::route_session_commands, crate::feature_dispatch::resume_lifecycle_commands, ( - crate::barracks_menu::sync_profile, - crate::class_dispatch::route_class_commands, + crate::barracks_menu::sync_profile.in_set(frame::InMatch), + crate::class_dispatch::route_class_commands.in_set(frame::InMatch), ) .chain(), - crate::class_dispatch::complete_pending_spawn, - crate::weapon_dispatch::clear_weapon_args_on_torn_down, - crate::weapon_dispatch::refresh_weapon_arg_completions, + crate::class_dispatch::complete_pending_spawn.in_set(frame::InMatch), + crate::weapon_dispatch::refresh_weapon_arg_completions + .in_set(frame::InMatch), crate::weapon_dispatch::route_weapon_commands, ) .chain(), @@ -193,17 +203,17 @@ impl Plugin for ConsolePlugin { crate::debug_draw_method::route_debug_draw_method_commands, crate::debug_view_proj::route_view_proj_commands, ( - crate::debug_dof::route, - crate::debug_distortion::route, - crate::debug_glow::route, - crate::debug_vision::route, + crate::debug_dof::route.in_set(frame::InMatch), + crate::debug_distortion::route.in_set(frame::InMatch), + crate::debug_glow::route.in_set(frame::InMatch), + crate::debug_vision::route.in_set(frame::InMatch), ) .chain(), - crate::debug_fog::route, + crate::debug_fog::route.in_set(frame::InMatch), crate::debug_smc::route_smc_enable_commands, crate::debug_sm::route_sm_commands, crate::debug_lod::route_lod_ramp_commands, - crate::debug_cg_gun::route_cg_gun_commands, + crate::debug_cg_gun::route_cg_gun_commands.in_set(frame::InMatch), crate::debug_cl_yawspeed::route_cl_yawspeed_commands, crate::debug_fx::route_debug_fx_commands, crate::debug_fx_marks::route_fx_mark_commands, @@ -216,7 +226,7 @@ impl Plugin for ConsolePlugin { .chain(), crate::user_settings::sync_binding_view, crate::user_settings::apply_master_volume, - crate::user_settings::sync_player_name, + crate::user_settings::sync_player_name.in_set(frame::InMatch), crate::user_settings::save_user_settings, update_console_ui, ) @@ -321,7 +331,7 @@ fn publish_client_action_input( binds: Res, mut scripted: ResMut, console: Res, - script_menus: Option>, + script_menus: hud::MenuState<'_>, mut hud_input: ResMut, settings: Res, mut out: ResMut, @@ -595,7 +605,7 @@ fn publish_client_action_input( /// every key, and the console, still work. fn sync_cursor_grab( console: Res, - script_menus: Option>, + script_menus: hud::MenuState<'_>, screen: Option>, mut focused: MessageReader, mut entered: MessageReader, @@ -640,7 +650,7 @@ fn setup_console( settings: Res, mut registry: ResMut, mut binds: ResMut, - weapon_completions: Res, + weapon_completions: Res>, ) { binds.apply_defaults(); if registry.resolve("hold").is_none() { @@ -743,6 +753,13 @@ fn setup_console( .arg(crate::StaticCompleter::new(presets)), ); } + if registry.resolve("unlock").is_none() { + registry.register( + crate::CommandSpec::new("unlock") + .usage("unlock — save maximum unlocks or reset progression (main menu)") + .arg(crate::StaticCompleter::new(["all", "reset"])), + ); + } crate::weapon_dispatch::register_weapon_commands(&mut registry, &weapon_completions); crate::debug_move::register_debug_move_commands(&mut registry); crate::saved_position::register_saved_position_commands(&mut registry); @@ -810,6 +827,7 @@ fn setup_console( log_entity = Some( clip.spawn(( ConsoleLogText, + TextReadWriteMode::ReadOnly, Text::new(""), TextFont { font: font.clone().into(), @@ -822,6 +840,7 @@ fn setup_console( selection_color: Color::srgba(0.35, 0.55, 0.85, 0.45), unfocused_selection_color: Color::srgba(0.35, 0.55, 0.85, 0.45), selected_text_color: None, + ..default() }, )) .id(), @@ -872,6 +891,7 @@ fn setup_console( prompt_entity = Some( row.spawn(( ConsoleInputText, + bevy::ui_widgets::TextInput, EditableText { allow_newlines: false, visible_lines: Some(1.0), @@ -889,6 +909,7 @@ fn setup_console( selection_color: Color::srgba(0.35, 0.55, 0.85, 0.45), unfocused_selection_color: Color::srgba(0.35, 0.55, 0.85, 0.2), selected_text_color: None, + ..default() }, Node { flex_grow: 1.0, @@ -1389,7 +1410,7 @@ fn handle_console_input( } fn copy_console_selection_on_release( - mut releases: MessageReader>, + mut releases: MessageReader, prompt: Query<&EditableText, With>, mut state: ResMut, settings: Res, @@ -1455,8 +1476,8 @@ fn copy_console_selection_on_release( } fn handle_scrollback_pointer( - mut presses: MessageReader>, - mut drags: MessageReader>, + mut presses: MessageReader, + mut drags: MessageReader, prompt: Option>, log: Option>, mut state: ResMut, @@ -1497,13 +1518,9 @@ fn handle_scrollback_pointer( if !on_log(press.entity) { continue; } - let Some(local) = pointer_to_text_local( - transform, - node, - target, - ui_scale.0, - press.pointer_location.position, - ) else { + let Some(local) = + pointer_to_text_local(transform, node, target, ui_scale.0, press.pointer.position) + else { continue; }; state.scroll_gesture = true; @@ -1553,13 +1570,9 @@ fn handle_scrollback_pointer( if drag.button != PointerButton::Primary || !on_log(drag.entity) { continue; } - let Some(local) = pointer_to_text_local( - transform, - node, - target, - ui_scale.0, - drag.pointer_location.position, - ) else { + let Some(local) = + pointer_to_text_local(transform, node, target, ui_scale.0, drag.pointer.position) + else { continue; }; let Some((_, _, ch)) = hit_scrollback_cell(&joined, layout, local) else { @@ -1642,7 +1655,7 @@ fn scrollback_cells(text: &str, layout: &TextLayoutInfo) -> Vec, scene: Option<&WorldScene>) -> bool { - !has_world.is_some_and(|h| h.0) && !scene.is_some_and(|s| s.spawned) +fn world_is_torn(has_world: Option<&State>, scene: Option<&WorldScene>) -> bool { + !has_world.is_some_and(|h| *h.get() == frame::MatchScope::Live) + && !scene.is_some_and(|s| s.spawned) } fn promote_interactive_interrupt( @@ -1812,8 +1826,13 @@ fn dispatch_console_command( mut console: ResMut, settings: Res, scene: Option>, - screen: Option>, - has_world: Option>, + (screen, class_phase, class_status, player_data): ( + Option>, + Option>, + Option>, + Option>, + ), + has_world: Option>>, ambient_booted: Option>, presented: Option>, (authority, clock, adopted, client_clock): ( @@ -1827,13 +1846,16 @@ fn dispatch_console_command( ) { let capacity = settings.log_capacity; let world_up = if headless.is_some() { - has_world.as_ref().is_some_and(|h| h.0) + has_world + .as_ref() + .is_some_and(|h| *h.get() == frame::MatchScope::Live) } else { scene.as_ref().is_some_and(|s| s.spawned) }; let waiting = dispatch.paused || dispatch.wait_world + || dispatch.wait_progression || dispatch.wait_spawn || dispatch.wait_spawn_admit || dispatch.wait_torn @@ -1885,6 +1907,29 @@ fn dispatch_console_command( } } } + if dispatch.wait_progression { + if player_data + .as_ref() + .is_some_and(|data| data.unlock_data_ready()) + { + dispatch.wait_progression = false; + diag::info!(Console, "wait progression: native account content ready"); + } else { + dispatch.wait_progression_elapsed += time.delta_secs(); + if dispatch.wait_progression_elapsed >= WAIT_WORLD_TIMEOUT_SECS { + dispatch.wait_progression = false; + abort_script_on_wait_timeout( + "wait progression", + "native account content unavailable", + &mut queue, + &mut console, + capacity, + ); + } else { + return; + } + } + } if dispatch.wait_spawn_admit { dispatch.wait_spawn_elapsed += time.delta_secs(); if dispatch.wait_spawn_elapsed >= WAIT_WORLD_TIMEOUT_SECS { @@ -1901,6 +1946,17 @@ fn dispatch_console_command( return; } } + if dispatch.wait_spawn + && class_phase + .as_ref() + .is_some_and(|phase| !phase.is_pending()) + && let Some(reason) = class_status.as_ref().and_then(|status| status.0.as_ref()) + { + dispatch.wait_spawn = false; + let message = format!("spawn: refused — {reason}"); + diag::warn!(Console, "{message}"); + console.echo(message, capacity); + } if dispatch.wait_spawn { if screen .as_ref() @@ -1966,7 +2022,7 @@ fn dispatch_console_command( dispatch.wait_torn = false; abort_script_on_wait_timeout( "wait torn", - "HasWorld or scene.spawned still set", + "match scope or scene.spawned still set", &mut queue, &mut console, capacity, @@ -2166,6 +2222,12 @@ fn dispatch_console_command( } if command.name == "wait" { match parse_wait_args(&command.args) { + WaitKind::Progression => { + dispatch.wait_progression = !player_data + .as_ref() + .is_some_and(|data| data.unlock_data_ready()); + dispatch.wait_progression_elapsed = 0.0; + } WaitKind::World => { if world_up { diag::info!(Console, "wait world: already spawned"); @@ -2193,7 +2255,10 @@ fn dispatch_console_command( } else { dispatch.wait_torn = true; dispatch.wait_torn_elapsed = 0.0; - diag::info!(Console, "wait torn: until HasWorld=0 and scene.spawned=0"); + diag::info!( + Console, + "wait torn: until match scope absent and scene.spawned=0" + ); } } WaitKind::Ambient => { diff --git a/crates/console/src/user_settings.rs b/crates/console/src/user_settings.rs index dab3692..1f52a4f 100644 --- a/crates/console/src/user_settings.rs +++ b/crates/console/src/user_settings.rs @@ -204,15 +204,15 @@ pub(crate) fn apply_master_volume( pub(crate) fn sync_player_name( settings: Res, generation: Res, - has_world: Res, + has_world: Res>, role: Res, local: Option>, link: Option>, mut inbox: Option>, mut seq: ResMut, - mut sent: Local>, + mut sent: Local>, ) { - if !has_world.0 || *role == frame::RuntimeRole::Replay { + if !(*has_world.get() == frame::MatchScope::Live) || *role == frame::RuntimeRole::Replay { *sent = None; return; } @@ -228,7 +228,7 @@ pub(crate) fn sync_player_name( return; } let name = entity_iw4::pack_client_state_name(&settings.player_name); - let next = (*generation, local.0, name); + let next = (generation.stamp(), local.0, name); if sent.as_ref() == Some(&next) { return; } diff --git a/crates/console/src/weapon_dispatch.rs b/crates/console/src/weapon_dispatch.rs index 5af356d..19ae690 100644 --- a/crates/console/src/weapon_dispatch.rs +++ b/crates/console/src/weapon_dispatch.rs @@ -1,9 +1,8 @@ -use std::sync::{Arc, RwLock}; +use frame::Published; use asset_game::{LoadoutRules, WeaponSelection}; use assets::PreparedWeapons; use bevy::prelude::*; -use frame::MatchTornDown; use net::{ClientActionInbox, LocalPresentClient, PresentedSnapshot}; use sim::{ClientAction, ClientLifecycle}; @@ -12,65 +11,32 @@ use crate::{ StaticCompleter, }; -#[derive(Resource, Clone)] +#[derive(Clone, Default)] pub struct WeaponArgCompletions { - pub give: Arc>>, - pub attach: Arc>>, - pub camos: Arc>>, - pub killstreaks: Arc>>, - pub bots: Arc>>, + pub give: Vec, + pub attach: Vec, + pub camos: Vec, + pub killstreaks: Vec, + pub bots: Vec, } -impl Default for WeaponArgCompletions { - fn default() -> Self { - Self { - give: Arc::new(RwLock::new(Vec::new())), - attach: Arc::new(RwLock::new(Vec::new())), - camos: Arc::new(RwLock::new(Vec::new())), - killstreaks: Arc::new(RwLock::new(Vec::new())), - bots: Arc::new(RwLock::new(Vec::new())), - } - } -} +type CompletionSlot = Published; -pub(crate) fn clear_weapon_args_on_torn_down( - mut torn: MessageReader, - completions: Res, -) { - if torn.read().len() == 0 { - return; - } - if let Ok(mut give) = completions.give.write() { - give.clear(); - } - if let Ok(mut attach) = completions.attach.write() { - attach.clear(); - } - if let Ok(mut values) = completions.camos.write() { - values.clear(); - } - if let Ok(mut values) = completions.killstreaks.write() { - values.clear(); - } - if let Ok(mut values) = completions.bots.write() { - values.clear(); - } -} - -struct LiveListCompleter(Arc>>); +struct LiveListCompleter(CompletionSlot, fn(&WeaponArgCompletions) -> &[String]); impl ArgCompleter for LiveListCompleter { fn complete(&self, prefix: &str) -> Vec { - let Ok(values) = self.0.read() else { - return Vec::new(); - }; - StaticCompleter::new(values.iter().cloned()).complete(prefix) + self.0.read(|value| { + value.map_or_else(Vec::new, |(_, value)| { + StaticCompleter::new((self.1)(value).iter().cloned()).complete(prefix) + }) + }) } } pub(crate) fn register_weapon_commands( registry: &mut ConsoleRegistry, - completions: &WeaponArgCompletions, + completions: &CompletionSlot, ) { if registry.resolve("give").is_none() { registry.register( @@ -91,75 +57,73 @@ pub(crate) fn register_weapon_commands( registry.register( crate::CommandSpec::new("attach") .usage("attach [name] — show the current set and choices, or toggle a named attachment") - .arg(LiveListCompleter(Arc::clone(&completions.attach))), + .arg(LiveListCompleter(completions.clone(), |value| &value.attach)), ); } } pub(crate) fn refresh_weapon_arg_completions( - weapons: Option>, + weapons: Res, killstreaks: Option>, presented: Res, local: Res, - completions: Res, - mut last_held: Local>, + completions: Res, + generation: Res, + mut previous: Local)>>, ) { - if let Ok(mut values) = completions.killstreaks.write() { - *values = killstreaks.as_ref().map_or_else(Vec::new, |v| v.0.clone()); - } - if let Ok(mut values) = completions.bots.write() { - *values = presented.snapshot().map_or_else(Vec::new, |snapshot| { - snapshot - .meta - .clients - .iter() - .filter(|(_, meta)| { - ["bot", "dummy"] - .iter() - .any(|name| meta.name == entity_iw4::pack_client_state_name(name)) - }) - .map(|(id, _)| id.0.to_string()) - .collect() - }); - } - let Some(weapons) = weapons.as_ref() else { - return; - }; let held = presented.held_weapon_id(local.0).unwrap_or(0); - let changed = weapons.is_changed(); - if !should_refresh_weapon_args(changed, *last_held, held) { + let bots = presented.snapshot().map_or_else(Vec::new, |snapshot| { + snapshot + .meta + .clients + .iter() + .filter(|(_, meta)| { + ["bot", "dummy"] + .iter() + .any(|name| meta.name == entity_iw4::pack_client_state_name(name)) + }) + .map(|(id, _)| id.0.to_string()) + .collect() + }); + let epoch = generation.0.expect("live match generation"); + if previous + .as_ref() + .is_some_and(|(old_epoch, old_held, old_bots)| { + *old_epoch == epoch && *old_held == held && *old_bots == bots + }) + && !weapons.is_changed() + && killstreaks.as_ref().is_none_or(|value| !value.is_changed()) + { return; } - *last_held = Some(held); - if let Ok(mut give) = completions.give.write() { - *give = weapon_completions(weapons); - } - if let Ok(mut camos) = completions.camos.write() { - *camos = if held == 0 { - Vec::new() - } else { - std::iter::once("none".to_owned()) - .chain( - weapons - .registry() - .camouflage_choices(held) - .into_iter() - .map(|(_, name)| name.to_owned()), - ) - .collect() - }; - } - if let Ok(mut attach) = completions.attach.write() { - *attach = if held == 0 { - Vec::new() - } else { - attach_completions(weapons, held) - }; - } -} - -fn should_refresh_weapon_args(catalog_changed: bool, last_held: Option, held: u32) -> bool { - catalog_changed || last_held != Some(held) + *previous = Some((epoch, held, bots.clone())); + let camos = if held == 0 { + Vec::new() + } else { + std::iter::once("none".to_owned()) + .chain( + weapons + .registry() + .camouflage_choices(held) + .into_iter() + .map(|(_, name)| name.to_owned()), + ) + .collect() + }; + completions.publish( + generation.0.expect("live match generation"), + WeaponArgCompletions { + give: weapon_completions(&weapons), + attach: if held == 0 { + Vec::new() + } else { + attach_completions(&weapons, held) + }, + camos, + killstreaks: killstreaks.map_or_else(Vec::new, |value| value.0.clone()), + bots, + }, + ); } pub(crate) fn route_weapon_commands( @@ -172,7 +136,7 @@ pub(crate) fn route_weapon_commands( local: Res, mut inbox: ResMut, mut seq: ResMut, - completions: Res, + completions: Res, authority: Option>, ) { let capacity = settings.log_capacity; @@ -477,7 +441,7 @@ fn change_camo( fn grant_killstreak( name: &str, - completions: &WeaponArgCompletions, + completions: &CompletionSlot, presented: &PresentedSnapshot, local: &LocalPresentClient, inbox: &mut ClientActionInbox, @@ -485,10 +449,7 @@ fn grant_killstreak( mut echo: impl FnMut(String), ) { let names = completions - .killstreaks - .read() - .map(|v| v.clone()) - .unwrap_or_else(|_| Vec::new()); + .read(|value| value.map_or_else(Vec::new, |(_, value)| value.killstreaks.clone())); let requested = name.to_ascii_lowercase().replace(['-', ' '], "_"); let requested = KILLSTREAK_ALIASES .iter() @@ -551,13 +512,13 @@ const KILLSTREAK_ALIASES: &[(&str, &str)] = &[ ("harrier", "harrier_airstrike"), ]; -struct KillstreakCompleter(Arc>>); +struct KillstreakCompleter(CompletionSlot); impl ArgCompleter for KillstreakCompleter { fn complete(&self, prefix: &str) -> Vec { - let Ok(names) = self.0.read() else { - return Vec::new(); - }; + let names = self + .0 + .read(|value| value.map_or_else(Vec::new, |(_, value)| value.killstreaks.clone())); let aliases = KILLSTREAK_ALIASES .iter() .filter(|(_, name)| names.iter().any(|value| value == name)) @@ -566,16 +527,18 @@ impl ArgCompleter for KillstreakCompleter { } } -struct GiveCompleter(WeaponArgCompletions); +struct GiveCompleter(CompletionSlot); impl ArgCompleter for GiveCompleter { fn complete(&self, prefix: &str) -> Vec { let mut items = vec!["ammo".to_owned(), "camo".to_owned()]; - if let Ok(weapons) = self.0.give.read() { - items.extend(weapons.iter().map(|name| format!("weapon/{name}"))); - } + self.0.read(|value| { + if let Some((_, value)) = value { + items.extend(value.give.iter().map(|name| format!("weapon/{name}"))); + } + }); items.extend( - KillstreakCompleter(Arc::clone(&self.0.killstreaks)) + KillstreakCompleter(self.0.clone()) .complete("") .into_iter() .map(|name| format!("killstreak/{name}")), @@ -586,13 +549,13 @@ impl ArgCompleter for GiveCompleter { fn complete_with_context(&self, prefix: &str, args: &[&str]) -> Vec { match args { [] => self.complete(prefix), - ["camo"] => LiveListCompleter(Arc::clone(&self.0.camos)).complete(prefix), + ["camo"] => LiveListCompleter(self.0.clone(), |value| &value.camos).complete(prefix), _ => Vec::new(), } } } -struct BotCompleter(WeaponArgCompletions); +struct BotCompleter(CompletionSlot); impl ArgCompleter for BotCompleter { fn complete(&self, prefix: &str) -> Vec { @@ -610,10 +573,7 @@ impl ArgCompleter for BotCompleter { ("give" | "fire" | "tp", 1) => { let mut ids = self .0 - .bots - .read() - .map(|v| v.clone()) - .unwrap_or_else(|_| Vec::new()); + .read(|value| value.map_or_else(Vec::new, |(_, value)| value.bots.clone())); if *verb != "give" { ids.insert(0, "all".into()); } @@ -622,10 +582,7 @@ impl ArgCompleter for BotCompleter { ("give", 2) => { let names = self .0 - .give - .read() - .map(|v| v.clone()) - .unwrap_or_else(|_| Vec::new()); + .read(|value| value.map_or_else(Vec::new, |(_, value)| value.give.clone())); StaticCompleter::new(names).complete(prefix) } ("tp", 2) => StaticCompleter::new(["above"]).complete(prefix), diff --git a/crates/d3d9_sm3/src/abi.rs b/crates/d3d9_sm3/src/abi.rs index f203433..6f68f68 100644 --- a/crates/d3d9_sm3/src/abi.rs +++ b/crates/d3d9_sm3/src/abi.rs @@ -60,4 +60,6 @@ pub struct PassLoweringAbi { pub samplers: Vec, pub alpha_tests: Vec, + + pub depth_to_colour: bool, } diff --git a/crates/d3d9_sm3/src/wgsl.rs b/crates/d3d9_sm3/src/wgsl.rs index 1a50997..500cd27 100644 --- a/crates/d3d9_sm3/src/wgsl.rs +++ b/crates/d3d9_sm3/src/wgsl.rs @@ -557,7 +557,7 @@ pub fn lower_pass_to_wgsl( return Err(Sm3WgslError::MissingColourOutput); } - let mut source = String::new(); + let mut source = String::from("enable wgpu_binding_array;\n"); source.push_str("struct Sm3ConstantArena { c: array> }\n"); source.push_str("@group(0) @binding(0) var sm3_constants: Sm3ConstantArena;\n"); if vertex_plan.uses_relative_float { @@ -772,6 +772,11 @@ pub fn lower_pass_to_wgsl( if let Some(alpha_test) = alpha_test { emit_alpha_test(&mut source, alpha_test, colour_output)?; } + if abi.depth_to_colour { + source.push_str(" return vec4(varyings.position.z);\n"); + source.push_str("}\n"); + continue; + } writeln!(source, " return {};", register_name(colour_output)).unwrap(); source.push_str("}\n"); } diff --git a/crates/dpvs_iw4/src/walk.rs b/crates/dpvs_iw4/src/walk.rs index 5592f54..10827e8 100644 --- a/crates/dpvs_iw4/src/walk.rs +++ b/crates/dpvs_iw4/src/walk.rs @@ -302,7 +302,6 @@ pub(crate) fn finalize_queued_hull_no_frustum(item: &mut Queued, view_dir: [f32; true } -/// Called for every portal visit of a cell, with the planes it was seen through. pub type CellVisit<'a> = dyn FnMut(usize, &CellClipPlanes) + 'a; pub fn visit_cells( @@ -388,10 +387,6 @@ pub fn visit_cells( } } } - // A cell seen through several portals is visited once per portal, each visit with its - // own clip planes; `cell_clips` keeps only the first. Entities in the cell must be culled - // against every visit (IW4 queues one cell command per visit), or what shows only - // through a later portal vanishes. if let Some(visit) = on_visit.as_mut() { let mut planes = CellClipPlanes::EMPTY; record_cell_clip(&item, clip_planes, &mut planes); diff --git a/crates/dxbc_sm5/src/wgsl.rs b/crates/dxbc_sm5/src/wgsl.rs index 97fa45e..ede9f13 100644 --- a/crates/dxbc_sm5/src/wgsl.rs +++ b/crates/dxbc_sm5/src/wgsl.rs @@ -998,7 +998,7 @@ pub fn lower_pass(abi: &PassAbi, vertex: &Shader, pixel: &Shader) -> Result> }\n"); out.push_str("@group(0) @binding(0) var sm3_constants: Sm3ConstantArena;\n"); diff --git a/crates/entity_iw4/Cargo.toml b/crates/entity_iw4/Cargo.toml index a2b058f..a8c27aa 100644 --- a/crates/entity_iw4/Cargo.toml +++ b/crates/entity_iw4/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true description = "Retail IW4 entity taxonomy: entityState/centity, eType, eFlags, event enums." [dependencies] +asset_core.workspace = true math_iw4.workspace = true libm = { version = "0.2.16", default-features = false } diff --git a/crates/entity_iw4/src/glass.rs b/crates/entity_iw4/src/glass.rs index 64cf763..9652059 100644 --- a/crates/entity_iw4/src/glass.rs +++ b/crates/entity_iw4/src/glass.rs @@ -169,12 +169,7 @@ pub enum GlassApplyAction { Delete, } -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct GlassPaneBasis { - pub origin: [f32; 3], - pub axis_s: [f32; 3], - pub axis_t: [f32; 3], -} +pub use asset_core::GlassPaneBasis; #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(C)] diff --git a/crates/frame/Cargo.toml b/crates/frame/Cargo.toml index d3a3d4b..6e729e5 100644 --- a/crates/frame/Cargo.toml +++ b/crates/frame/Cargo.toml @@ -3,7 +3,7 @@ name = "frame" version = "0.1.0" edition.workspace = true license.workspace = true -description = "Shared frame TOC labels and session identity. No plugins, no systems." +description = "Shared frame schedules, session identity and state-scoped ownership." [dependencies] bevy.workspace = true diff --git a/crates/frame/src/lib.rs b/crates/frame/src/lib.rs index 170feb3..ab4da3c 100644 --- a/crates/frame/src/lib.rs +++ b/crates/frame/src/lib.rs @@ -1,3 +1,8 @@ +pub mod scope; +pub use scope::{ + InMatch, InRound, MatchScope, Published, RoundPhase, ScopeApp, ScopeEpoch, ScopePlugin, + ScopeRegistry, ScopeSet, Staging, +}; mod screen_effects; pub use screen_effects::{ScreenEffectsDvars, ScreenEffectsPublished, ScreenEffectsView}; pub mod class_presets; @@ -24,12 +29,12 @@ pub use schedule::{ pub use script_entity_notify::{AbortKillcam, BeginKillcam, KillcamEnded, SpawnedPlayer}; pub use script_notify::{ExitLevelCalled, register_script_notify}; pub use session::{ - AdmissionKey, AppScreen, BotNavigationReady, CacWeaponOffer, ClassSelectHandoff, HasWorld, - Headless, HostClassLoadouts, HostClassSlot, HudInputView, LaunchIdentity, LaunchReport, - LifeEndCause, LifeEnded, LifeStartReason, LifeStarted, LocalLoadKey, LocalSpawnArmed, - MapLoadApproved, MapLoadFailed, MatchInstalled, MatchKey, MatchTornDown, ReadinessState, - ReturnedToMenu, RuntimeRole, TeardownReason, UiCamera, UiDraw, ViewSubject, WorldGeneration, - WorldProducts, WorldReadiness, + AdmissionKey, AppScreen, BotNavigationReady, CacWeaponOffer, ClassSelectHandoff, + DisplayEncodedCamera, Headless, HostClassLoadouts, HostClassSlot, HudInputView, InstalledMatch, + LaunchIdentity, LaunchReport, LifeEndCause, LifeEnded, LifeStartReason, LifeStarted, + LocalLoadKey, LocalSpawnArmed, MapLoadApproved, MapLoadFailed, MatchKey, ReadinessState, + RuntimeRole, TeardownReason, UiCamera, UiDraw, ViewSubject, WorldGeneration, WorldProducts, + WorldReadiness, WorldStamp, }; pub use settings::{DisplayResolution, GameSettings, OtherGame}; pub use ui::{ diff --git a/crates/frame/src/scope.rs b/crates/frame/src/scope.rs new file mode 100644 index 0000000..f119b2b --- /dev/null +++ b/crates/frame/src/scope.rs @@ -0,0 +1,604 @@ +use bevy::ecs::message::MessageCursor; +use bevy::ecs::schedule::ScheduleLabel; +use bevy::prelude::*; +use bevy::state::{app::StatesPlugin, state::StateTransitionSystems}; +use std::any::{TypeId, type_name}; +use std::marker::PhantomData; +use std::sync::{Arc, RwLock}; + +#[derive(States, Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] +pub enum MatchScope { + #[default] + Absent, + Loading, + Live, +} + +#[derive(SubStates, Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] +#[source(MatchScope = MatchScope::Live)] +pub enum RoundPhase { + #[default] + Between, + Playing, +} + +#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ScopeCommit; + +#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ScopeRetired; + +#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct InMatch; + +#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct InRound; + +#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ScopeSet { + Install, + Derive, + Announce, + Release, +} + +#[derive(Resource)] +pub struct ScopeEpoch(pub u64, PhantomData); + +impl Default for ScopeEpoch { + fn default() -> Self { + Self(1, PhantomData) + } +} + +#[derive(Resource)] +pub struct Staging(Option); + +impl Default for Staging { + fn default() -> Self { + Self(None) + } +} + +impl Staging { + pub fn put(&mut self, value: R) { + assert!(self.0.is_none(), "{} is already staged", type_name::()); + self.0 = Some(value); + } + + pub fn is_empty(&self) -> bool { + self.0.is_none() + } +} + +#[derive(Resource)] +pub struct Published(Arc>>); + +impl Clone for Published { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Default for Published { + fn default() -> Self { + Self(Arc::new(RwLock::new(None))) + } +} + +impl Published { + pub fn read(&self, read: impl FnOnce(Option<(u64, &T)>) -> R) -> R { + let guard = self.0.read().expect("published slot poisoned"); + read(guard.as_ref().map(|(epoch, value)| (*epoch, value))) + } + + pub fn publish(&self, epoch: u64, value: T) { + *self.0.write().expect("published slot poisoned") = Some((epoch, value)); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScopeKind { + Resource, + Staged, + Published, + Message, +} + +struct Row { + id: TypeId, + state: TypeId, + label: String, + name: &'static str, + kind: ScopeKind, + optional: bool, + live: bool, + install: fn(&mut World), + retire: fn(&mut World), + present: fn(&World) -> bool, + staging_empty: fn(&World) -> bool, + active: fn(&World, &str) -> bool, + discard: fn(&mut World), +} + +#[derive(Resource, Default)] +pub struct ScopeRegistry { + rows: Vec, + pub retirement_serial: u64, + pub retired_resources: usize, +} + +impl ScopeRegistry { + pub fn lifetimes(&self) -> impl Iterator { + self.rows + .iter() + .map(|row| (row.name, row.label.as_str(), row.kind, row.live)) + } + + pub fn rows(&self, world: &World) -> impl Iterator { + self.rows + .iter() + .map(move |row| (row.name, row.label.as_str(), row.kind, (row.present)(world))) + } +} + +pub struct ScopePlugin; + +impl Plugin for ScopePlugin { + fn build(&self, app: &mut App) { + if !app.is_plugin_added::() { + app.add_plugins(StatesPlugin); + } + app.init_state::() + .add_sub_state::() + .init_resource::() + .init_resource::() + .init_resource::>() + .init_resource::>() + .add_systems( + StateTransition, + (retire_state::, retire_state::) + .chain() + .in_set(ScopeRetired) + .in_set(StateTransitionSystems::TransitionSchedules), + ); + for schedule in [ + First.intern(), + PreUpdate.intern(), + RunFixedMainLoop.intern(), + FixedFirst.intern(), + FixedPreUpdate.intern(), + FixedUpdate.intern(), + FixedPostUpdate.intern(), + FixedLast.intern(), + Update.intern(), + PostUpdate.intern(), + Last.intern(), + ] { + app.configure_sets(schedule, InMatch.run_if(in_state(MatchScope::Live))) + .configure_sets(schedule, InRound.run_if(in_state(RoundPhase::Playing))); + } + app.configure_sets( + PostUpdate, + ( + crate::RenderSet::Anim, + crate::RenderSet::FrontendAssemble, + crate::RenderSet::FrontendPrepare, + crate::RenderSet::Fx, + ) + .run_if(in_state(MatchScope::Live)), + ); + app.add_systems( + Update, + capture_census + .after(crate::ClientSet::Effects) + .before(crate::ClientEdge(9)), + ); + configure_scope(app, MatchScope::Loading); + configure_scope(app, MatchScope::Live); + configure_scope(app, RoundPhase::Playing); + if cfg!(debug_assertions) { + app.add_systems(Last, verify_registry.after(ScopeCommit)); + } + } +} + +fn configure_scope(app: &mut App, scope: S) { + app.configure_sets( + OnEnter(scope.clone()), + (ScopeSet::Install, ScopeSet::Derive, ScopeSet::Announce).chain(), + ) + .add_systems( + OnEnter(scope.clone()), + (move |world: &mut World| install_scope::(world, &scope)).in_set(ScopeSet::Install), + ); +} + +fn install_scope(world: &mut World, scope: &S) { + let label = format!("{scope:?}"); + let actions: Vec<_> = world + .resource::() + .rows + .iter() + .filter(|row| row.state == TypeId::of::() && row.label == label) + .map(|row| row.install) + .collect(); + for install in actions { + install(world); + } +} + +fn retire_state( + world: &mut World, + mut cursor: Local>>, +) { + let exited: Vec<_> = cursor + .read(world.resource::>>()) + .filter_map(|event| event.exited.clone()) + .collect(); + for state in exited { + let label = format!("{state:?}"); + let actions: Vec<_> = world + .resource::() + .rows + .iter() + .rev() + .filter(|row| row.state == TypeId::of::() && row.label == label) + .map(|row| (row.retire, row.present, row.kind, row.name)) + .collect(); + for (retire, present, kind, name) in &actions { + retire(world); + if matches!(kind, ScopeKind::Resource | ScopeKind::Staged) { + assert!(!present(world), "{name} survived scope exit"); + } + } + world.resource_mut::>().0 = + world.resource::>().0.wrapping_add(1); + if !actions.is_empty() { + world.resource_mut::().mark_teardown(); + let mut registry = world.resource_mut::(); + registry.retirement_serial = registry.retirement_serial.wrapping_add(1); + registry.retired_resources = actions.len(); + } + let mut entities = world.query_filtered::<&DespawnOnExit, bevy::ecs::query::Allow>(); + assert!( + !entities.iter(world).any(|owner| owner.0 == state), + "scope entity survived exit of {label}" + ); + } +} + +fn verify_registry(world: &World) { + for row in &world.resource::().rows { + if matches!(row.kind, ScopeKind::Resource | ScopeKind::Staged) { + assert!( + (row.active)(world, &row.label) && (row.optional || (row.present)(world)) + || !(row.active)(world, &row.label) && !(row.present)(world), + "{} outside {}", + row.name, + row.label + ); + } + let install_pending = matches!( + world.resource::>(), + NextState::Pending(MatchScope::Live) + ); + if !install_pending { + assert!( + (row.staging_empty)(world), + "{} staging not consumed", + row.name + ); + } + } +} + +pub fn discard_staging(world: &mut World) { + let discard: Vec<_> = world + .resource::() + .rows + .iter() + .map(|row| row.discard) + .collect(); + for retire in discard { + retire(world); + } +} + +pub trait ScopeApp { + fn init_app(&mut self) -> &mut Self; + fn scoped(&mut self, scope: impl States) -> &mut Self; + fn staged(&mut self, scope: impl States) -> &mut Self; + fn published(&mut self, scope: impl States) -> &mut Self; + fn scoped_message(&mut self, scope: impl States) -> &mut Self; +} + +fn present(world: &World) -> bool { + world.contains_resource::() +} +fn active(world: &World, label: &str) -> bool { + world + .get_resource::>() + .is_some_and(|state| format!("{:?}", state.get()) == label) +} +fn install(world: &mut World) { + assert!( + !world.contains_resource::(), + "{} already installed", + type_name::() + ); + let value = world + .get_resource_mut::>() + .and_then(|mut staging| staging.0.take()) + .unwrap_or_else(|| R::from_world(world)); + world.insert_resource(value); +} +fn install_staged(world: &mut World) { + let value = world + .resource_mut::>() + .0 + .take() + .unwrap_or_else(|| panic!("{} was not staged", type_name::())); + world.insert_resource(value); +} +fn retire(world: &mut World) { + if let Some(value) = world.remove_resource::() { + world.resource_mut::().hand_over(value); + } + if let Some(mut staged) = world.get_resource_mut::>() + && let Some(value) = staged.0.take() + { + world.resource_mut::().hand_over(value); + } +} +fn retire_staging(world: &mut World) { + if let Some(mut staging) = world.get_resource_mut::>() + && let Some(value) = staging.0.take() + { + world.resource_mut::().hand_over(value); + } +} +fn staging_empty(world: &World) -> bool { + world + .get_resource::>() + .is_none_or(Staging::is_empty) +} +fn published_present(world: &World) -> bool { + world + .get_resource::>() + .is_some_and(|slot| slot.read(|value| value.is_some())) +} +fn clear_published(world: &mut World) { + let value = world + .resource::>() + .0 + .write() + .expect("published slot poisoned") + .take(); + if let Some(value) = value { + world.resource_mut::().hand_over(value); + } +} +fn messages_present(world: &World) -> bool { + world + .get_resource::>() + .is_some_and(|messages| !messages.is_empty()) +} +fn clear_messages(world: &mut World) { + world.resource_mut::>().clear(); +} +fn noop(_: &mut World) {} +fn empty(_: &World) -> bool { + true +} + +fn register( + app: &mut App, + scope: S, + kind: ScopeKind, + install: fn(&mut World), + retire: fn(&mut World), + present: fn(&World) -> bool, + staging_empty: fn(&World) -> bool, + discard: fn(&mut World), +) { + let label = format!("{scope:?}"); + assert!( + (TypeId::of::() == TypeId::of::() + && matches!(label.as_str(), "Loading" | "Live")) + || (TypeId::of::() == TypeId::of::() && label == "Playing"), + "unsupported resource scope {label}" + ); + if !app.is_plugin_added::() { + app.add_plugins(ScopePlugin); + } + let label = format!("{scope:?}"); + let mut registry = app.world_mut().resource_mut::(); + if let Some(row) = registry.rows.iter().find(|row| row.id == TypeId::of::()) { + assert!( + row.state == TypeId::of::() && row.label == label && row.kind == kind, + "{} has conflicting scope owners", + type_name::() + ); + return; + } + let row = Row { + id: TypeId::of::(), + state: TypeId::of::(), + label, + name: type_name::(), + kind, + optional: false, + live: false, + install, + retire, + present, + staging_empty, + active: active::, + discard, + }; + if TypeId::of::() == TypeId::of::() { + registry.rows.insert(0, row); + } else { + registry.rows.push(row); + } +} + +impl ScopeApp for App { + fn init_app(&mut self) -> &mut Self { + self.init_resource::() + } + fn scoped(&mut self, scope: impl States) -> &mut Self { + register::( + self, + scope, + ScopeKind::Resource, + install::, + retire::, + present::, + staging_empty::, + retire_staging::, + ); + self + } + fn staged(&mut self, scope: impl States) -> &mut Self { + register::( + self, + scope, + ScopeKind::Staged, + install_staged::, + retire::, + present::, + staging_empty::, + retire_staging::, + ); + self.init_resource::>() + } + fn published(&mut self, scope: impl States) -> &mut Self { + register::, _>( + self, + scope, + ScopeKind::Published, + noop, + clear_published::, + published_present::, + empty, + noop, + ); + self.init_resource::>() + } + fn scoped_message(&mut self, scope: impl States) -> &mut Self { + register::, _>( + self, + scope, + ScopeKind::Message, + noop, + clear_messages::, + messages_present::, + empty, + noop, + ); + self.add_message::() + } +} + +pub fn stage(world: &mut World, value: R) { + let registered = world + .resource::() + .rows + .iter() + .find(|row| row.id == TypeId::of::()) + .map(|row| { + assert!( + row.state == TypeId::of::() && row.label == "Live", + "{} staged for a conflicting scope", + type_name::() + ); + }) + .is_some(); + if !registered { + world.resource_mut::().rows.push(Row { + id: TypeId::of::(), + state: TypeId::of::(), + label: format!("{:?}", MatchScope::Live), + name: type_name::(), + kind: ScopeKind::Staged, + optional: true, + live: false, + install: install_optional_staged::, + retire: retire::, + present: present::, + staging_empty: staging_empty::, + active: active::, + discard: retire_staging::, + }); + } + world.init_resource::>(); + world.resource_mut::>().put(value); +} + +fn install_optional_staged(world: &mut World) { + if let Some(value) = world.resource_mut::>().0.take() { + world.insert_resource(value); + } +} + +pub fn insert(world: &mut World, value: R, scope: MatchScope) { + assert_eq!( + *world.resource::>().get(), + scope, + "{} inserted outside its scope", + type_name::() + ); + let registered = world + .resource::() + .rows + .iter() + .find(|row| row.id == TypeId::of::()) + .map(|row| { + assert!( + row.state == TypeId::of::() && row.label == format!("{scope:?}"), + "{} inserted for a conflicting scope", + type_name::() + ); + }) + .is_some(); + if !registered { + world.resource_mut::().rows.push(Row { + id: TypeId::of::(), + state: TypeId::of::(), + label: format!("{scope:?}"), + name: type_name::(), + kind: ScopeKind::Resource, + optional: true, + live: false, + install: noop, + retire: retire::, + present: present::, + staging_empty: staging_empty::, + active: active::, + discard: retire_staging::, + }); + } + if let Some(previous) = world.remove_resource::() { + world.resource_mut::().hand_over(previous); + } + world.insert_resource(value); +} + +fn capture_census(world: &mut World) { + let live: Vec<_> = world + .resource::() + .rows + .iter() + .map(|row| (row.present)(world)) + .collect(); + for (row, live) in world + .resource_mut::() + .rows + .iter_mut() + .zip(live) + { + row.live = live; + } +} diff --git a/crates/frame/src/session.rs b/crates/frame/src/session.rs index 241423b..960956c 100644 --- a/crates/frame/src/session.rs +++ b/crates/frame/src/session.rs @@ -80,9 +80,6 @@ impl Default for LaunchReport { } } -#[derive(Resource, Clone, Copy, Default, Debug, PartialEq, Eq)] -pub struct HasWorld(pub bool); - #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct CacWeaponOffer { pub key: String, @@ -122,10 +119,44 @@ pub struct ClassSelectHandoff { pub lock_reasons: Vec>, } -#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq, Default)] -pub struct WorldGeneration(pub Option); +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub struct WorldStamp(pub Option); +#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)] +pub struct WorldGeneration { + id: std::num::NonZeroU64, + stamp: WorldStamp, +} impl WorldGeneration { + pub fn from_install(request_id: u64) -> Self { + Self { + id: std::num::NonZeroU64::new( + request_id + .checked_add(1) + .expect("match generation exhausted"), + ) + .unwrap(), + stamp: WorldStamp(Some(request_id)), + } + } + pub fn id(self) -> std::num::NonZeroU64 { + self.id + } + pub fn stamp(self) -> WorldStamp { + self.stamp + } +} +impl std::ops::Deref for WorldGeneration { + type Target = WorldStamp; + fn deref(&self) -> &WorldStamp { + &self.stamp + } +} + +impl WorldStamp { + pub fn stamp(self) -> Self { + self + } pub fn from_install(request_id: u64) -> Self { Self(Some(request_id)) } @@ -201,8 +232,8 @@ pub struct AdmissionKey { pub bootstrap_id: u32, } -#[derive(Message, Clone, Debug, PartialEq, Eq)] -pub struct MatchInstalled { +#[derive(Resource, Clone, Debug, PartialEq, Eq)] +pub struct InstalledMatch { pub request_id: u64, pub load_key: LocalLoadKey, @@ -212,26 +243,6 @@ pub struct MatchInstalled { pub spawn_count: usize, } -#[derive(Message, Clone, Copy, Debug, PartialEq, Eq)] -pub struct MatchTornDown { - pub reason: TeardownReason, - - pub world_generation: WorldGeneration, - - pub match_key: MatchKey, - - pub match_epoch: u32, -} - -#[derive(Message, Clone, Copy, Debug, PartialEq, Eq)] -pub struct ReturnedToMenu { - pub swap_id: u64, - - pub had_world: bool, - - pub reason: Option, -} - #[derive(Message, Clone, Debug, PartialEq, Eq)] pub struct MapLoadApproved { pub request_id: u64, @@ -359,10 +370,10 @@ pub struct HudInputView { } #[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct LocalSpawnArmed(pub WorldGeneration); +pub struct LocalSpawnArmed(pub WorldStamp); impl LocalSpawnArmed { - pub fn armed_for(self, generation: WorldGeneration) -> bool { + pub fn armed_for(self, generation: WorldStamp) -> bool { generation.0.is_some() && self.0 == generation } } @@ -378,22 +389,22 @@ pub enum ReadinessState { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct WorldReadiness { - pub generation: WorldGeneration, + pub generation: WorldStamp, pub state: ReadinessState, } impl WorldReadiness { - pub fn new(generation: WorldGeneration, state: ReadinessState) -> Self { + pub fn new(generation: WorldStamp, state: ReadinessState) -> Self { Self { generation, state } } - pub fn ready_for(self, generation: WorldGeneration) -> bool { + pub fn ready_for(self, generation: WorldStamp) -> bool { generation.0.is_some() && self.generation == generation && matches!(self.state, ReadinessState::Ready | ReadinessState::Silent) } - pub fn failed_for(self, generation: WorldGeneration) -> bool { + pub fn failed_for(self, generation: WorldStamp) -> bool { generation.0.is_some() && self.generation == generation && self.state == ReadinessState::Failed @@ -406,3 +417,7 @@ pub struct BotNavigationReady(pub WorldReadiness); #[derive(Component)] pub struct UiCamera; + +#[derive(Component, Clone, bevy::render::extract_component::ExtractComponent)] +#[extract_app(bevy::render::RenderApp)] +pub struct DisplayEncodedCamera; diff --git a/crates/fx/src/draw.rs b/crates/fx/src/draw.rs index 69f91c2..e8dde36 100644 --- a/crates/fx/src/draw.rs +++ b/crates/fx/src/draw.rs @@ -381,13 +381,7 @@ fn draw_one_elem( let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); let elem_random_seed = elem.random_seed; - let origin = crate::spark::spark_elem_world_origin( - elem.origin, - elem.flags, - &effect.frame_now(), - &effect.frame_when_played(), - Some(elem.orient_spawn_params(elem_random_seed)), - ); + let origin = elem.world_origin(&effect.frame_now(), &effect.frame_when_played()); let ctx = FxDrawElemContext { def_name: effect.def_name.as_str(), catalog_index: effect.catalog_index, @@ -539,13 +533,7 @@ fn draw_one_elem( let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); let elem_random_seed = elem.random_seed; - let origin = crate::spark::spark_elem_world_origin( - elem.origin, - elem.flags, - &effect.frame_now(), - &effect.frame_when_played(), - Some(elem.orient_spawn_params(elem_random_seed)), - ); + let origin = elem.world_origin(&effect.frame_now(), &effect.frame_when_played()); let ctx = FxDrawElemContext { def_name: effect.def_name.as_str(), catalog_index: effect.catalog_index, @@ -579,13 +567,7 @@ fn draw_one_elem( let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); let elem_random_seed = elem.random_seed; - let origin = crate::spark::spark_elem_world_origin( - elem.origin, - elem.flags, - &effect.frame_now(), - &effect.frame_when_played(), - Some(elem.orient_spawn_params(elem_random_seed)), - ); + let origin = elem.world_origin(&effect.frame_now(), &effect.frame_when_played()); let ctx = FxDrawElemContext { def_name: effect.def_name.as_str(), catalog_index: effect.catalog_index, @@ -631,13 +613,7 @@ fn draw_one_elem( let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); let elem_random_seed = elem.random_seed; - let origin = crate::spark::spark_elem_world_origin( - elem.origin, - elem.flags, - &effect.frame_now(), - &effect.frame_when_played(), - Some(elem.orient_spawn_params(elem_random_seed)), - ); + let origin = elem.world_origin(&effect.frame_now(), &effect.frame_when_played()); let ctx = FxDrawElemContext { def_name: effect.def_name.as_str(), catalog_index: effect.catalog_index, diff --git a/crates/fx/src/elem.rs b/crates/fx/src/elem.rs index 07d2eff..9a54166 100644 --- a/crates/fx/src/elem.rs +++ b/crates/fx/src/elem.rs @@ -25,13 +25,12 @@ pub struct FxElemSlot { /// `elem_random_seed` of the owning effect, sequence and `msec_begin`, /// none of which change while the element lives. pub random_seed: u64, + pub motion_random: fx_iw4::FxMotionRandom, pub base_vel: [f32; 3], pub origin: [f32; 3], - pub spawn_origin: [[f32; 2]; 3], - pub spawn_offset_radius: [f32; 2], - pub spawn_offset_height: [f32; 2], + pub spawn_orientation: fx_iw4::FxOrientation, pub owner_effect_slot: u16, @@ -58,11 +57,13 @@ impl Default for FxElemSlot { msec_begin: 0, life_span_msec: 0, random_seed: 0, + motion_random: fx_iw4::FxMotionRandom::default(), base_vel: [0.0; 3], origin: [0.0; 3], - spawn_origin: [[0.0; 2]; 3], - spawn_offset_radius: [0.0; 2], - spawn_offset_height: [0.0; 2], + spawn_orientation: fx_iw4::FxOrientation { + origin: [0.0; 3], + axis: fx_iw4::FxOrientFrame::IDENTITY.axis, + }, owner_effect_slot: 0, class_index: 0, sort_order: 0, @@ -73,14 +74,27 @@ impl Default for FxElemSlot { impl FxElemSlot { #[inline] - pub fn orient_spawn_params(&self, seed: u64) -> fx_iw4::FxOrientSpawnParams { - fx_iw4::FxOrientSpawnParams { - spawn_origin: self.spawn_origin, - spawn_offset_radius: self.spawn_offset_radius, - spawn_offset_height: self.spawn_offset_height, - seed, + pub fn orientation( + &self, + effect_now: &fx_iw4::FxOrientFrame, + effect_at_spawn: &fx_iw4::FxOrientFrame, + ) -> fx_iw4::FxOrientation { + if fx_iw4::elem_run_mode(self.flags) == fx_iw4::FX_ELEM_RUN_RELATIVE_TO_OFFSET { + self.spawn_orientation + } else { + fx_iw4::get_orientation(self.flags, effect_now, effect_at_spawn, None) } } + + #[inline] + pub fn world_origin( + &self, + effect_now: &fx_iw4::FxOrientFrame, + effect_at_spawn: &fx_iw4::FxOrientFrame, + ) -> [f32; 3] { + let orient = self.orientation(effect_now, effect_at_spawn); + fx_iw4::orientation_pos_to_world(orient.origin, orient.axis, self.origin) + } } #[inline] diff --git a/crates/fx/src/motion.rs b/crates/fx/src/motion.rs index 97856c4..3fe9ccd 100644 --- a/crates/fx/src/motion.rs +++ b/crates/fx/src/motion.rs @@ -1,9 +1,9 @@ use fx_iw4::{ FX_ELEM_AT_REST_NONE, collide_marks_at_rest, collide_substep_schedule, - collision_reflect_base_vel_delta, elem_dies_on_touch, elem_gravity_accel_z_sampled, + collision_reflect_base_vel_delta, elem_dies_on_touch, elem_gravity_accel_z, elem_update_has_velocity_graph, elem_uses_collision, elem_uses_vel_local, elem_uses_vel_world, - get_at_rest_fraction, get_velocity_at_time, impact_child_speed_allows, - integrate_velocity_graph, orientation_pos_from_world, orientation_pos_to_world, + get_at_rest_fraction, get_velocity_at_time_sampled, impact_child_speed_allows, + integrate_velocity_graph_sampled, orientation_pos_from_world, orientation_pos_to_world, sample_reflection_factor, trace_mask, vec3_length_sq, }; @@ -13,7 +13,6 @@ pub fn evaluate_elem_motion( flags: i32, gravity_base: f32, gravity_amp: f32, - elem_random_seed: u64, vel_graph_local: &[fx_iw4::FxElemVec3Range], vel_graph_world: &[fx_iw4::FxElemVec3Range], q: FxElemMotionQuery<'_>, @@ -26,7 +25,6 @@ pub fn evaluate_elem_motion( flags, gravity_base, gravity_amp, - elem_random_seed, vel_graph_local, vel_graph_world, q, @@ -43,7 +41,6 @@ pub fn evaluate_elem_collide_motion( coll_maxs: [f32; 3], use_item_clip: bool, has_effect_on_impact: bool, - elem_random_seed: u64, vel_graph_local: &[fx_iw4::FxElemVec3Range], vel_graph_world: &[fx_iw4::FxElemVec3Range], q: FxElemMotionQuery<'_>, @@ -54,7 +51,6 @@ pub fn evaluate_elem_collide_motion( flags, gravity_base, gravity_amp, - elem_random_seed, vel_graph_local, vel_graph_world, q, @@ -100,7 +96,8 @@ pub fn evaluate_elem_collide_motion( life_ms, dt_sec, base_vel, - elem_random_seed, + elem_random_seed: q.elem_random_seed, + motion_random: q.motion_random, origin, prev_msec: t0, msec_now: t_end, @@ -113,7 +110,6 @@ pub fn evaluate_elem_collide_motion( flags, gravity_base, gravity_amp, - elem_random_seed, vel_graph_local, vel_graph_world, sub_q, @@ -155,7 +151,7 @@ pub fn evaluate_elem_collide_motion( let age_at_hit_msec = (t0.saturating_sub(q.msec_begin)).max(0) as f32 + (t_end.saturating_sub(t0)).max(0) as f32 * hit.fraction; - let pre_vel = get_velocity_at_time( + let pre_vel = get_velocity_at_time_sampled( flags, base_vel, age_at_hit_msec, @@ -163,7 +159,7 @@ pub fn evaluate_elem_collide_motion( vel_graph_local, vel_graph_world, q.effect_axis, - elem_random_seed, + q.motion_random.velocity, ); let pre_speed_sq = vec3_length_sq(pre_vel); if has_effect_on_impact && impact_child_speed_allows(pre_speed_sq) { @@ -179,7 +175,7 @@ pub fn evaluate_elem_collide_motion( } let reflection = - sample_reflection_factor(reflection_base, reflection_amp, elem_random_seed); + sample_reflection_factor(reflection_base, reflection_amp, q.elem_random_seed); let scaled = [ pre_vel[0] * reflection, pre_vel[1] * reflection, @@ -232,23 +228,32 @@ fn integrate_free_flight( flags: i32, gravity_base: f32, gravity_amp: f32, - elem_random_seed: u64, local_samples: &[fx_iw4::FxElemVec3Range], world_samples: &[fx_iw4::FxElemVec3Range], q: FxElemMotionQuery<'_>, ) -> FxElemMotionResult { let mut stored = q.origin; if elem_uses_vel_local(flags) && local_samples.len() >= 2 { - let d = - integrate_velocity_graph(local_samples, q.age0, q.age1, q.life_ms, elem_random_seed); + let d = integrate_velocity_graph_sampled( + local_samples, + q.age0, + q.age1, + q.life_ms, + q.motion_random.velocity, + ); stored[0] += d[0]; stored[1] += d[1]; stored[2] += d[2]; } let mut world = orientation_pos_to_world(q.orient.origin, q.orient.axis, stored); if elem_uses_vel_world(flags) && world_samples.len() >= 2 { - let d = - integrate_velocity_graph(world_samples, q.age0, q.age1, q.life_ms, elem_random_seed); + let d = integrate_velocity_graph_sampled( + world_samples, + q.age0, + q.age1, + q.life_ms, + q.motion_random.velocity, + ); world[0] += d[0]; world[1] += d[1]; world[2] += d[2]; @@ -257,7 +262,11 @@ fn integrate_free_flight( if elem_update_has_velocity_graph(flags) { { let dt = q.dt_sec; - let g = elem_gravity_accel_z_sampled(gravity_base, gravity_amp, elem_random_seed); + let g = elem_gravity_accel_z(fx_iw4::sample_float_range( + gravity_base, + gravity_amp, + q.motion_random.gravity, + )); world[0] += base_vel[0] * dt; world[1] += base_vel[1] * dt; world[2] += base_vel[2] * dt; diff --git a/crates/fx/src/sort.rs b/crates/fx/src/sort.rs index c758087..9d1d3d5 100644 --- a/crates/fx/src/sort.rs +++ b/crates/fx/src/sort.rs @@ -1,7 +1,6 @@ use fx_iw4::{existing_elem_sorts_before_new, sort_dist_to_cam_sq}; use crate::elem::{FX_ELEM_HANDLE_NONE, elem_slot_for_handle}; -use crate::spark::spark_elem_world_origin; use crate::system::FxSystemHost; pub fn sort_new_elems_in_effect( @@ -145,14 +144,7 @@ fn sort_distance( } let elem = host.elems.get(elem_slot)?; let effect = host.effect_at(effect_slot)?; - let seed = elem.random_seed; - let world = spark_elem_world_origin( - elem.origin, - elem.flags, - &effect.frame_now(), - &effect.frame_when_played(), - Some(elem.orient_spawn_params(seed)), - ); + let world = elem.world_origin(&effect.frame_now(), &effect.frame_when_played()); let distance = sort_dist_to_cam_sq(camera_origin, world); host.sort_distances[elem_slot] = (host.sort_epoch, distance); Some(distance) diff --git a/crates/fx/src/spawn.rs b/crates/fx/src/spawn.rs index 0c492a4..1b53444 100644 --- a/crates/fx/src/spawn.rs +++ b/crates/fx/src/spawn.rs @@ -480,6 +480,24 @@ fn spawn_elem( let prev_head = effect.first_elem_handle[class]; effect.first_elem_handle[class] = handle; + let spawn_orientation = fx_iw4::get_orientation( + elem_def.flags, + &effect.frame_now(), + &effect.frame_when_played(), + Some(fx_iw4::FxOrientSpawnParams { + spawn_origin: elem_def.spawn_origin, + spawn_offset_radius: [ + elem_def.spawn_offset_radius_base, + elem_def.spawn_offset_radius_amp, + ], + spawn_offset_height: [ + elem_def.spawn_offset_height_base, + elem_def.spawn_offset_height_amp, + ], + seed: life_idx, + }), + ); + host.elems[elem_slot] = FxElemSlot { occupied: true, def_index, @@ -494,17 +512,10 @@ fn spawn_elem( msec_begin, life_span_msec: life, random_seed: life_idx, + motion_random: fx_iw4::FxMotionRandom::from_seed(life_idx), base_vel: [0.0; 3], origin, - spawn_origin: elem_def.spawn_origin, - spawn_offset_radius: [ - elem_def.spawn_offset_radius_base, - elem_def.spawn_offset_radius_amp, - ], - spawn_offset_height: [ - elem_def.spawn_offset_height_base, - elem_def.spawn_offset_height_amp, - ], + spawn_orientation, owner_effect_slot: effect_slot as u16, class_index: class as u8, sort_order: elem_def.sort_order, @@ -529,7 +540,6 @@ fn spawn_elem( match crate::spark_fountain::alloc_spark_fountain(host) { Some(fountain_handle) => { host.elems[elem_slot].spark_cloud_handle = fountain_handle; - // Fountain cells are world space in every run mode. match crate::spark_fountain::spray_spark_fountain( host, fountain_handle, diff --git a/crates/fx/src/update.rs b/crates/fx/src/update.rs index 63b4f3a..013ec54 100644 --- a/crates/fx/src/update.rs +++ b/crates/fx/src/update.rs @@ -1,9 +1,9 @@ use fx_iw4::{ FX_ELEM_TYPE_SPARK_CLOUD, FX_ELEM_TYPE_SPARK_FOUNTAIN, FX_STATUS_HAS_PENDING_LOOP_ELEMS, - FX_STATUS_REF_COUNT_MASK_IW4, FxOrientFrame, FxUpdateEffectBolt, axis_to_quat, + FX_STATUS_REF_COUNT_MASK_IW4, FxUpdateEffectBolt, axis_to_quat, begin_iterating_over_effects_exclusive, bolt_compose_orientation, bolt_mark_lost, elem_norm_time, elem_uses_collision, end_iterating_over_effects, end_iterating_runs_gc, - get_orientation, unit_quat_to_axis, update_effect_bolt, vector_vectors, + unit_quat_to_axis, update_effect_bolt, vector_vectors, }; use std::collections::HashMap; @@ -36,6 +36,7 @@ pub struct FxElemMotionQuery<'a> { pub base_vel: [f32; 3], pub elem_random_seed: u64, + pub motion_random: fx_iw4::FxMotionRandom, pub origin: [f32; 3], pub prev_msec: i32, @@ -61,6 +62,7 @@ pub struct PendingCollide { dt_sec: f32, base_vel: [f32; 3], elem_random_seed: u64, + motion_random: fx_iw4::FxMotionRandom, origin: [f32; 3], prev_msec: i32, msec_now: i32, @@ -82,6 +84,7 @@ impl PendingCollide { dt_sec: self.dt_sec, base_vel: self.base_vel, elem_random_seed: self.elem_random_seed, + motion_random: self.motion_random, origin: self.origin, prev_msec: self.prev_msec, msec_now: self.msec_now, @@ -387,10 +390,10 @@ fn pending_collide_for_elem( let def_index = elem.def_index; let base_vel = elem.base_vel; let elem_seed = elem.random_seed; + let motion_random = elem.motion_random; let origin = elem.origin; let msec_begin = elem.msec_begin; let life_msec = elem.life_span_msec.max(1); - let flags = elem.flags; let (def_name, catalog_index, now, alt) = match host.effect_at(effect_slot) { Some(e) => ( slot_def_name(e), @@ -400,11 +403,7 @@ fn pending_collide_for_elem( ), None => return None, }; - let spawn = host - .elems - .get(slot) - .map(|e| e.orient_spawn_params(elem_seed)); - let orient = crate::spark::spark_elem_orientation(flags, &now, &alt, spawn); + let orient = elem.orientation(&now, &alt); let life_ms = life_msec as f32; Some(PendingCollide { handle, @@ -417,6 +416,7 @@ fn pending_collide_for_elem( dt_sec: (msec_now.saturating_sub(prev_msec)).max(0) as f32 * 0.001, base_vel, elem_random_seed: elem_seed, + motion_random, origin, prev_msec, msec_now, @@ -615,6 +615,7 @@ fn update_element( let life_msec = elem.life_span_msec.max(1); let base_vel = elem.base_vel; let elem_seed = elem.random_seed; + let motion_random = elem.motion_random; let origin = elem.origin; let elem_type = elem.elem_type; let spark_handle = elem.spark_cloud_handle; @@ -631,11 +632,7 @@ fn update_element( ), None => return true, }; - let spawn = host - .elems - .get(slot) - .map(|e| e.orient_spawn_params(elem_seed)); - let orient = crate::spark::spark_elem_orientation(flags, &now, &alt, spawn); + let orient = elem.orientation(&now, &alt); if msec_now >= death { spawn_death_child( @@ -644,11 +641,8 @@ fn update_element( def_name.as_str(), catalog_index, def_index, - flags, origin, - &now, - &alt, - spawn, + orient, msec_now, products, mark_entity, @@ -670,6 +664,7 @@ fn update_element( dt_sec, base_vel, elem_random_seed: elem_seed, + motion_random, origin, prev_msec, msec_now, @@ -781,11 +776,8 @@ fn update_element( def_name.as_str(), catalog_index, def_index, - flags, death_origin, - &now, - &alt, - spawn, + orient, msec_now, products, mark_entity, @@ -808,20 +800,17 @@ fn update_element( elem_random_seed: elem_seed, norm_time: elem_norm_time(msec_now.saturating_sub(msec_begin).max(0), life_msec), }) { - let (origin_now, at_rest_now, spawn, seed) = match host.elems.get(slot) { - Some(e) => ( - e.origin, - e.at_rest_fraction, - Some(e.orient_spawn_params(elem_seed)), - elem_seed, + let (world, at_rest_now) = match host.elems.get(slot) { + Some(e) => (e.world_origin(&now, &alt), e.at_rest_fraction), + None => ( + spark_elem_world_origin(origin, flags, &now, &alt, None), + at_rest_fraction, ), - None => (origin, at_rest_fraction, None, elem_seed), }; - let world = spark_elem_world_origin(origin_now, flags, &now, &alt, spawn); let axis = spark_elem_axis( visual.spawn_angles, visual.angular_velocity, - seed, + elem_seed, msec_now.saturating_sub(msec_begin).max(0), life_msec, at_rest_now, @@ -881,16 +870,12 @@ fn spawn_death_child( parent_def_name: &str, catalog_index: u16, def_index: u8, - flags: i32, elem_origin: [f32; 3], - effect_now: &FxOrientFrame, - effect_alt: &FxOrientFrame, - spawn: Option, + orient: fx_iw4::FxOrientation, msec: i32, products: crate::FxSpawnProducts, mark_entity: Option, ) { - let orient = get_orientation(flags, effect_now, effect_alt, spawn); let world = fx_iw4::orientation_pos_to_world(orient.origin, orient.axis, elem_origin); let played = on_child( host, diff --git a/crates/fx_iw4/src/integrate.rs b/crates/fx_iw4/src/integrate.rs index 159866c..d2d2c16 100644 --- a/crates/fx_iw4/src/integrate.rs +++ b/crates/fx_iw4/src/integrate.rs @@ -12,14 +12,7 @@ impl FxElemVec3Range { } } -fn velocity_random(seed: u64) -> [f32; 3] { - use crate::random::{FxRandomChannel, sample_f32}; - [ - sample_f32(seed, FxRandomChannel::VelocityX), - sample_f32(seed, FxRandomChannel::VelocityY), - sample_f32(seed, FxRandomChannel::VelocityZ), - ] -} +use crate::random::velocity_random; pub fn integrate_velocity_graph( samples: &[FxElemVec3Range], @@ -27,6 +20,28 @@ pub fn integrate_velocity_graph( end_age01: f32, life_ms: f32, seed: u64, +) -> [f32; 3] { + if samples.len() < 2 + || life_ms <= 0.0 + || clamp01(end_age01).max(clamp01(start_age01)) <= clamp01(start_age01) + { + return [0.0; 3]; + } + integrate_velocity_graph_sampled( + samples, + start_age01, + end_age01, + life_ms, + velocity_random(seed), + ) +} + +pub fn integrate_velocity_graph_sampled( + samples: &[FxElemVec3Range], + start_age01: f32, + end_age01: f32, + life_ms: f32, + random: [f32; 3], ) -> [f32; 3] { if samples.len() < 2 || life_ms <= 0.0 { return [0.0, 0.0, 0.0]; @@ -39,7 +54,6 @@ pub fn integrate_velocity_graph( let segment_count = samples.len() - 1; let segment_span_ms = life_ms * segment_count as f32; - let random = velocity_random(seed); let mut delta = [0.0f32; 3]; let first = (libm::floorf(start * segment_count as f32) as usize).min(segment_count - 1); let end_excl = @@ -64,7 +78,16 @@ pub fn integrate_velocity_graph( #[inline] pub fn sample_vel_graph_at_age(samples: &[FxElemVec3Range], age01: f32, seed: u64) -> [f32; 3] { - sample_lerp(samples, age01, velocity_random(seed)) + sample_vel_graph_at_age_sampled(samples, age01, velocity_random(seed)) +} + +#[inline] +pub fn sample_vel_graph_at_age_sampled( + samples: &[FxElemVec3Range], + age01: f32, + random: [f32; 3], +) -> [f32; 3] { + sample_lerp(samples, age01, random) } fn clamp01(x: f32) -> f32 { diff --git a/crates/fx_iw4/src/lib.rs b/crates/fx_iw4/src/lib.rs index b4cdc35..d8695f0 100644 --- a/crates/fx_iw4/src/lib.rs +++ b/crates/fx_iw4/src/lib.rs @@ -187,8 +187,8 @@ pub use impact::{ }; pub use integrate::{ FX_SPARKCLOUD_HISTORY_FAR_MS, FX_SPARKCLOUD_HISTORY_NEAR_MS, FxElemVec3Range, - integrate_velocity_graph, particle_cloud_cell_count, sample_vel_graph_at_age, - sparkcloud_history_lookback_ms, + integrate_velocity_graph, integrate_velocity_graph_sampled, particle_cloud_cell_count, + sample_vel_graph_at_age, sample_vel_graph_at_age_sampled, sparkcloud_history_lookback_ms, }; pub use laser::{ FX_LASER_POST_LIGHT_COLOR, FX_LASER_POST_LIGHT_HALF, FX_LASER_POST_LIGHT_MATERIAL, @@ -262,8 +262,8 @@ pub use post_light::{ }; pub use quat::{axis_to_quat, quat_nlerp, quat_normalize}; pub use random::{ - FX_RANDOM_VERSION, FxRandomChannel, effect_random_key, elem_random_seed, elem_visual_index, - sample_at, sample_f32, sample_u16, trail_random_seed, + FX_RANDOM_VERSION, FxMotionRandom, FxRandomChannel, effect_random_key, elem_random_seed, + elem_visual_index, sample_at, sample_f32, sample_u16, trail_random_seed, }; pub use rotate_axis::{ FX_DEG_TO_RAD, FX_RAD_TO_DEG, FX_RAND_ROT_DEGREES, impact_mark_axis, randomly_rotate_axis, @@ -341,7 +341,7 @@ pub use vec::{ FX_PERP_VECTOR_UNIT, effect_orient_arc, perpendicular_vector, vec3_distance, vec3_length_sq, vec3_normalize, vector_vectors, }; -pub use velocity::{FX_VEL_AT_TIME_SCALE, get_velocity_at_time}; +pub use velocity::{FX_VEL_AT_TIME_SCALE, get_velocity_at_time, get_velocity_at_time_sampled}; pub use view::{ FX_EFFECT_DEF_SIZE, FX_ELEM_DEF_STRIDE, FxEffectDefView, FxElemDefView, effect_def_view, elem_def_gravity_accel_z, elem_def_view, elem_def_view_x64, diff --git a/crates/fx_iw4/src/random.rs b/crates/fx_iw4/src/random.rs index 6ee16bf..a9e69ec 100644 --- a/crates/fx_iw4/src/random.rs +++ b/crates/fx_iw4/src/random.rs @@ -103,3 +103,26 @@ pub fn elem_visual_index(count: u8, key: u64) -> usize { } ((u32::from(sample_u16(key, FxRandomChannel::Visual)) * u32::from(count)) >> 16) as usize } + +#[derive(Clone, Copy, Debug, Default)] +pub struct FxMotionRandom { + pub velocity: [f32; 3], + pub gravity: f32, +} + +impl FxMotionRandom { + pub fn from_seed(seed: u64) -> Self { + Self { + velocity: velocity_random(seed), + gravity: sample_f32(seed, FxRandomChannel::Gravity), + } + } +} + +pub(crate) fn velocity_random(seed: u64) -> [f32; 3] { + [ + sample_f32(seed, FxRandomChannel::VelocityX), + sample_f32(seed, FxRandomChannel::VelocityY), + sample_f32(seed, FxRandomChannel::VelocityZ), + ] +} diff --git a/crates/fx_iw4/src/velocity.rs b/crates/fx_iw4/src/velocity.rs index e3b8803..7850433 100644 --- a/crates/fx_iw4/src/velocity.rs +++ b/crates/fx_iw4/src/velocity.rs @@ -1,5 +1,5 @@ use crate::flags::{elem_uses_vel_local, elem_uses_vel_world}; -use crate::integrate::sample_vel_graph_at_age; +use crate::integrate::sample_vel_graph_at_age_sampled; pub const FX_VEL_AT_TIME_SCALE: f64 = 1000.0; @@ -13,6 +13,34 @@ pub fn get_velocity_at_time( world_samples: &[crate::FxElemVec3Range], orient_axis: [[f32; 3]; 3], seed: u64, +) -> [f32; 3] { + if !(elem_uses_vel_world(flags) && world_samples.len() >= 2) + && !(elem_uses_vel_local(flags) && local_samples.len() >= 2) + { + return base_vel; + } + get_velocity_at_time_sampled( + flags, + base_vel, + age_msec, + life_ms, + local_samples, + world_samples, + orient_axis, + crate::random::velocity_random(seed), + ) +} + +#[inline] +pub fn get_velocity_at_time_sampled( + flags: i32, + base_vel: [f32; 3], + age_msec: f32, + life_ms: f32, + local_samples: &[crate::FxElemVec3Range], + world_samples: &[crate::FxElemVec3Range], + orient_axis: [[f32; 3]; 3], + random: [f32; 3], ) -> [f32; 3] { let mut out = base_vel; let age01 = if life_ms > 0.0 { @@ -30,13 +58,13 @@ pub fn get_velocity_at_time( let scale = FX_VEL_AT_TIME_SCALE as f32; if elem_uses_vel_world(flags) && world_samples.len() >= 2 { - let s = sample_vel_graph_at_age(world_samples, age01, seed); + let s = sample_vel_graph_at_age_sampled(world_samples, age01, random); out[0] += s[0] * scale; out[1] += s[1] * scale; out[2] += s[2] * scale; } if elem_uses_vel_local(flags) && local_samples.len() >= 2 { - let s = sample_vel_graph_at_age(local_samples, age01, seed); + let s = sample_vel_graph_at_age_sampled(local_samples, age01, random); let wx = s[0] * orient_axis[0][0] + s[1] * orient_axis[1][0] + s[2] * orient_axis[2][0]; let wy = s[0] * orient_axis[0][1] + s[1] * orient_axis[1][1] + s[2] * orient_axis[2][1]; diff --git a/crates/gamemode_iw4/src/lib.rs b/crates/gamemode_iw4/src/lib.rs index a1c048e..02e9b28 100644 --- a/crates/gamemode_iw4/src/lib.rs +++ b/crates/gamemode_iw4/src/lib.rs @@ -18,6 +18,7 @@ pub mod perks; pub mod phase; pub mod playerlogic; pub mod prematch; +pub mod progression; pub mod radius_damage; mod score; pub mod stuck_in_client; diff --git a/crates/gamemode_iw4/src/progression.rs b/crates/gamemode_iw4/src/progression.rs new file mode 100644 index 0000000..a2415be --- /dev/null +++ b/crates/gamemode_iw4/src/progression.rs @@ -0,0 +1,129 @@ +extern crate alloc; + +use alloc::{collections::BTreeMap, string::String, vec::Vec}; + +pub trait ProgressionTable { + fn rows(&self) -> usize; + fn cell(&self, row: usize, column: usize) -> &str; +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RankProgression { + thresholds: Vec<(u32, i32)>, +} + +impl RankProgression { + pub fn capture(table: &impl ProgressionTable) -> Result { + let mut thresholds = Vec::new(); + for row in 0..table.rows() { + let Ok(rank) = table.cell(row, 0).parse::() else { + continue; + }; + let xp = table + .cell(row, 2) + .parse::() + .map_err(|_| "rank.invalid_threshold")?; + if xp < 0 { + return Err("rank.invalid_threshold"); + } + thresholds.push((rank, xp)); + } + thresholds.sort_unstable_by_key(|&(rank, _)| rank); + if thresholds.first() != Some(&(0, 0)) + || thresholds + .windows(2) + .any(|pair| pair[1].0 != pair[0].0 + 1 || pair[1].1 <= pair[0].1) + { + return Err("rank.invalid_table"); + } + Ok(Self { thresholds }) + } + + pub fn thresholds(&self) -> &[(u32, i32)] { + &self.thresholds + } + + pub fn rank(&self, experience: i32) -> Option { + self.thresholds + .iter() + .rev() + .find(|&&(_, xp)| xp <= experience.max(0)) + .map(|&(rank, _)| rank) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ChallengeRequirement { + pub name: String, + pub tier: i32, +} + +impl ChallengeRequirement { + pub fn parse(name: &str) -> Self { + let (name, tier) = name + .rsplit_once('_') + .filter(|(_, suffix)| suffix.len() == 1 && suffix.as_bytes()[0].is_ascii_digit()) + .map_or((name, 1), |(base, suffix)| { + (base, i32::from(suffix.as_bytes()[0] - b'0')) + }); + Self { + name: name.into(), + tier, + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct UnlockRequirement { + pub rank: u32, + pub challenges: Vec, +} + +impl UnlockRequirement { + pub fn capture(rank: &str, challenges: &str) -> Result { + Ok(Self { + rank: rank.parse().map_err(|_| "unlock.invalid_rank")?, + challenges: challenges + .split(|c: char| c == ';' || c.is_ascii_whitespace()) + .filter(|name| !name.is_empty()) + .map(ChallengeRequirement::parse) + .collect(), + }) + } + + pub fn unlocked( + &self, + rank: u32, + mut challenge_state: impl FnMut(&str) -> Option, + ) -> bool { + rank >= self.rank + && self.challenges.iter().all(|challenge| { + challenge_state(&challenge.name).is_some_and(|state| state > challenge.tier) + }) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct PlayerProgression { + pub experience: i32, + pub prestige: i32, + pub rank: u32, + pub challenges: BTreeMap, +} + +impl PlayerProgression { + pub fn unlocked(&self, requirement: &UnlockRequirement) -> bool { + requirement.unlocked(self.rank, |name| self.challenges.get(name).copied()) + } +} + +pub fn custom_class_capacity(rank: u32, prestige: i32) -> usize { + if rank < 3 { + return 0; + } + custom_class_slot_count(prestige) +} + +pub fn custom_class_slot_count(prestige: i32) -> usize { + 5 + ((prestige.clamp(0, 10) + 1) / 2) as usize +} diff --git a/crates/hud/Cargo.toml b/crates/hud/Cargo.toml index 17bafba..81a12ce 100644 --- a/crates/hud/Cargo.toml +++ b/crates/hud/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true description = "Draws what hud_iw4 computes / Snapshot presents." [dependencies] +structured_data_iw4.workspace = true diag.workspace = true bevy.workspace = true asset_core.workspace = true diff --git a/crates/hud/src/lib.rs b/crates/hud/src/lib.rs index 8a40164..fc1be4f 100644 --- a/crates/hud/src/lib.rs +++ b/crates/hud/src/lib.rs @@ -41,7 +41,7 @@ pub use gpu_list::{ HudTessBatch, HudTessGpuFrame, HudTessTechnique, HudTessVertex, ShellshockScreen, }; pub use hudelem::HudElemSoundLatch; -pub use menus::ScriptMenus; +pub use menus::{MenuState, ScriptMenus}; pub use overhead_names::{ OverheadPosedHead, OverheadPosedModelFrame, OverheadPosedPlayerFrame, OverheadPosedPlayerFramePublished, diff --git a/crates/hud/src/menus/host.rs b/crates/hud/src/menus/host.rs index c467bcb..177fe50 100644 --- a/crates/hud/src/menus/host.rs +++ b/crates/hud/src/menus/host.rs @@ -4,6 +4,7 @@ use hud_iw4::{ExprError, ExprHost, Operand, PartyFlag, WeaponLockView}; use crate::playercard::UiLocalVars; pub(crate) struct MenuWorld<'a> { + pub player_data: Option<&'a sim::LocalPlayerData>, pub ms: i32, pub in_game: bool, pub party: &'a frame::UiPartyState, @@ -102,7 +103,13 @@ impl MenuHost<'_> { let int = |at: usize| path.get(at).map_or(-1, hud_iw4::source_int); let field_name = field(2).to_ascii_lowercase(); if field_name == "inuse" { - return Ok(Operand::Int(i32::from(slot.is_some()))); + return Ok(Operand::Int(i32::from( + slot.is_some() + && self + .world + .player_data + .is_some_and(|data| data.custom_class_available(index)), + ))); } let Some(slot) = slot else { return Ok(Operand::Str(String::from("none"))); @@ -363,10 +370,32 @@ impl ExprHost for MenuHost<'_> { let root = path.first().map(hud_iw4::source_str).unwrap_or_default(); if root.eq_ignore_ascii_case("customClasses") { self.custom_class(path) - } else if root.eq_ignore_ascii_case("killstreaks") { - Ok(Operand::Str(String::from("none"))) } else { - Err(ExprError::Host("player data path")) + let data = self + .world + .player_data + .ok_or(ExprError::Host("player data unavailable"))?; + let names: Vec<_> = path.iter().map(hud_iw4::source_str).collect(); + let keys: Vec<_> = names + .iter() + .zip(path) + .map(|(name, operand)| match operand { + Operand::Int(index) => structured_data_iw4::Key::Index(*index), + _ => structured_data_iw4::Key::Name(name), + }) + .collect(); + match data + .read(&keys) + .map_err(|_| ExprError::Host("player data path"))? + { + structured_data_iw4::Value::Int(n) => Ok(Operand::Int(n)), + structured_data_iw4::Value::Bool(n) => Ok(Operand::Int(n.into())), + structured_data_iw4::Value::Float(n) => Ok(Operand::Float(n)), + structured_data_iw4::Value::String(s) => Ok(Operand::Str(s.into())), + structured_data_iw4::Value::Bytes(s) => { + Ok(Operand::Str(String::from_utf8_lossy(s).into_owned())) + } + } } } fn table_lookup( @@ -376,10 +405,27 @@ impl ExprHost for MenuHost<'_> { key: &str, result_col: i32, ) -> Result { + if let Some(value) = self + .world + .player_data + .and_then(|data| data.table_lookup(table, col0, key, result_col)) + { + return Ok(Operand::Str(value.into())); + } if let Some(value) = self.weapon_table_field(table, col0, key, result_col) { return Ok(Operand::Str(value)); } - let Some(t) = self.world.catalog.and_then(|c| c.string_table(table)) else { + let shared = self.world.weapons.and_then(|weapons| { + if table.eq_ignore_ascii_case("mp/ranktable.csv") { + weapons.shared_rank_table().ok() + } else if table.eq_ignore_ascii_case("mp/rankicontable.csv") { + weapons.shared_rank_icons().ok() + } else { + None + } + }); + let Some(t) = shared.or_else(|| self.world.catalog.and_then(|c| c.string_table(table))) + else { return Err(ExprError::Host("string table")); }; Ok(Operand::Str( @@ -389,7 +435,24 @@ impl ExprHost for MenuHost<'_> { )) } fn table_lookup_by_row(&self, table: &str, row: i32, col: i32) -> Result { - let Some(t) = self.world.catalog.and_then(|c| c.string_table(table)) else { + if let Some(value) = self + .world + .player_data + .and_then(|data| data.table_cell(table, row, col)) + { + return Ok(Operand::Str(value.into())); + } + let shared = self.world.weapons.and_then(|weapons| { + if table.eq_ignore_ascii_case("mp/ranktable.csv") { + weapons.shared_rank_table().ok() + } else if table.eq_ignore_ascii_case("mp/rankicontable.csv") { + weapons.shared_rank_icons().ok() + } else { + None + } + }); + let Some(t) = shared.or_else(|| self.world.catalog.and_then(|c| c.string_table(table))) + else { return Err(ExprError::Host("string table")); }; Ok(Operand::Str(t.cell(row, col).to_owned())) @@ -408,8 +471,34 @@ impl ExprHost for MenuHost<'_> { fn weapon_lock(&self) -> Result { Err(ExprError::Host("weapon lock")) } - fn is_item_unlocked(&self, _item: &str) -> Result { - Ok(1) + fn is_item_unlocked(&self, item: &str) -> Result { + let requirement = if let Some(family) = asset_game::FamilyKey::parse(item) { + self.world + .weapons + .ok_or(ExprError::Host("weapon catalog"))? + .item_unlock_requirement(family.namespace, &family.base) + .map_err(|_| ExprError::Host("unlock data"))? + } else { + let table = self + .world + .catalog + .and_then(|catalog| catalog.string_table("mp/unlockTable.csv")) + .ok_or(ExprError::Host("unlock table"))?; + let Some(row) = table.lookup_row_in_col(0, item) else { + return Ok(1); + }; + gamemode_iw4::progression::UnlockRequirement::capture( + table.cell(row, 2), + table.cell(row, 3), + ) + .map_err(|_| ExprError::Host("unlock requirement"))? + }; + Ok(self + .world + .player_data + .and_then(|data| data.progression.as_ref()) + .is_some_and(|progression| progression.unlocked(&requirement)) + .into()) } fn party_flag(&self, flag: PartyFlag) -> Result { let party = self.world.party; diff --git a/crates/hud/src/menus/mod.rs b/crates/hud/src/menus/mod.rs index af28515..36553ea 100644 --- a/crates/hud/src/menus/mod.rs +++ b/crates/hud/src/menus/mod.rs @@ -7,15 +7,44 @@ use runtime::Runner; use state::OpenMenu; pub use state::ScriptMenus; +#[derive(bevy::prelude::Resource, Default)] +pub struct FrontendMenus(pub ScriptMenus); + +#[derive(bevy::ecs::system::SystemParam)] +pub struct MenuState<'w> { + scoped: Option>, + frontend: Option>, +} +impl MenuState<'_> { + pub fn as_ref(&self) -> Option<&ScriptMenus> { + self.scoped + .as_deref() + .or_else(|| self.frontend.as_deref().map(|menus| &menus.0)) + } + pub fn as_deref(&self) -> Option<&ScriptMenus> { + self.as_ref() + } + pub fn is_some(&self) -> bool { + self.as_ref().is_some() + } + pub fn is_some_and(&self, predicate: impl FnOnce(&ScriptMenus) -> bool) -> bool { + self.as_ref().is_some_and(predicate) + } + pub fn open_names(&self) -> Vec { + self.as_ref().map_or_else(Vec::new, ScriptMenus::open_names) + } + pub fn focused_item(&self) -> Option<(&str, usize)> { + self.as_ref().and_then(ScriptMenus::focused_item) + } +} + use std::collections::HashMap; use asset_game::{MenuCatalog, MenuDef, SessionTeamSettings}; use assets::PreparedLocalizedStrings; use bevy::prelude::*; use bevy::ui::FocusPolicy; -use frame::{ - AppScreen, MatchTornDown, RuntimeRole, UiExecCommand, UiMenuKey, UiMenuRequest, UiPlaySound, -}; +use frame::{AppScreen, RuntimeRole, UiExecCommand, UiMenuKey, UiMenuRequest, UiPlaySound}; use net::{ ActionRequestIds, ClientActionInbox, ClientActionInput, LocalPresentClient, PresentedSnapshot, }; @@ -82,6 +111,54 @@ fn painted_def(def: &MenuDef, open: &OpenMenu) -> MenuDef { out } +fn custom_class_slot(item: &asset_game::MenuItem) -> Option { + fn response(events: &[asset_game::MenuEvent]) -> Option { + events.iter().find_map(|event| match event { + asset_game::MenuEvent::Script(text) => { + script::parse(text).into_iter().find_map(|cmd| { + let script::Command::ScriptMenuResponse(value) = cmd else { + return None; + }; + value + .strip_prefix("custom")? + .parse::() + .ok()? + .checked_sub(1) + .filter(|&slot| slot < sim::match_state::PERSONAL_CLASS_SLOTS) + }) + } + asset_game::MenuEvent::If { then, .. } | asset_game::MenuEvent::Else(then) => { + response(then) + } + _ => None, + }) + } + response(&item.handlers.action).or_else(|| response(&item.handlers.accept)) +} + +fn class_unavailable<'a>( + menu: &str, + item: &asset_game::MenuItem, + dvars: &'a frame::UiMenuDvars, +) -> Option<&'a str> { + let key = if menu.eq_ignore_ascii_case("class_picker") || menu.eq_ignore_ascii_case("class_samples_picker") { + let slot = item.name.strip_prefix("choice_")?.parse::().ok()?; + format!("ui_class_choice_unavailable_{slot}") + } else if menu.eq_ignore_ascii_case("iw4l_main") && item.name == "classes" + || menu.eq_ignore_ascii_case("game_lobby") && item.handlers.action.iter().any(|event| { + matches!(event, asset_game::MenuEvent::Script(text) if text.contains("ui_class_setup")) + }) + { + "ui_class_feature_lock".to_owned() + } else if menu.to_ascii_lowercase().starts_with("changeclass") { + let slot = custom_class_slot(item)?; + format!("ui_class_unavailable_{slot}") + } else { + return None; + }; + dvars.get(&key).filter(|reason| !reason.is_empty()) +} + #[derive(Component)] pub(crate) struct ScriptMenuRaster; @@ -115,6 +192,7 @@ pub(crate) struct MenuInputs<'w, 's> { compass: Option>, party: Res<'w, frame::UiPartyState>, frontend_strings: Option>, + player_data: Option>, } #[derive(Default)] @@ -155,17 +233,28 @@ pub(crate) fn update_script_menus( local: Res, role: Option>, mut input: MenuInputs, - mut torn: MessageReader, - mut menus: ResMut, - mut locals: ResMut, + mut transitions: MessageReader>, + mut menu_states: (Option>, ResMut), + mut local_states: ( + Option>, + ResMut, + ), mut exprs: ResMut, mut out: MenuOutputs, mut hud_images: ResMut, mut images: ResMut>, mut pass: ResMut, ) { + let mut menus = menu_states.0.as_deref_mut().unwrap_or(&mut menu_states.1.0); + let mut locals = local_states + .0 + .as_deref_mut() + .unwrap_or(&mut local_states.1.0); pass.script_menus = TessJob::Hide; - if torn.read().count() > 0 { + if transitions + .read() + .any(|event| event.exited == Some(frame::MatchScope::Live)) + { out.binding.command = None; *menus = ScriptMenus::default(); } @@ -205,6 +294,7 @@ pub(crate) fn update_script_menus( input.frontend_strings.as_deref() }; let world = MenuWorld { + player_data: input.player_data.as_deref(), ms: crate::scorebar::milliseconds() as i32, in_game, party: &input.party, @@ -396,6 +486,18 @@ pub(crate) fn update_script_menus( let mut painted = painted_def(def, open); let mut slider_parts = Vec::new(); for item in &mut painted.items { + if let Some(reason) = class_unavailable(&open.name, item, &out.dvars) { + if let Some(slot) = custom_class_slot(item) { + let name = world + .classes + .and_then(|classes| classes.slots.get(slot)) + .map_or("Class", |class| class.name.as_str()); + item.text_key = format!("{name} — {reason}"); + item.text_literal = true; + item.text_exp.clear(); + } + item.fore_color = [0.5, 0.5, 0.5, 1.0]; + } if item.item_type == 14 && !item.dvar.is_empty() { item.text_key = out .dvars diff --git a/crates/hud/src/menus/runtime.rs b/crates/hud/src/menus/runtime.rs index 28f8347..759982e 100644 --- a/crates/hud/src/menus/runtime.rs +++ b/crates/hud/src/menus/runtime.rs @@ -649,6 +649,14 @@ impl Runner<'_, '_> { } pub(crate) fn activate(&mut self, menu: &str, index: usize, accept: bool) { + if self + .catalog + .get(menu) + .and_then(|def| def.items.get(index)) + .is_some_and(|item| super::class_unavailable(menu, item, self.dvars).is_some()) + { + return; + } if self.adjust(menu, index, 1) { return; } diff --git a/crates/hud/src/overhead_names.rs b/crates/hud/src/overhead_names.rs index 3513a01..9a88db0 100644 --- a/crates/hud/src/overhead_names.rs +++ b/crates/hud/src/overhead_names.rs @@ -2,6 +2,7 @@ use crate::draw2d::{Draw2dCmd, Draw2dList, Draw2dOp, Draw2dProvenance, tessellat use crate::gpu_list::{HudTessPass, TessJob}; use asset_game::MenuCatalog; use bevy::prelude::*; +use frame::ScopeApp; use hud_iw4::*; use net::{CEntity, CEntityRuntime, FrameClock, LocalPresentClient, PresentedSnapshot}; @@ -99,11 +100,12 @@ impl TargetBoxSettings { } pub(crate) fn register(app: &mut App) { - app.init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, update_overhead_names + .in_set(frame::InMatch) .after(OverheadPosedPlayerFramePublished) .after(frame::ScreenEffectsPublished) .after(crate::surface::update_hud_surface) diff --git a/crates/hud/src/playercard.rs b/crates/hud/src/playercard.rs index 3e88139..0142b4f 100644 --- a/crates/hud/src/playercard.rs +++ b/crates/hud/src/playercard.rs @@ -25,6 +25,9 @@ pub(crate) struct UiLocalVars { strings: HashMap, } +#[derive(Resource, Default)] +pub(crate) struct FrontendLocalVars(pub UiLocalVars); + impl UiLocalVars { pub(crate) fn set_int(&mut self, name: &str, value: i32) { self.strings.remove(name); diff --git a/crates/hud/src/plugin.rs b/crates/hud/src/plugin.rs index 6225769..e41f2b1 100644 --- a/crates/hud/src/plugin.rs +++ b/crates/hud/src/plugin.rs @@ -1,9 +1,8 @@ +use frame::ScopeApp; use std::time::Instant; use bevy::prelude::*; -use frame::{ - AppScreen, LaunchIdentity, LaunchReport, LifeFrontPublished, LifeStarted, MatchTornDown, UiDraw, -}; +use frame::{AppScreen, LaunchIdentity, LaunchReport, LifeFrontPublished, LifeStarted, UiDraw}; use net::{ClientSet, UpdatePhaseCensus}; use crate::blood::{BloodGpuJob, BloodOverlayLatch, HudRootVisible, update_blood_overlay}; @@ -36,10 +35,13 @@ pub struct HudPlugin; impl Plugin for HudPlugin { fn build(&self, app: &mut App) { + app.add_systems( + OnExit(frame::MatchScope::Live), + release_hud_publication.in_set(frame::ScopeSet::Release), + ); crate::gpu_list::register(app); let _ = crate::scorebar::milliseconds(); - app.init_resource::() - .init_resource::() + app.init_resource::() .init_resource::() .init_resource::() .init_resource::() @@ -47,19 +49,21 @@ impl Plugin for HudPlugin { .init_resource::() .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .init_resource::() .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) .init_resource::() .init_resource::() .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .init_resource::() .add_message::() .add_message::(); @@ -70,7 +74,6 @@ impl Plugin for HudPlugin { .add_systems( Update, ( - reset_match_hud_on_torn_down, hud_stamp_open, sync_games_root, sync_map_zone_tree, @@ -94,32 +97,34 @@ impl Plugin for HudPlugin { ( hud_surfaces_open, crate::surface::update_hud_surface, - update_reticle, + update_reticle.in_set(frame::InMatch), hud_stage_close::<0>, - update_iris, + update_iris.in_set(frame::InMatch), hud_stage_close::<1>, - update_blood_overlay, + update_blood_overlay.in_set(frame::InMatch), hud_stage_close::<2>, - update_flash_whiteout.after(frame::ScreenEffectsPublished), + update_flash_whiteout + .in_set(frame::InMatch) + .after(frame::ScreenEffectsPublished), hud_stage_close::<3>, - update_compass, + update_compass.in_set(frame::InMatch), hud_stage_close::<4>, - update_playercard, - update_weaponbar, + update_playercard.in_set(frame::InMatch), + update_weaponbar.in_set(frame::InMatch), hud_stage_close::<5>, ) .chain(), ( - update_scorebar, - update_splash, - update_killfeed, + update_scorebar.in_set(frame::InMatch), + update_splash.in_set(frame::InMatch), + update_killfeed.in_set(frame::InMatch), update_scoreboard, update_killcam_skip, - crate::emp_static::update, + crate::emp_static::update.in_set(frame::InMatch), update_mantle_hint, - crate::breath_hint::update, - crate::use_hint::update, - update_hud_elems, + crate::breath_hint::update.in_set(frame::InMatch), + crate::use_hint::update.in_set(frame::InMatch), + update_hud_elems.in_set(frame::InMatch), update_targetmap, crate::menus::update_script_menus, hud_stage_close::<7>, @@ -298,8 +303,9 @@ fn sync_frontend_camera( commands.spawn(( FrontendCamera, Camera3d::default(), - bevy::camera::CompositingSpace::Srgb, + frame::DisplayEncodedCamera, bevy::core_pipeline::tonemapping::Tonemapping::None, + bevy::render::view::DebandDither::Disabled, Camera { order: -1, ..default() @@ -771,24 +777,6 @@ fn hud_surfaces_close(stamp: Res, mut census: ResMut, - mut killfeed: ResMut, - mut splash: ResMut, - mut pings: ResMut, - mut cache: ResMut, - mut local_vars: ResMut, -) { - if torn.read().len() == 0 { - return; - } - *killfeed = KillfeedWindow::default(); - *splash = SplashSlots::default(); - *pings = crate::compass::CompassPingLatch::default(); - *cache = PlayerCardCache::default(); - *local_vars = UiLocalVars::default(); -} - fn flush_use_hint_tess( surface: Res, mut pass: ResMut, @@ -908,3 +896,17 @@ fn flush_breath_hint_tess( ); } } + +fn release_hud_publication( + mut latches: Query<&mut crate::gpu_list::GpuListLatch>, + mut frame: ResMut, + mut blood: ResMut, + mut flash: ResMut, +) { + for mut latch in &mut latches { + latch.mark_hidden(); + } + *frame = Default::default(); + *blood = BloodGpuJob::Hide; + *flash = FlashGpuJob::Hide; +} diff --git a/crates/hud/src/reticle.rs b/crates/hud/src/reticle.rs index a91815a..52f9034 100644 --- a/crates/hud/src/reticle.rs +++ b/crates/hud/src/reticle.rs @@ -237,7 +237,12 @@ pub(crate) fn update_reticle( side_size_v, ); - let center_size_v = assets.center_size as f32 * trans_scale; + let cook_size = if facts.cooks_grenade && ps.grenade_time_left > 0 { + (ps.grenade_time_left % 1000) as f32 / 100.0 + } else { + 0.0 + }; + let center_size_v = (assets.center_size as f32 + cook_size) * trans_scale; let alpha = calc_reticle_alpha( 1.0, CG_CROSSHAIR_ALPHA_DEFAULT, diff --git a/crates/launcher/src/main.rs b/crates/launcher/src/main.rs index 287ef0c..e1babc5 100644 --- a/crates/launcher/src/main.rs +++ b/crates/launcher/src/main.rs @@ -32,6 +32,11 @@ fn main() { } return; } + prepare_process_root().unwrap_or_else(|e| { + diag::exit_launch_error(&e); + }); + let artifacts = ensure_artifacts_dir().unwrap_or_else(|e| diag::exit_launch_error(&e)); + announce_log(diag::init_log(&artifacts)); let mut args = match updater::startup().unwrap_or_else(|e| diag::exit_launch_error(&e)) { Some(args) => args, None => return, @@ -40,11 +45,6 @@ fn main() { args.push("menu".into()); } bootstrap::bench::arm(); - prepare_process_root().unwrap_or_else(|e| { - diag::exit_launch_error(&e); - }); - let artifacts = ensure_artifacts_dir().unwrap_or_else(|e| diag::exit_launch_error(&e)); - announce_log(diag::init_log(&artifacts)); let (mode, acceptance, cheats) = bootstrap::parse_cli( args.into_iter() .map(|arg| arg.to_string_lossy().into_owned()), diff --git a/crates/net/src/authority/runtime.rs b/crates/net/src/authority/runtime.rs index 36d22ce..e12ce82 100644 --- a/crates/net/src/authority/runtime.rs +++ b/crates/net/src/authority/runtime.rs @@ -1,6 +1,7 @@ use bevy::app::{RunFixedMainLoop, RunFixedMainLoopSystems}; use bevy::ecs::system::SystemParam; use bevy::prelude::*; +use frame::ScopeApp; use std::collections::HashMap; use crate::authority::inbox::{AuthorityClock, ClientActionInbox, ClientCommandInbox}; @@ -8,10 +9,10 @@ use crate::client::predict::CmdSeq; use crate::client::presentation::presented::LocalPresentClient; use crate::gaps::NetIdentityGaps; use crate::policy::seat::ActiveKillcams; -use crate::schedule::{AuthoritySet, ClientSet}; +use crate::schedule::AuthoritySet; use crate::transport::archive::FrameArchive; use crate::transport::loopback_live::ListenLoopback; -use frame::{ExitLevelCalled, MatchTornDown, RuntimeRole, register_script_notify}; +use frame::{ExitLevelCalled, RuntimeRole, register_script_notify}; use sim::{ClientId, ClientLifecycle, DamageSource, SimEvent}; #[derive(Resource)] @@ -430,51 +431,19 @@ pub fn authority_should_tick( && hold.is_some_and(|h| !h.0) } -fn reset_authority_on_match_torn_down( - mut torn: MessageReader, - mut clock: ResMut, +fn release_authority( mut world: Option>, - mut loopback: Option>, - mut archive: Option>, mut inbox: ResMut, mut transactions: ActionTransactionScope, - mut samples: ResMut, mut input_gate: ResMut, - mut pending_input: ResMut, - mut pending_acks: ResMut, - mut pending_step: ResMut, - mut server_tick: ResMut, ) { - if torn.read().count() == 0 { - return; - } - *clock = AuthorityClock::default(); - inbox.clear(); transactions.open_new_scope(); - samples.0.clear(); *input_gate = AuthorityInputGate::default(); - pending_input.0 = None; - pending_acks.0.clear(); - pending_step.0 = None; - server_tick.0 = None; - if let Some(world) = world.as_mut() { world.0.shutdown_game(); } - if let Some(loopback) = loopback.as_mut() { - loopback.reset(); - } - if let Some(archive) = archive.as_mut() { - archive.clear(); - } - perf::sim_hold( - i64::from(world.as_ref().is_some_and(|world| world.0.is_running())), - world - .as_ref() - .map_or(0, |world| world.0.script_mover_count() as i64), - loopback.as_ref().map(|l| l.pending() as i64).unwrap_or(0), - ); + perf::sim_hold(0, 0, 0); } #[derive(Resource, Debug, Default)] @@ -1098,31 +1067,28 @@ pub fn authority_bookkeeping( } pub fn register_listen_runtime(app: &mut App) { register_script_notify(app); - if *app.world().resource::() != RuntimeRole::Client { - app.init_resource::(); - } app.init_resource::() .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) .init_resource::(); let role = *app.world().resource::(); if role != RuntimeRole::Dedicated { @@ -1132,17 +1098,34 @@ pub fn register_listen_runtime(app: &mut App) { begin_fixed_census .before(AuthoritySet::Advance) .before(frame::AuthorityEdge(0)), - advance_authority_clock.in_set(AuthoritySet::Advance), - ingress_authority.in_set(AuthoritySet::Ingress), - gather_authority_input.in_set(AuthoritySet::Gather), - step_authority.in_set(AuthoritySet::Step), - publish_server_tick.in_set(AuthoritySet::Snapshot), - fanout_loopback.in_set(AuthoritySet::Fanout), + advance_authority_clock + .in_set(frame::InMatch) + .in_set(AuthoritySet::Advance), + ingress_authority + .in_set(frame::InMatch) + .in_set(AuthoritySet::Ingress), + gather_authority_input + .in_set(frame::InMatch) + .in_set(AuthoritySet::Gather), + step_authority + .in_set(frame::InMatch) + .in_set(AuthoritySet::Step), + publish_server_tick + .in_set(frame::InMatch) + .in_set(AuthoritySet::Snapshot), + fanout_loopback + .in_set(frame::InMatch) + .in_set(AuthoritySet::Fanout), apply_connection_faults + .in_set(frame::InMatch) .in_set(AuthoritySet::Bookkeeping) .before(retire_departed_peers), - retire_departed_peers.in_set(AuthoritySet::Bookkeeping), - authority_bookkeeping.in_set(frame::AuthorityBookkeeping), + retire_departed_peers + .in_set(frame::InMatch) + .in_set(AuthoritySet::Bookkeeping), + authority_bookkeeping + .in_set(frame::InMatch) + .in_set(frame::AuthorityBookkeeping), end_fixed_census.after(AuthoritySet::Bookkeeping), ) .run_if(authority_should_tick), @@ -1154,19 +1137,35 @@ pub fn register_listen_runtime(app: &mut App) { begin_fixed_census .before(AuthoritySet::Advance) .before(frame::AuthorityEdge(0)), - advance_authority_clock.in_set(AuthoritySet::Advance), - ingress_authority.in_set(AuthoritySet::Ingress), - gather_authority_input.in_set(AuthoritySet::Gather), - step_authority.in_set(AuthoritySet::Step), - publish_server_tick.in_set(AuthoritySet::Snapshot), + advance_authority_clock + .in_set(frame::InMatch) + .in_set(AuthoritySet::Advance), + ingress_authority + .in_set(frame::InMatch) + .in_set(AuthoritySet::Ingress), + gather_authority_input + .in_set(frame::InMatch) + .in_set(AuthoritySet::Gather), + step_authority + .in_set(frame::InMatch) + .in_set(AuthoritySet::Step), + publish_server_tick + .in_set(frame::InMatch) + .in_set(AuthoritySet::Snapshot), fanout_loopback + .in_set(frame::InMatch) .in_set(AuthoritySet::Fanout) .after(crate::client::presentation::entities::sync_authority_entities), apply_connection_faults + .in_set(frame::InMatch) .in_set(AuthoritySet::Bookkeeping) .before(retire_departed_peers), - retire_departed_peers.in_set(AuthoritySet::Bookkeeping), - authority_bookkeeping.in_set(frame::AuthorityBookkeeping), + retire_departed_peers + .in_set(frame::InMatch) + .in_set(AuthoritySet::Bookkeeping), + authority_bookkeeping + .in_set(frame::InMatch) + .in_set(frame::AuthorityBookkeeping), end_fixed_census.after(AuthoritySet::Bookkeeping), ) .run_if(authority_should_tick), @@ -1179,10 +1178,8 @@ pub fn register_listen_runtime(app: &mut App) { .in_set(RunFixedMainLoopSystems::AfterFixedMainLoop), ); app.add_systems( - Update, - reset_authority_on_match_torn_down - .in_set(ClientSet::Load) - .after(frame::SessionSwapApplied), + OnExit(frame::MatchScope::Live), + release_authority.in_set(frame::ScopeSet::Release), ); register_authority_phase_seams(app); } diff --git a/crates/net/src/client/fire_verdict.rs b/crates/net/src/client/fire_verdict.rs index b44b6c2..3a669e1 100644 --- a/crates/net/src/client/fire_verdict.rs +++ b/crates/net/src/client/fire_verdict.rs @@ -16,7 +16,7 @@ pub enum PredictedFireStatus { #[derive(Default)] struct Verdicts { - world: frame::WorldGeneration, + world: frame::WorldStamp, results: HashMap<(sim::ClientId, sim::CommandSequence), (sim::FireCommandResult, Instant)>, } @@ -26,7 +26,7 @@ pub struct FireVerdictState(Arc>); impl FireVerdictState { pub(crate) fn apply( &self, - world: frame::WorldGeneration, + world: frame::WorldStamp, results: &[sim::FireCommandResult], ) -> Result<(), &'static str> { let mut state = self.0.lock().unwrap_or_else(|poison| poison.into_inner()); @@ -65,11 +65,7 @@ impl FireVerdictState { Ok(()) } - pub fn status( - &self, - world: frame::WorldGeneration, - cause: sim::FireCause, - ) -> PredictedFireStatus { + pub fn status(&self, world: frame::WorldStamp, cause: sim::FireCause) -> PredictedFireStatus { let state = self.0.lock().unwrap_or_else(|poison| poison.into_inner()); if state.world != world { return PredictedFireStatus::Pending; @@ -88,6 +84,6 @@ impl FireVerdictState { pub(crate) fn clear(&self) { let mut state = self.0.lock().unwrap_or_else(|poison| poison.into_inner()); state.results.clear(); - state.world = frame::WorldGeneration::default(); + state.world = frame::WorldStamp::default(); } } diff --git a/crates/net/src/client/input.rs b/crates/net/src/client/input.rs index 0dbdfed..35b348f 100644 --- a/crates/net/src/client/input.rs +++ b/crates/net/src/client/input.rs @@ -57,7 +57,6 @@ pub struct ClientActionInput { pub pad_sensitivity: f32, pub pad_acceleration: bool, pub pad_ads_sensitivity: f32, - /// Already applied to `pad_look`. pub pad_invert: bool, pub pad_turn_rate: [f32; 2], /// Pitch and yaw, degrees. diff --git a/crates/net/src/client/predict.rs b/crates/net/src/client/predict.rs index 6c9d948..2487fb8 100644 --- a/crates/net/src/client/predict.rs +++ b/crates/net/src/client/predict.rs @@ -410,20 +410,6 @@ impl ClientPrediction { Some((seq, cmd)) } - /// Drop the unacknowledged history so the next snapshot is adopted whole — - /// the same recovery `MAX_UNACKED_SNAPSHOTS` performs, on demand. The - /// pending send window is deliberately left alone: the authority consumes a - /// client's commands in sequence, so a hole punched in that window wedges it - /// for good. - pub fn force_resync(&mut self) { - self.snapshots_since_ack = 0; - if self.history.is_empty() { - return; - } - self.drop_history(); - self.metrics.forced_adopts += 1; - } - /// The floor lets stale acks for the dropped commands retire; read as /// `Broken` they would drop every later window too, for good. fn drop_history(&mut self) { diff --git a/crates/net/src/client/presentation/entities.rs b/crates/net/src/client/presentation/entities.rs index e66ef2c..ae7c01a 100644 --- a/crates/net/src/client/presentation/entities.rs +++ b/crates/net/src/client/presentation/entities.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use frame::ScopeApp; use playerstate_iw4::{ENTITYNUM_NONE, PLAYER_CORPSE_ENTITY_BASE}; use sim::{ClientId, PlayerCorpsePool, PlayerCorpseSlot}; @@ -13,6 +14,7 @@ pub const CLIENT_ENTITY_SLOT_COUNT: usize = ENTITYNUM_NONE as usize; #[derive(Component, Clone, Copy, Debug, PartialEq, Eq)] #[component(immutable)] +#[require(bevy::prelude::DespawnOnExit = bevy::prelude::DespawnOnExit(frame::MatchScope::Live))] pub struct CEntity { number: u16, client: Option, @@ -395,22 +397,25 @@ fn sync_snapshot_entities( } pub(crate) fn register_authority_entities(app: &mut App) { - app.init_resource::() + app.scoped::(frame::MatchScope::Live) .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) .add_systems( FixedUpdate, - sync_authority_entities.in_set(AuthoritySet::Fanout), + sync_authority_entities + .in_set(frame::InMatch) + .in_set(AuthoritySet::Fanout), ); } pub fn register_client_entities(app: &mut App) { - app.init_resource::() + app.scoped::(frame::MatchScope::Live) .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) .add_systems( Update, sync_client_entities + .in_set(frame::InMatch) .in_set(ClientSet::Reconcile) .after(crate::reconcile_prediction), ); diff --git a/crates/net/src/client/presentation/entity_event_dispatch.rs b/crates/net/src/client/presentation/entity_event_dispatch.rs index 793b29b..10dd769 100644 --- a/crates/net/src/client/presentation/entity_event_dispatch.rs +++ b/crates/net/src/client/presentation/entity_event_dispatch.rs @@ -3,6 +3,7 @@ use entity_iw4::{ EntityEventAction, EntityEventKind, UnsupportedEntityEvent, consume_entity_events, entity_event_action, packet_entity_uses_event_ring, }; +use frame::ScopeApp; use sim::{EntityEventPayload, EventSequence, Tick}; use std::collections::VecDeque; use std::time::{Duration, Instant}; @@ -29,7 +30,7 @@ pub enum EntityEventDomain { #[derive(Clone, Copy, Debug, PartialEq)] pub struct DispatchedEntityEvent { - pub world: frame::WorldGeneration, + pub world: frame::WorldStamp, pub domain: EntityEventDomain, pub timeline: u64, pub sequence: EventSequence, @@ -144,7 +145,7 @@ const TARGET_RETRY_WAIT: Duration = Duration::from_secs(5); #[derive(Clone, Copy, Debug)] struct DeferredTarget { - world: frame::WorldGeneration, + world: frame::WorldStamp, timeline: u64, client: sim::ClientId, life: sim::LifeSequence, @@ -340,7 +341,7 @@ fn dispatch_entity_events( number, entity, DispatchedEntityEvent { - world: *generation, + world: generation.stamp(), domain: EntityEventDomain::Ring, timeline, sequence: EventSequence(u32::try_from(ev.sequence).unwrap_or(0)), @@ -395,7 +396,7 @@ fn dispatch_entity_events( continue; } let number = record.payload.number; - if target.world != *generation || target.timeline != cursor.timeline { + if target.world != generation.stamp() || target.timeline != cursor.timeline { target_gaps.raise(NetGapCause::EventRetryScopeChanged { number, sequence: record.sequence, @@ -444,7 +445,7 @@ fn dispatch_entity_events( walk.last_number = record.payload.number; let dispatched = DispatchedEntityEvent { - world: *generation, + world: generation.stamp(), domain: EntityEventDomain::Snapshot, timeline: cursor.timeline, sequence: record.sequence, @@ -487,7 +488,7 @@ fn dispatch_entity_events( let client = sim::ClientId(u32::try_from(number).ok()?); let life = adopted.next()?.meta.for_client(client)?.life_sequence; Some(DeferredTarget { - world: *generation, + world: generation.stamp(), timeline: cursor.timeline, client, life, @@ -557,17 +558,17 @@ fn dispatch_owner_events( record.fire_cause.is_some_and(|cause| { cause.client == local.0 && Some(cause.life) == life - && verdicts.status(*generation, cause) + && verdicts.status(generation.stamp(), cause) != crate::PredictedFireStatus::Refused }) }) - .map(|record| (*generation, EntityEventDomain::Predicted, record)), + .map(|record| (generation.stamp(), EntityEventDomain::Predicted, record)), ); for record in prediction.0.take_owner_events() { if record.payload.fire_cause.is_some_and(|cause| { cause.client != local.0 || Some(cause.life) != life - || verdicts.status(*generation, cause) == crate::PredictedFireStatus::Refused + || verdicts.status(generation.stamp(), cause) == crate::PredictedFireStatus::Refused }) { continue; } @@ -581,7 +582,7 @@ fn dispatch_owner_events( local_number, entity, DispatchedEntityEvent { - world: *generation, + world: generation.stamp(), domain: EntityEventDomain::Predicted, timeline: cursor.timeline, sequence: record.sequence, @@ -730,8 +731,7 @@ fn set_ads_from_reset( } pub fn register_entity_event_dispatch(app: &mut App) { - app.init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) .init_resource::() .init_resource::() .init_resource::() @@ -741,9 +741,11 @@ pub fn register_entity_event_dispatch(app: &mut App) { Update, ( dispatch_entity_events + .in_set(frame::InMatch) .in_set(ClientSet::Reconcile) .after(crate::sync_client_entities), dispatch_owner_events + .in_set(frame::InMatch) .in_set(frame::OwnerEventsPublished) .after(crate::predict_local_move), ), diff --git a/crates/net/src/client/runtime.rs b/crates/net/src/client/runtime.rs index ddbbc21..7302bb9 100644 --- a/crates/net/src/client/runtime.rs +++ b/crates/net/src/client/runtime.rs @@ -1,9 +1,10 @@ +use frame::ScopeApp; use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use bevy::prelude::*; use entity_iw4::EntityEventKind; -use frame::{HasWorld, MatchTornDown, RuntimeRole}; +use frame::RuntimeRole; use playerstate_iw4::UserCmd; use crate::ServerTime; @@ -17,7 +18,6 @@ use crate::client::input::{ remote_control_axes, }; use crate::client::predict::{ClientPrediction, CmdSeq}; -use crate::client::presentation::entities::CEntityBirthCensus; use crate::client::presentation::presented::{ LocalPresentClient, PresentLocalCensus, PresentedSnapshot, interpolate_player_state, }; @@ -44,7 +44,7 @@ pub struct PendingPresentedEntityEvents { #[derive(Resource, Default)] pub struct PendingPelletFx( pub(crate) Vec<( - frame::WorldGeneration, + frame::WorldStamp, crate::EntityEventDomain, sim::PelletFxRecord, )>, @@ -57,15 +57,12 @@ impl PendingPelletFx { pub fn take( &mut self, ) -> Vec<( - frame::WorldGeneration, + frame::WorldStamp, crate::EntityEventDomain, sim::PelletFxRecord, )> { core::mem::take(&mut self.0) } - fn clear(&mut self) { - self.0.clear(); - } } fn collect_received_entity_events( @@ -570,7 +567,7 @@ pub fn reconcile_prediction( .0 .extend(tick.snapshot.meta.pellet_fx.drain(..).map(|record| { ( - *reliable.generation, + reliable.generation.stamp(), crate::EntityEventDomain::Snapshot, record, ) @@ -644,7 +641,7 @@ pub struct SvcFrameWriters<'w> { #[derive(Message, Clone, Debug)] pub struct FireCommandVerdicts { - pub world: frame::WorldGeneration, + pub world: frame::WorldStamp, pub results: Vec, } @@ -705,12 +702,13 @@ impl ReliableInbound<'_> { self.fail("FireResultRecipientMismatch: connection retired"); return; } - if let Err(reason) = self.fire_verdicts.apply(*self.generation, results) { + if let Err(reason) = self.fire_verdicts.apply(self.generation.stamp(), results) + { self.fail(reason); return; } self.fire.write(FireCommandVerdicts { - world: *self.generation, + world: self.generation.stamp(), results: results.clone(), }); } @@ -971,7 +969,6 @@ pub fn sample_client_input( let mouse = (actions.mouse_x, actions.mouse_y); actions.mouse_x = 0.0; actions.mouse_y = 0.0; - // remote_control_axes still reads pad_look and pad_move. actions.pad_look_delta = [0.0; 2]; mouse }); @@ -1221,6 +1218,11 @@ fn melee_charge_target( return None; } let local_team = snapshot.meta.for_client(local)?.client_state_team; + let range = if ps.perks[1] & playerstate_iw4::PERK1_EXTENDEDMELEE != 0 { + playerstate_iw4::EXTENDED_MELEE_TARGET_RANGE + } else { + AIM_AUTOMELEE_RANGE + }; let eye = [ ps.origin[0], ps.origin[1], @@ -1247,7 +1249,7 @@ fn melee_charge_target( other.origin[1] - ps.origin[1], other.origin[2] - ps.origin[2], ]; - if dot(delta, delta) > AIM_AUTOMELEE_RANGE * AIM_AUTOMELEE_RANGE { + if dot(delta, delta) > range * range { continue; } let center = [other.origin[0], other.origin[1], other.origin[2] + 36.0]; @@ -1309,9 +1311,7 @@ const LOCATION_PAD_AIM_DEFLECTION: f32 = 0.5; #[derive(Clone, Copy, Debug)] struct LocationPad { - /// Forward and right. movement: [f32; 2], - /// Right and up, with up map-up even when look is inverted. look: [f32; 2], look_deflection: f32, } @@ -1478,9 +1478,8 @@ pub fn enforce_client_work_limits( diag::warn!( Net, "input backlog: no authority ack in {BACKLOG_STALL_MS} ms \ - (stall {counted}/{BACKLOG_STALLS_BEFORE_FAIL}) — re-adopting" + (stall {counted}/{BACKLOG_STALLS_BEFORE_FAIL})" ); - prediction.0.force_resync(); } return; }; @@ -1722,7 +1721,7 @@ pub fn publish_presented( last_adopted: Res, proxy: Res, local: Res, - has_world: Option>, + has_world: Option>>, mut presented: ResMut, mut present_census: ResMut, phase: Option>, @@ -1747,7 +1746,7 @@ pub fn publish_presented( }; push_phase(trace, "Present"); - if has_world.is_some_and(|world| !world.0) { + if has_world.is_some_and(|world| *world.get() != frame::MatchScope::Live) { presented.clear(); *present_census = PresentLocalCensus { choice: Some("no_next"), @@ -1939,118 +1938,70 @@ pub fn publish_presented( ); } -pub fn reset_cgame_on_match_torn_down( - mut torn: MessageReader, - mut clock: ResMut, - mut active: ResMut, - mut join: ResMut, - mut adopted: ResMut, - mut received: ResMut, - (mut prediction, mut sends, mut signon, mut admission): ( - ResMut, - ResMut, - ResMut, - ResMut, - ), - mut client_clock: ResMut, - mut proxy: ResMut, +fn release_client( + mut actions: ResMut, mut presented: ResMut, - mut present_census: ResMut, - mut select: ResMut, - (mut entity_events, mut pellet_fx, mut entity_event_cursor, mut fire_verdicts, fire_state): ( - ResMut, - ResMut, - ResMut, - ResMut>, - Res, - ), - (mut reliable_ack, mut actions, mut events, mut scores): ( - ResMut, - Option>, - ResMut>, - ResMut, - ), - mut birth: Option>, - mut pacer: ResMut, + mut signon: ResMut, + mut admission: ResMut, + mut events: ResMut>, + mut fire_verdicts: ResMut>, + fire_state: Res, ) { - if torn.read().count() == 0 { - return; - } + actions.clear(); + presented.clear(); *signon = crate::SignonState::default(); *admission = crate::ClientAdmission::default(); - clock.reset(); - active.0 = false; - join.clear(); - *adopted = LastAdoptedSnapshot::default(); - received.0.clear(); - prediction.0.disarm(); - - *sends = PendingClientSends::default(); - *client_clock = ClientClock::default(); - *pacer = GameplaySendPacer::default(); - *proxy = RemoteProxyState::default(); - presented.clear(); - *present_census = PresentLocalCensus::default(); - *select = WeaponSelect::default(); - - *entity_events = PendingPresentedEntityEvents::default(); - pellet_fx.clear(); - *entity_event_cursor = crate::EntityEventCursor::default(); + events.clear(); fire_verdicts.clear(); fire_state.clear(); - - *reliable_ack = ClientReliableAck::default(); - events.clear(); - *scores = crate::Scoreboard::default(); - if let Some(actions) = actions.as_mut() { - actions.clear(); - } - if let Some(birth) = birth.as_mut() { - **birth = CEntityBirthCensus::default(); - } perf::cgame_hold("no_next", 0); } pub fn register_client_runtime(app: &mut App) { + app.add_systems( + OnExit(frame::MatchScope::Live), + release_client + .in_set(frame::ScopeSet::Release) + .after(crate::signon::release_match_protocol), + ); app.add_systems( Update, enforce_client_work_limits + .in_set(frame::InMatch) .before(predict_local_move) .after(reconcile_prediction), ); app.init_resource::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_message::() - .add_message::() + .scoped_message::(frame::MatchScope::Live) .init_resource::() - .add_message::() - .add_message::() - .init_resource::() - .init_resource::() - .init_resource::() - .add_message::() - .add_message::() - .add_message::() - .add_message::() - .add_message::() - .add_message::(); + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live); crate::client::frame_census::register_update_phase_census(app); app.configure_sets( Update, @@ -2067,13 +2018,13 @@ pub fn register_client_runtime(app: &mut App) { Update, ( advance_cls_realtime.in_set(ClientSet::Load), - reset_cgame_on_match_torn_down - .in_set(ClientSet::Load) - .after(frame::SessionSwapApplied), advance_cg_frame_clock + .in_set(frame::InMatch) .in_set(ClientSet::Reconcile) .after(reconcile_prediction), - receive_ticks.in_set(ClientSet::Receive), + receive_ticks + .in_set(frame::InMatch) + .in_set(ClientSet::Receive), crate::signon::drive_client_admission_facts .in_set(ClientSet::Receive) .after(receive_ticks), @@ -2081,23 +2032,41 @@ pub fn register_client_runtime(app: &mut App) { .in_set(ClientSet::Receive) .after(crate::signon::drive_client_admission_facts), crate::signon::drive_map_loaded + .in_set(frame::InMatch) .in_set(ClientSet::Receive) .after(crate::signon::drive_client_admission_facts), - reconcile_prediction.in_set(ClientSet::Reconcile), + reconcile_prediction + .in_set(frame::InMatch) + .in_set(ClientSet::Reconcile), flush_bootstrap_applied + .in_set(frame::InMatch) .in_set(ClientSet::Reconcile) .after(reconcile_prediction), apply_weapon_switch_requests + .in_set(frame::InMatch) .in_set(ClientSet::Input) .before(sample_client_input), - sample_client_input.in_set(ClientSet::Input), - predict_local_move.in_set(ClientSet::Predict), - send_pending_commands.in_set(ClientSet::Send), - publish_presented.in_set(ClientSet::Present), + sample_client_input + .in_set(frame::InMatch) + .in_set(ClientSet::Input), + predict_local_move + .in_set(frame::InMatch) + .in_set(ClientSet::Predict), + send_pending_commands + .in_set(frame::InMatch) + .in_set(ClientSet::Send), + publish_presented + .in_set(frame::InMatch) + .in_set(ClientSet::Present), ), ); } pub fn register_listen_prediction_arm(app: &mut App) { - app.add_systems(Update, arm_listen_prediction.in_set(ClientSet::Load)); + app.add_systems( + Update, + arm_listen_prediction + .in_set(frame::InMatch) + .in_set(ClientSet::Load), + ); } diff --git a/crates/net/src/lib.rs b/crates/net/src/lib.rs index c476bf1..840872c 100644 --- a/crates/net/src/lib.rs +++ b/crates/net/src/lib.rs @@ -174,6 +174,6 @@ pub use transport::wire::{WireError, WireReader, WireWriter}; pub use svc_script_audio::SvcScriptAudio; -pub const PROTOCOL_VERSION: u32 = 113; +pub const PROTOCOL_VERSION: u32 = 114; pub use client::fire_verdict::{FireVerdictState, PredictedFireStatus}; diff --git a/crates/net/src/plugin.rs b/crates/net/src/plugin.rs index 4df7321..c21fcb6 100644 --- a/crates/net/src/plugin.rs +++ b/crates/net/src/plugin.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use frame::ScopeApp; use crate::authority::inbox::{AuthorityClock, ClientActionInbox, ClientCommandInbox}; use crate::client::presentation::presented::{LocalPresentClient, PresentedSnapshot}; @@ -42,16 +43,22 @@ impl Plugin for NetPlugin { .world() .get_resource::() .is_some_and(crate::MasterLaunchIntent::enabled); + if !app.is_plugin_added::() { + app.add_plugins(frame::ScopePlugin); + } + app.add_systems( + OnExit(frame::MatchScope::Live), + crate::signon::release_match_protocol.in_set(frame::ScopeSet::Release), + ); app.init_resource::(); app.insert_resource(self.role) .init_resource::() .init_resource::() - .init_resource::() - .init_resource::(); + .init_resource::(); app.add_systems(Last, crate::time_scale::apply_time_scale); { configure_authority_sets(app); - app.init_resource::() + app.scoped::(frame::MatchScope::Live) .init_resource::() .init_resource::() .init_resource::() @@ -66,11 +73,11 @@ impl Plugin for NetPlugin { app.init_resource::() .init_resource::(); - app.init_resource::(); + app.scoped::(frame::MatchScope::Live); app.init_resource::(); if self.role == RuntimeRole::Client { app.init_resource::() - .init_resource::(); + .scoped::(frame::MatchScope::Live); } if !self.role.runs_authority() { app.init_resource::() diff --git a/crates/net/src/role_matrix.rs b/crates/net/src/role_matrix.rs index ab016d0..90f2ce7 100644 --- a/crates/net/src/role_matrix.rs +++ b/crates/net/src/role_matrix.rs @@ -1,5 +1,5 @@ use bevy::prelude::*; -use frame::{MatchTornDown, RuntimeRole}; +use frame::RuntimeRole; use crate::plugin::NetPlugin; use crate::schedule::{ @@ -67,7 +67,7 @@ fn install_client_probes(app: &mut App) { fn probe_app() -> App { let mut app = App::new(); app.add_plugins(bevy::time::TimePlugin); - app.add_message::(); + app.add_plugins(frame::ScopePlugin); app } @@ -105,6 +105,10 @@ pub fn client_toc_order() -> Result<(), String> { app.init_resource::(); configure_client_sets(&mut app); install_client_probes(&mut app); + app.world_mut() + .resource_mut::>() + .set(frame::MatchScope::Live); + app.world_mut().run_schedule(StateTransition); app.world_mut().run_schedule(Update); let trace = app.world().resource::().0.clone(); let expected = expected_client_trace(); @@ -196,6 +200,10 @@ pub fn worker_cmd_nested_in_present() -> Result<(), String> { push("W:cell_static").in_set(WorkerCmdSet::CellStatic), ); app.add_systems(Update, push("W:skin_model").in_set(WorkerCmdSet::SkinModel)); + app.world_mut() + .resource_mut::>() + .set(frame::MatchScope::Live); + app.world_mut().run_schedule(StateTransition); app.world_mut().run_schedule(Update); let trace = app.world().resource::().0.clone(); let send = trace @@ -246,6 +254,10 @@ pub fn role(role: RuntimeRole, expect_authority: bool, expect_client: bool) -> R app.world_mut().run_schedule(FixedUpdate); } if expect_client { + app.world_mut() + .resource_mut::>() + .set(frame::MatchScope::Live); + app.world_mut().run_schedule(StateTransition); app.world_mut().run_schedule(Update); } diff --git a/crates/net/src/session_core.rs b/crates/net/src/session_core.rs index 3bd4e0e..50695fe 100644 --- a/crates/net/src/session_core.rs +++ b/crates/net/src/session_core.rs @@ -843,18 +843,11 @@ impl Default for ClientMatchCore { } impl ClientMatchCore { - pub fn apply_teardown(&mut self, fact: frame::MatchTornDown) { - if !match_key_boundary_applies(fact.match_key, self.match_key) - || self - .installed - .is_some_and(|load| fact.world_generation.0 != Some(load.local_load_request_id)) - { - return; - } + pub fn retire_match(&mut self) { *self = Self { session_open: self.session_open, - retired_match: (!fact.match_key.is_none()) - .then_some(fact.match_key) + retired_match: (!self.match_key.is_none()) + .then_some(self.match_key) .or(self.retired_match), ..Self::default() }; diff --git a/crates/net/src/signon.rs b/crates/net/src/signon.rs index e8c26f8..007d661 100644 --- a/crates/net/src/signon.rs +++ b/crates/net/src/signon.rs @@ -1,10 +1,10 @@ +use bevy::prelude::DetectChanges; use bevy::prelude::Resource; -use frame::{AdmissionKey, MatchInstalled, MatchKey, MatchTornDown, RuntimeRole}; +use frame::{AdmissionKey, InstalledMatch, MatchKey, RuntimeRole}; use sim::ClientId; use crate::session_core::{ ClientMatchCore, FailStage, HostWorldReady, SessionFail, confirm_keyed_world_ready, - format_session_transition, }; use crate::transport::master::{MasterBridge, MasterBridgeState}; use crate::transport::protocol::HandshakeReject; @@ -257,23 +257,13 @@ pub fn live_match_key(bridge: Option<&MasterBridge>) -> MatchKey { } } -fn owner_match_key(bridge: Option<&MasterBridge>) -> MatchKey { - bridge - .map(|bridge| bridge.state().identity().match_key()) - .unwrap_or(MatchKey::NONE) -} - pub fn drive_client_admission_facts( mut admission: bevy::prelude::ResMut, - mut installed: bevy::prelude::MessageReader, - mut torn: bevy::prelude::MessageReader, + installed: Option>, link: Option>, bridge: Option>, ) { - for fact in torn.read() { - admission.core.apply_teardown(*fact); - } - for fact in installed.read() { + if let Some(fact) = installed.filter(|fact| fact.is_added()) { admission.core.apply_start(fact.load_key.match_key); admission.core.apply_install(fact.load_key); } @@ -393,7 +383,7 @@ pub fn drive_signon( pub fn drive_map_loaded( bridge: Option>, descriptor: Option>, - has_world: Option>, + has_world: Option>>, admission: Option>, signon: bevy::prelude::Res, hold: Option>, @@ -405,7 +395,7 @@ pub fn drive_map_loaded( let Some(bridge) = bridge else { return; }; - let installed = has_world.is_some_and(|world| world.0); + let installed = has_world.is_some_and(|world| *world.get() == frame::MatchScope::Live); let hold = hold.is_some_and(|hold| hold.0); let live = live_match_key(Some(&bridge)); let belongs = admission.as_ref().is_some_and(|admission| { @@ -520,57 +510,36 @@ pub fn drive_map_loaded( } pub fn drive_match_boundary( - mut torn: bevy::prelude::MessageReader, mut hub: Option>, mut link: Option>, bridge: Option>, mut live: bevy::prelude::Local, ) { - let live_key = owner_match_key(bridge.as_deref()); - let live_epoch = live_key.match_epoch; - let mut reset = false; - let mut end_match = false; - let mut torn_key = frame::MatchKey::NONE; - for fact in torn.read() { - if !crate::session_core::match_key_boundary_applies(fact.match_key, live_key) { - diag::info!( - Net, - "session-transition {}", - format_session_transition( - fact.match_key.session_id, - None, - fact.match_epoch, - None, - None, - None, - None, - &format!("live={live_epoch}"), - "torn", - "ignored", - None, - "stale world", - ) - ); - continue; - } - reset = true; - torn_key = fact.match_key; - end_match = fact.reason.keeps_session(); - } let in_match = bridge .as_ref() .map(|bridge| bridge.state().in_match()) .unwrap_or(*live); - if reset || (*live && !in_match) { + if *live && !in_match { if let Some(hub) = hub.as_mut() { hub.reset_match(); } if let Some(link) = link.as_mut() { link.reset_match(); } - if end_match && let Some(bridge) = bridge.as_ref() { - bridge.report_match_ended(torn_key); - } } *live = in_match; } + +pub(crate) fn release_match_protocol( + mut admission: bevy::prelude::ResMut, + mut hub: Option>, + mut link: Option>, +) { + admission.core.retire_match(); + if let Some(hub) = hub.as_mut() { + hub.reset_match(); + } + if let Some(link) = link.as_mut() { + link.reset_match(); + } +} diff --git a/crates/net/src/transport/master.rs b/crates/net/src/transport/master.rs index 6a65692..1b2164c 100644 --- a/crates/net/src/transport/master.rs +++ b/crates/net/src/transport/master.rs @@ -1373,7 +1373,7 @@ fn open_hosted_epoch_when_world_is_live( bridge: Option>, intent: Res, admission: Res, - has_world: Option>, + has_world: Option>>, launch: Option>, hold: Option>, mut sent: Local>, @@ -1385,7 +1385,7 @@ fn open_hosted_epoch_when_world_is_live( return; }; if !config.auto_start_map - || !has_world.is_some_and(|world| world.0) + || !has_world.is_some_and(|world| *world.get() == frame::MatchScope::Live) || hold.is_some_and(|hold| hold.0) { return; diff --git a/crates/net/src/transport/meta_wire.rs b/crates/net/src/transport/meta_wire.rs index bd1a63b..ba2b5e4 100644 --- a/crates/net/src/transport/meta_wire.rs +++ b/crates/net/src/transport/meta_wire.rs @@ -418,6 +418,7 @@ pub fn encode_snapshot_meta_body( let mut sizes = SnapshotMetaSectionBytes::default(); let mut mark = out.len(); out.put_u8(phase_tag(meta.phase)); + out.put_u32(meta.round_serial); out.put_u32(meta.match_elapsed_ms); let (prematch_tag, elapsed_ms) = match meta.prematch { gamemode_iw4::PrematchStep::Waiting { elapsed_ms } => (0, elapsed_ms), @@ -502,6 +503,7 @@ pub(super) fn decode_snapshot_meta_body( remaining_after: &mut [usize; META_SEGMENTS], ) -> Result { let phase = phase_from_tag(input.get_u8()?)?; + let round_serial = input.get_u32()?; let match_elapsed_ms = input.get_u32()?; let prematch_tag = input.get_u8()?; let elapsed_ms = input.get_u32()?; @@ -569,6 +571,7 @@ pub(super) fn decode_snapshot_meta_body( Ok(DecodedSnapshotMeta { body: SnapshotMeta { phase, + round_serial, match_elapsed_ms, prematch, score_limit, diff --git a/crates/playerstate_iw4/src/lib.rs b/crates/playerstate_iw4/src/lib.rs index 5346476..f7b3f94 100644 --- a/crates/playerstate_iw4/src/lib.rs +++ b/crates/playerstate_iw4/src/lib.rs @@ -14,11 +14,13 @@ pub use chrome::{ mode0_from_kill_cam_entity, third_person_in_killcam, }; pub use playerstate::{ - AnimPair, BREATH_GASP_TIME_MS, BREATH_HOLD_TIME_MS, ENTITYNUM_NONE, PERK_COLDBLOODED, - PERK_EXTRA_BREATH_MS, PERK_FASTMANTLE, PERK_HEARTBREAKER, PERK_HOLDBREATH, PERK_PISTOLDEATH, - PERK_QUICKDRAW, PERK_QUICKDRAW_SPEED_SCALE, PERK_QUIETER, PERK_SCAVENGER, PERK1_SPYGAME, - PlayerState, breath_hold_time_ms, eflags, get_viewmodel_weapon_index, mantle_flags, - other_flags, pm_flags, weap_flags, + AnimPair, BREATH_GASP_TIME_MS, BREATH_HOLD_TIME_MS, ENTITYNUM_NONE, + EXTENDED_MELEE_TARGET_RANGE, PERK_COLDBLOODED, PERK_EXTRA_BREATH_MS, PERK_FASTMANTLE, + PERK_HEARTBREAKER, PERK_HOLDBREATH, PERK_PISTOLDEATH, PERK_QUICKDRAW, + PERK_QUICKDRAW_SPEED_SCALE, PERK_QUIETER, PERK_SCAVENGER, PERK_SELECTIVEHEARING, + PERK1_EXTENDEDMELEE, PERK1_FASTSPRINTRECOVERY, PERK1_SPYGAME, PlayerState, + SPRINT_RECOVERY_MULTIPLIER, breath_hold_time_ms, eflags, get_viewmodel_weapon_index, + mantle_flags, other_flags, pm_flags, weap_flags, }; pub use seat::{ HITSCAN_KILL_CAM_ENTITY, SeatFocus, apply_killcam_seat, rebase_archived_timers, diff --git a/crates/playerstate_iw4/src/playerstate.rs b/crates/playerstate_iw4/src/playerstate.rs index d8b912f..183f4d0 100644 --- a/crates/playerstate_iw4/src/playerstate.rs +++ b/crates/playerstate_iw4/src/playerstate.rs @@ -370,6 +370,11 @@ pub const PERK_QUICKDRAW_SPEED_SCALE: f32 = 1.5; pub const PERK_COLDBLOODED: u32 = 1 << 27; pub const PERK_HEARTBREAKER: u32 = 1 << 28; +pub const PERK_SELECTIVEHEARING: u32 = 1 << 29; +pub const PERK1_FASTSPRINTRECOVERY: u32 = 1 << 2; +pub const PERK1_EXTENDEDMELEE: u32 = 1 << 3; +pub const EXTENDED_MELEE_TARGET_RANGE: f32 = 192.0; +pub const SPRINT_RECOVERY_MULTIPLIER: f32 = 0.6; pub const PERK1_SPYGAME: u32 = 1; diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index 86962a0..9101787 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -21,7 +21,7 @@ net = { path = "../net" } assets.workspace = true serde.workspace = true serde_json.workspace = true -naga = { version = "29", features = ["wgsl-in"] } +naga = { version = "30", features = ["wgsl-in"] } perf.workspace = true [lints] diff --git a/crates/render/src/diag/acceptance.rs b/crates/render/src/diag/acceptance.rs index 1d6eae3..03caa50 100644 --- a/crates/render/src/diag/acceptance.rs +++ b/crates/render/src/diag/acceptance.rs @@ -6,7 +6,6 @@ use bevy::prelude::*; use bevy::render::view::screenshot::{Screenshot, save_to_disk}; use bevy::time::Real; use bevy::window::PresentMode; -use frame::HasWorld; use net::ClientSet; use serde::{Deserialize, Serialize}; @@ -175,8 +174,10 @@ pub fn register_acceptance_systems(app: &mut App) { app.init_resource::().add_systems( Update, ( - acceptance_prepare, - acceptance_sample.after(apply_dpvs_cull), + acceptance_prepare.in_set(frame::InMatch), + acceptance_sample + .in_set(frame::InMatch) + .after(apply_dpvs_cull), acceptance_capture_and_exit, ) .chain() @@ -187,14 +188,14 @@ pub fn register_acceptance_systems(app: &mut App) { fn acceptance_prepare( run: Res, mut state: ResMut, - has_world: Res, + has_world: Res>, scene: Res, mut sim_cam: Option>, ) { if state.phase != Phase::WaitWorld { return; } - if !has_world.0 { + if !(*has_world.get() == frame::MatchScope::Live) { return; } if scene.intermission_view.is_none() { diff --git a/crates/render/src/diag/capture.rs b/crates/render/src/diag/capture.rs index 6fd660a..02c5055 100644 --- a/crates/render/src/diag/capture.rs +++ b/crates/render/src/diag/capture.rs @@ -9,7 +9,7 @@ use bevy::prelude::*; use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat}; use bevy::render::view::screenshot::{Screenshot, ScreenshotCaptured}; use bevy::tasks::IoTaskPool; -use frame::{AppScreen, HasWorld}; +use frame::AppScreen; use render_frontend::prepare::scene::cull::DpvsFrameStats; @@ -299,7 +299,7 @@ pub(crate) fn capture_frame( mut queue: ResMut, screen: Option>, loading: Option>, - has_world: Option>, + has_world: Option>>, stats: Option>, working: Option>, mut exit: MessageWriter, @@ -312,7 +312,9 @@ pub(crate) fn capture_frame( let facts = CaptureFrameFacts { screen: screen.map(|s| *s).unwrap_or_default(), loading_overlay: loading.is_some(), - has_world: has_world.map(|w| w.0).unwrap_or(false), + has_world: has_world + .map(|w| *w.get() == frame::MatchScope::Live) + .unwrap_or(false), submitted_batches, g0_world, }; diff --git a/crates/render/src/extract.rs b/crates/render/src/extract.rs index e37fd72..8f87338 100644 --- a/crates/render/src/extract.rs +++ b/crates/render/src/extract.rs @@ -194,8 +194,8 @@ pub fn seal_render_frame( let generation = runtime.catalog.generation_id(); let world_generation = world_generation .as_ref() - .map(|generation| **generation) - .unwrap_or(frame::WorldGeneration(None)); + .map(|generation| generation.stamp()) + .unwrap_or(frame::WorldStamp(None)); let Some(products) = products.as_ref() else { insert_empty_colour(&mut commands); return; @@ -684,7 +684,7 @@ fn frame_submission_matches( inputs: Option<&render_frontend::assemble::drawsurf::FrameAssemblyInputs>, snapshot: &render_frame::FrameProductsSnapshot, generation: render_frontend::assemble::drawsurf::MaterialGenerationId, - world_generation: frame::WorldGeneration, + world_generation: frame::WorldStamp, ) -> bool { inputs.is_some_and(|inputs| { inputs.frame_id == snapshot.frame_id diff --git a/crates/render/src/plugin.rs b/crates/render/src/plugin.rs index c0764d7..69e7450 100644 --- a/crates/render/src/plugin.rs +++ b/crates/render/src/plugin.rs @@ -29,8 +29,8 @@ impl Plugin for RenderPlugin { .add_systems( Update, ( - publish_overhead_posed_players, - publish_overhead_posed_models, + publish_overhead_posed_players.in_set(frame::InMatch), + publish_overhead_posed_models.in_set(frame::InMatch), ) .after(frame::WorkerCmdSet::SkinModel) .in_set(hud::OverheadPosedPlayerFramePublished) diff --git a/crates/render_anim/src/anim/fpv.rs b/crates/render_anim/src/anim/fpv.rs index 679513a..0df77ce 100644 --- a/crates/render_anim/src/anim/fpv.rs +++ b/crates/render_anim/src/anim/fpv.rs @@ -150,6 +150,7 @@ pub struct FpvAuthoritySample { pub last_weapon_hand: i32, pub perks0: u32, + pub perks1: u32, pub clip_ammo: Option, @@ -320,7 +321,7 @@ pub struct FpvNotetracks { impl FpvNotetracks { pub fn into_audio_batch( self, - generation: frame::WorldGeneration, + generation: frame::WorldStamp, timeline: u64, client: sim::ClientId, life: sim::LifeSequence, @@ -401,7 +402,9 @@ pub fn tick_equipped_fpv_with_extra_events( } } if let Some(sample) = sample { - equipped.controller.set_predicted_perks(sample.perks0); + equipped + .controller + .set_predicted_perks(sample.perks0, sample.perks1); } apply_present_events(&mut equipped.controller, &events); if let Some(sample) = sample { @@ -442,7 +445,7 @@ pub fn tick_equipped_fpv_with_extra_events( equipped.controller.apply_ads_overlay_frame(ads_frac); if let (Some(left), Some(sample)) = (equipped.left.as_mut(), sample) { if sample.last_weapon_hand == 1 { - left.set_predicted_perks(sample.perks0); + left.set_predicted_perks(sample.perks0, sample.perks1); match present.observe_weap_anim_secondary_edge(sample.weap_anim_secondary) { WeapAnimEdge::Unchanged => {} WeapAnimEdge::Idle => { diff --git a/crates/render_anim/src/anim/remote_animation.rs b/crates/render_anim/src/anim/remote_animation.rs index 0e2b748..66af2ed 100644 --- a/crates/render_anim/src/anim/remote_animation.rs +++ b/crates/render_anim/src/anim/remote_animation.rs @@ -31,6 +31,14 @@ pub struct RemoteBodyTrees { } impl RemoteBodyTrees { + pub fn retained_bindings(&self) -> usize { + self.by_ent.len() + } + + pub fn tree_storage_bytes(&self) -> usize { + self.by_ent.capacity() * std::mem::size_of::<(u32, PersistentRemoteTree)>() + } + pub fn get(&self, persist_key: u32) -> Option<&PersistentRemoteTree> { self.by_ent.get(&persist_key) } diff --git a/crates/render_anim/src/anim/viewmodel_controller.rs b/crates/render_anim/src/anim/viewmodel_controller.rs index 09b9051..1e29af9 100644 --- a/crates/render_anim/src/anim/viewmodel_controller.rs +++ b/crates/render_anim/src/anim/viewmodel_controller.rs @@ -70,6 +70,7 @@ pub struct ViewmodelController { last_ads_frac: f32, predicted_perks0: u32, + predicted_perks1: u32, } impl ViewmodelController { @@ -87,6 +88,7 @@ impl ViewmodelController { action_remaining: None, last_ads_frac: 0.0, predicted_perks0: 0, + predicted_perks1: 0, }; this.set_weight( WeaponAnimSlot::Idle, @@ -115,8 +117,9 @@ impl ViewmodelController { self.state } - pub fn set_predicted_perks(&mut self, perks0: u32) { + pub fn set_predicted_perks(&mut self, perks0: u32, perks1: u32) { self.predicted_perks0 = perks0; + self.predicted_perks1 = perks1; } pub fn weapon_name(&self) -> &str { @@ -483,7 +486,13 @@ impl ViewmodelController { } } - fn perk_scaled_reload_timer(&self, state: WeaponState, timer_ms: Option) -> Option { + fn perk_scaled_action_timer(&self, state: WeaponState, timer_ms: Option) -> Option { + if state == WeaponState::SprintOut + && self.predicted_perks1 & playerstate_iw4::PERK1_FASTSPRINTRECOVERY != 0 + { + return timer_ms + .map(|t| ((t as f32) * playerstate_iw4::SPRINT_RECOVERY_MULTIPLIER) as i32); + } let reload_family = matches!( state, WeaponState::Reloading { .. } | WeaponState::ReloadStarting | WeaponState::ReloadEnding @@ -517,7 +526,7 @@ impl ViewmodelController { return EventResult::IgnoredMissingClip(slot); }; let duration = clip.duration(); - let timer_ms = self.perk_scaled_reload_timer(state, timer_ms); + let timer_ms = self.perk_scaled_action_timer(state, timer_ms); self.zero_dispatch_slots(ACTION_GOAL_TIME_SECS); self.tree .set_clip(slot.index(), Arc::clone(&clip)) diff --git a/crates/render_anim/src/gaps.rs b/crates/render_anim/src/gaps.rs index 9ba4443..a15cac9 100644 --- a/crates/render_anim/src/gaps.rs +++ b/crates/render_anim/src/gaps.rs @@ -1,4 +1,5 @@ use core::fmt; +use frame::ScopeApp; use bevy::prelude::*; use diag::gap::{self as ledger, Gap as _, GapLedger}; @@ -330,6 +331,8 @@ pub(crate) fn report_render_gaps(gaps: Res) { } pub fn register_render_gaps(app: &mut App) { - app.init_resource::() - .add_systems(Update, report_render_gaps.in_set(net::ClientSet::Diag)); + app.init_app::().add_systems( + Update, + (report_render_gaps.in_set(net::ClientSet::Diag)).in_set(frame::InMatch), + ); } diff --git a/crates/render_anim/src/occupancy/dyn_ent.rs b/crates/render_anim/src/occupancy/dyn_ent.rs index 4319ef6..237090d 100644 --- a/crates/render_anim/src/occupancy/dyn_ent.rs +++ b/crates/render_anim/src/occupancy/dyn_ent.rs @@ -5,6 +5,7 @@ use dpvs_iw4::{ link_dyn_ent_primary_light_bit, unfilter_dyn_ent_from_cells, unlink_dyn_ent_from_primary_lights, }; +use frame::ScopeApp; use crate::anim::body_frustum::AdmittedCellVis; use crate::anim::xmodel_pose::PosedModelSurface; @@ -146,26 +147,37 @@ impl DynEntPoseProduct { pub fn register_dyn_ent_systems(app: &mut App) { dyn_ent_phys::register_dyn_ent_phys(app); - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, - ( - link_dyn_ent_cells.after(dyn_ent_phys::step_phys_world0), + (( + link_dyn_ent_cells + .in_set(frame::InMatch) + .after(dyn_ent_phys::step_phys_world0), cull_dyn_ent_cell_models + .in_set(frame::InMatch) .after(link_dyn_ent_cells) .after(frame::WorkerCmdSet::CellStatic), - pose_dyn_ents.after(cull_dyn_ent_cell_models), - append_dynent_draws.after(pose_dyn_ents), + pose_dyn_ents + .in_set(frame::InMatch) + .after(cull_dyn_ent_cell_models), + append_dynent_draws + .in_set(frame::InMatch) + .after(pose_dyn_ents), ) .in_set(frame::RenderSet::Anim) - .in_set(frame::WorkerCmdSet::CellDynModel), + .in_set(frame::WorkerCmdSet::CellDynModel)) + .in_set(frame::InMatch), ) .add_systems( Update, - dyn_ent_phys::step_phys_world0.in_set(frame::WorkerCmdSet::Physics), + (dyn_ent_phys::step_phys_world0 + .in_set(frame::InMatch) + .in_set(frame::WorkerCmdSet::Physics)) + .in_set(frame::InMatch), ); } diff --git a/crates/render_anim/src/occupancy/dyn_ent_phys.rs b/crates/render_anim/src/occupancy/dyn_ent_phys.rs index 32ca162..82d095f 100644 --- a/crates/render_anim/src/occupancy/dyn_ent_phys.rs +++ b/crates/render_anim/src/occupancy/dyn_ent_phys.rs @@ -1,3 +1,4 @@ +use frame::ScopeApp; use std::collections::HashMap; use std::sync::Arc; @@ -60,9 +61,9 @@ struct DynEntPhysBody { } pub(crate) fn register_dyn_ent_phys(app: &mut App) { - app.init_resource::() - .init_resource::() - .add_message::(); + app.staged::(frame::MatchScope::Live) + .staged::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live); } pub fn step_phys_world0( @@ -202,10 +203,6 @@ fn integrate_body(body: &mut DynEntPhysBody, clip: &ClipCollision, dt: f32) { CONTENTS_SOLID, ); if hit.allsolid || hit.startsolid { - // The hull is the model's whole bounds (a potted plant's leaves with it), so a prop where - // the map placed it can already overlap a wall, pillar or shelf. Lifting it out a unit a - // step floated such props up until the hull cleared the obstacle, where they fell asleep - // in the air; the map's placement stands instead and the prop settles where it is. body.vel = Vec3::ZERO; body.on_ground = true; return; diff --git a/crates/render_anim/src/occupancy/fpv_present.rs b/crates/render_anim/src/occupancy/fpv_present.rs index 335019e..0896cfd 100644 --- a/crates/render_anim/src/occupancy/fpv_present.rs +++ b/crates/render_anim/src/occupancy/fpv_present.rs @@ -1,3 +1,4 @@ +use frame::ScopeApp; use std::sync::Arc; use anim_iw4::{DOBJ_RADIUS_PARENT_ROOT, compute_bounds_radius}; @@ -306,6 +307,7 @@ pub fn spawn_pending_fpv( parent.spawn(( FpvViewmodel, FpvPlacementRoot, + DespawnOnExit(frame::MatchScope::Live), Transform::IDENTITY, Visibility::Visible, )); @@ -755,6 +757,7 @@ pub fn tick_fpv_viewmodel( }) .unwrap_or(0), perks0: ps.map(|p| p.perks[0]).unwrap_or(0), + perks1: ps.map(|p| p.perks[1]).unwrap_or(0), clip_ammo: clip_ammo(0), left_clip_ammo: clip_ammo(1), }), @@ -782,7 +785,11 @@ pub fn tick_fpv_viewmodel( None }; let weapon_id = session.weapon_id; - let held = if session.parent_weapon != 0 { + let held = if session.parent_weapon != 0 + && table + .and_then(|table| table.facts_of(weapon_id)) + .is_some_and(|facts| facts.is_alternate()) + { session.parent_weapon } else { weapon_id @@ -826,7 +833,7 @@ pub fn tick_fpv_viewmodel( && let Some(meta) = snapshot.meta.for_client(local.0) { pending_notes.batch = Some(notetracks.into_audio_batch( - *generation, + generation.stamp(), events.timeline(), local.0, meta.life_sequence, @@ -1234,8 +1241,8 @@ pub fn apply_fpv_placement( *transform = placed; } -fn fpv_spawn_queued(pending: Res) -> bool { - pending.0.is_some() +fn fpv_spawn_queued(pending: Option>) -> bool { + pending.is_some_and(|pending| pending.0.is_some()) } fn flush_fpv_spawn(world: &mut World) { @@ -1252,60 +1259,81 @@ fn publish_fpv_notetracks( } pub fn register_fpv_present_systems(app: &mut App) { - app.init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) + .init_app::() .add_systems( Update, - super::screen_effects::update + (super::screen_effects::update + .in_set(frame::InMatch) .in_set(frame::ScreenEffectsPublished) .in_set(LifeFrontPublished) - .after(reset_view_kick_on_life_started), + .after(reset_view_kick_on_life_started)) + .in_set(frame::InMatch), ) - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .init_app::() .add_systems( Update, - reset_view_kick_on_life_started.in_set(LifeFrontPublished), + (reset_view_kick_on_life_started + .in_set(frame::InMatch) + .in_set(LifeFrontPublished)) + .in_set(frame::InMatch), ) .add_systems( Update, - occupy_fpv_scene + (occupy_fpv_scene + .in_set(frame::InMatch) .after(PresentedPublished) .in_set(render_scene::GfxSceneAdd) - .in_set(AnimSceneSubmit), + .in_set(AnimSceneSubmit)) + .in_set(frame::InMatch), ) .add_systems( Update, - ( - tick_session_view_kick.after(reset_view_kick_on_life_started), - sync_camera_from_presented.after(tick_session_view_kick), + (( + tick_session_view_kick + .in_set(frame::InMatch) + .after(reset_view_kick_on_life_started), + sync_camera_from_presented + .in_set(frame::InMatch) + .after(tick_session_view_kick), spawn_pending_fpv + .in_set(frame::InMatch) .run_if(fpv_spawn_queued) .after(sync_camera_from_presented), flush_fpv_spawn.after(spawn_pending_fpv), - tick_fpv_viewmodel.after(flush_fpv_spawn), + tick_fpv_viewmodel + .in_set(frame::InMatch) + .after(flush_fpv_spawn), skin_fpv_geometry + .in_set(frame::InMatch) .after(tick_fpv_viewmodel) .in_set(FpvGeometrySet), - publish_fpv_notetracks.after(tick_fpv_viewmodel), + publish_fpv_notetracks + .in_set(frame::InMatch) + .after(tick_fpv_viewmodel), apply_fpv_placement + .in_set(frame::InMatch) .after(tick_fpv_viewmodel) .before(WorkerCmdSet::CellSceneEnt), // Neither placement nor bone publication reads a vertex. stamp_fpv_placement_matrix + .in_set(frame::InMatch) .after(apply_fpv_placement) .in_set(FpvPlacementSet), publish_fpv_dobj_pose + .in_set(frame::InMatch) .after(stamp_fpv_placement_matrix) .after(crate::anim::dobj_pose::begin_dobj_pose_frame), ) - .in_set(ClientSet::Present), + .in_set(ClientSet::Present)) + .in_set(frame::InMatch), ); } diff --git a/crates/render_anim/src/occupancy/held_sync.rs b/crates/render_anim/src/occupancy/held_sync.rs index 711bffa..af74293 100644 --- a/crates/render_anim/src/occupancy/held_sync.rs +++ b/crates/render_anim/src/occupancy/held_sync.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use frame::ScopeApp; use frame::{LaunchReport, LifeEnded, LifeFrontPublished, LifeStarted, ViewSubject}; use net::{ClientSet, LocalPresentClient, PresentedSnapshot}; @@ -225,8 +226,20 @@ pub fn sync_fpv_status_to_probe( } pub(crate) fn register_held_sync_systems(app: &mut App) { - app.add_message::() - .add_message::() - .add_systems(Update, sync_fpv_to_held_weapon.in_set(LifeFrontPublished)) - .add_systems(Update, sync_fpv_status_to_probe.in_set(ClientSet::Diag)); + app.scoped_message::(frame::MatchScope::Live) + .scoped_message::(frame::MatchScope::Live) + .add_systems( + Update, + (sync_fpv_to_held_weapon + .in_set(frame::InMatch) + .in_set(LifeFrontPublished)) + .in_set(frame::InMatch), + ) + .add_systems( + Update, + (sync_fpv_status_to_probe + .in_set(frame::InMatch) + .in_set(ClientSet::Diag)) + .in_set(frame::InMatch), + ); } diff --git a/crates/render_anim/src/occupancy/item.rs b/crates/render_anim/src/occupancy/item.rs index cf3f876..c2d8736 100644 --- a/crates/render_anim/src/occupancy/item.rs +++ b/crates/render_anim/src/occupancy/item.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use frame::ScopeApp; use crate::anim::scene_submission::{AnimDObjSceneSkels, AnimDObjSceneSubmission, AnimSceneSubmit}; use crate::anim::xmodel_pose::PosedModelSurface; @@ -74,26 +75,32 @@ fn item_scene_slot(scene: &render_scene::GfxScene, entnum: u32) -> ItemSceneSlot } pub fn register_item_systems(app: &mut App) { - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, - occupy_item_scene_ents + (occupy_item_scene_ents + .in_set(frame::InMatch) .in_set(frame::RenderSet::Anim) .before(frame::WorkerCmdSet::CellDynModel) .in_set(render_scene::GfxSceneAdd) - .in_set(AnimSceneSubmit), + .in_set(AnimSceneSubmit)) + .in_set(frame::InMatch), ) .add_systems( Update, - (pose_items, append_item_draws) + (( + pose_items.in_set(frame::InMatch), + append_item_draws.in_set(frame::InMatch), + ) .chain() .after(frame::WorkerCmdSet::CellSceneEnt) .after(frame::WorkerCmdSet::DpvsEnt) .before(frame::WorkerCmdSet::CellDynModel) - .in_set(frame::WorkerCmdSet::SkinModel), + .in_set(frame::WorkerCmdSet::SkinModel)) + .in_set(frame::InMatch), ); } diff --git a/crates/render_anim/src/occupancy/match_reset.rs b/crates/render_anim/src/occupancy/match_reset.rs deleted file mode 100644 index a40133f..0000000 --- a/crates/render_anim/src/occupancy/match_reset.rs +++ /dev/null @@ -1,70 +0,0 @@ -use bevy::prelude::*; -use frame::{MatchInstalled, MatchTornDown, SessionSwapApplied}; -use net::ClientSet; - -use crate::anim::fpv_prepared::PreparedFpv; -use crate::anim::remote_body::RemoteBodyTrees; -use crate::anim::scene_submission::AnimDObjSceneSkels; -use crate::occupancy::dyn_ent::{DynEntPhysClip, DynEntPhysWorld}; -use crate::occupancy::fpv_present::{ - FpvHeldLife, FpvHeldSettled, FpvPlacementRoot, FpvPresentCursor, PendingFpvSpawn, - SessionViewmodel, -}; - -pub(crate) fn reset_anim_for_match( - mut commands: Commands, - mut installed: MessageReader, - mut torn: MessageReader, - mut cursor: ResMut, - mut pending: ResMut, - mut viewmodel: ResMut, - mut prepared_fpv: ResMut, - mut item_compositions: ResMut, - mut remote_kits: ResMut, - mut model_materials: ResMut, - mut settled: ResMut, - mut held_life: ResMut, - mut scene_skels: ResMut, - mut remote_trees: ResMut, - existing: Query>, -) { - let installs = installed.read().count(); - let teardowns = torn.read().count(); - if installs == 0 && teardowns == 0 { - return; - } - - cursor.0.clear(); - pending.0 = None; - *viewmodel = SessionViewmodel::default(); - prepared_fpv.clear(); - item_compositions.clear(); - remote_kits.clear(); - model_materials.clear(); - settled.0 = None; - held_life.0 = None; - *scene_skels = AnimDObjSceneSkels::default(); - *remote_trees = RemoteBodyTrees::default(); - - for entity in &existing { - commands.entity(entity).try_despawn(); - } -} - -fn reset_dyn_ent_phys_on_teardown(mut torn: MessageReader, mut commands: Commands) { - if torn.read().count() == 0 { - return; - } - commands.insert_resource(DynEntPhysClip::default()); - commands.insert_resource(DynEntPhysWorld::default()); -} - -pub(crate) fn register_match_reset_systems(app: &mut App) { - app.add_systems(Update, reset_anim_for_match.in_set(ClientSet::Load)) - .add_systems( - Update, - reset_dyn_ent_phys_on_teardown - .after(SessionSwapApplied) - .in_set(ClientSet::Load), - ); -} diff --git a/crates/render_anim/src/occupancy/missile.rs b/crates/render_anim/src/occupancy/missile.rs index 9755ddb..c2b7ab1 100644 --- a/crates/render_anim/src/occupancy/missile.rs +++ b/crates/render_anim/src/occupancy/missile.rs @@ -1,3 +1,4 @@ +use frame::ScopeApp; use std::collections::HashMap; use bevy::prelude::*; @@ -92,34 +93,42 @@ pub struct MissileBoltRow { } pub fn register_missile_systems(app: &mut App) { - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, - occupy_missile_scene_ents + (occupy_missile_scene_ents + .in_set(frame::InMatch) .in_set(frame::RenderSet::Anim) .before(frame::WorkerCmdSet::CellDynModel) .in_set(render_scene::GfxSceneAdd) - .in_set(AnimSceneSubmit), + .in_set(AnimSceneSubmit)) + .in_set(frame::InMatch), ) .add_systems( Update, - publish_missile_dobj_poses + (publish_missile_dobj_poses + .in_set(frame::InMatch) .after(occupy_missile_scene_ents) .after(frame::WorkerCmdSet::SkinModel) .before(frame::WorkerCmdSet::FxRemaining) - .in_set(frame::ClientSet::Present), + .in_set(frame::ClientSet::Present)) + .in_set(frame::InMatch), ) .add_systems( Update, - (pose_missiles, append_missile_draws) + (( + pose_missiles.in_set(frame::InMatch), + append_missile_draws.in_set(frame::InMatch), + ) .chain() .after(frame::WorkerCmdSet::CellSceneEnt) .after(frame::WorkerCmdSet::DpvsEnt) .before(frame::WorkerCmdSet::CellDynModel) - .in_set(frame::WorkerCmdSet::SkinModel), + .in_set(frame::WorkerCmdSet::SkinModel)) + .in_set(frame::InMatch), ); } diff --git a/crates/render_anim/src/occupancy/mod.rs b/crates/render_anim/src/occupancy/mod.rs index e999cad..1af1f0c 100644 --- a/crates/render_anim/src/occupancy/mod.rs +++ b/crates/render_anim/src/occupancy/mod.rs @@ -4,7 +4,6 @@ pub mod fpv_present; pub mod held_sync; pub mod item; pub mod killcam; -pub mod match_reset; pub mod missile; pub mod remote_body; mod screen_effects; diff --git a/crates/render_anim/src/occupancy/remote_body.rs b/crates/render_anim/src/occupancy/remote_body.rs index 9b1021e..f9bbb30 100644 --- a/crates/render_anim/src/occupancy/remote_body.rs +++ b/crates/render_anim/src/occupancy/remote_body.rs @@ -24,6 +24,7 @@ use assets::{PreparedBodies, PreparedWeapons, PreparedWorldWeapons}; use bevy::prelude::*; use bevy::tasks::ComputeTaskPool; use entity_iw4::{ET_PLAYER, ET_PLAYER_CORPSE}; +use frame::ScopeApp; use frame::{ModelLightingSeated, ViewSubject, WorkerCmdSet}; use net::{ CEntity, CEntityRuntime, ClientSet, FrameClock, LocalPresentClient, PlayerDrawGate, @@ -68,47 +69,59 @@ struct RemoteBodyLightingBinds { } pub fn register_remote_body_systems(app: &mut App) { - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .add_systems(Update, reset_remote_kits.in_set(ClientSet::Load)) + app.init_app::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, - prepare_remote_kits + (reset_remote_kits + .in_set(frame::InMatch) + .in_set(ClientSet::Load)) + .in_set(frame::InMatch), + ) + .add_systems( + Update, + (prepare_remote_kits + .in_set(frame::InMatch) .after(sync_remote_bodies) .before(occupy_remote_scene_ents) .before(pose_remote_bodies) - .in_set(render_scene::GfxSceneAdd), + .in_set(render_scene::GfxSceneAdd)) + .in_set(frame::InMatch), ) - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, - sync_remote_bodies + (sync_remote_bodies .after(PresentedPublished) .before(occupy_remote_scene_ents) - .in_set(render_scene::GfxSceneAdd), + .in_set(render_scene::GfxSceneAdd)) + .in_set(frame::InMatch), ) .add_systems( Update, - occupy_remote_scene_ents + (occupy_remote_scene_ents + .in_set(frame::InMatch) .after(sync_remote_bodies) .after(PresentedPublished) .after(crate::occupancy::fpv_present::occupy_fpv_scene) .in_set(render_scene::GfxSceneAdd) - .in_set(AnimSceneSubmit), + .in_set(AnimSceneSubmit)) + .in_set(frame::InMatch), ) .add_systems( Update, - ( + (( begin_remote_body_tess .after(sync_remote_bodies) .before(pose_remote_bodies), pose_remote_bodies + .in_set(frame::InMatch) .after(sync_remote_bodies) .after(begin_remote_body_tess) .after(crate::anim::dobj_pose::begin_dobj_pose_frame) @@ -117,30 +130,40 @@ pub fn register_remote_body_systems(app: &mut App) { .after(WorkerCmdSet::DpvsEnt), ) .after(PresentedPublished) - .in_set(WorkerCmdSet::SkinModel), + .in_set(WorkerCmdSet::SkinModel)) + .in_set(frame::InMatch), ) .add_systems( Update, - crate::anim::dobj_pose::begin_dobj_pose_frame + (crate::anim::dobj_pose::begin_dobj_pose_frame + .in_set(frame::InMatch) .after(PresentedPublished) - .in_set(ClientSet::Present), + .in_set(ClientSet::Present)) + .in_set(frame::InMatch), ) .add_systems( Update, - enqueue_remote_body_lighting + (enqueue_remote_body_lighting + .in_set(frame::InMatch) .after(WorkerCmdSet::SkinModel) .after(crate::occupancy::script_model::ScriptModelSkinSet) .before(WorkerCmdSet::CellDynModel) .after(PresentedPublished) - .in_set(ClientSet::Present), + .in_set(ClientSet::Present)) + .in_set(frame::InMatch), ) .add_systems( Update, - ( - submit_remote_bodies.after(ModelLightingSeated), - finish_body_draw_plan.after(submit_remote_bodies), + (( + submit_remote_bodies + .in_set(frame::InMatch) + .after(ModelLightingSeated), + finish_body_draw_plan + .in_set(frame::InMatch) + .after(submit_remote_bodies), ) - .in_set(WorkerCmdSet::AddSceneEnt), + .in_set(WorkerCmdSet::AddSceneEnt)) + .in_set(frame::InMatch), ); } diff --git a/crates/render_anim/src/occupancy/script_model.rs b/crates/render_anim/src/occupancy/script_model.rs index 04a48e6..3c64f8b 100644 --- a/crates/render_anim/src/occupancy/script_model.rs +++ b/crates/render_anim/src/occupancy/script_model.rs @@ -1,3 +1,4 @@ +use frame::ScopeApp; use std::collections::{HashMap, HashSet}; use bevy::ecs::system::SystemParam; @@ -256,42 +257,45 @@ pub fn register_script_model_systems(app: &mut App) { let select = RenderFocusSelect::from_env() .unwrap_or_else(|error| panic!("focused render observation refused: {error}")); app.insert_resource(select) - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, - ( + (( sync_spawned_scene_models, apply_presented_script_model_dobjs, - apply_script_mover_centity_pose, - occupy_script_model_scene_ents, + apply_script_mover_centity_pose.in_set(frame::InMatch), + occupy_script_model_scene_ents.in_set(frame::InMatch), ) .chain() .in_set(frame::RenderSet::Anim) .before(frame::WorkerCmdSet::CellDynModel) .in_set(ScriptModelDrawSet) .in_set(render_scene::GfxSceneAdd) - .in_set(AnimSceneSubmit), + .in_set(AnimSceneSubmit)) + .in_set(frame::InMatch), ) .add_systems( Update, - ( - publish_script_model_dobjs, + (( + publish_script_model_dobjs.in_set(frame::InMatch), publish_script_model_bolt_poses + .in_set(frame::InMatch) .after(crate::anim::dobj_pose::begin_dobj_pose_frame), - pose_script_models, - commit_script_model_draw_plan, + pose_script_models.in_set(frame::InMatch), + commit_script_model_draw_plan.in_set(frame::InMatch), ) .chain() .after(occupy_script_model_scene_ents) .after(frame::WorkerCmdSet::CellSceneEnt) .after(frame::WorkerCmdSet::DpvsEnt) .in_set(ScriptModelSkinSet) - .in_set(frame::WorkerCmdSet::SkinModel), + .in_set(frame::WorkerCmdSet::SkinModel)) + .in_set(frame::InMatch), ); } diff --git a/crates/render_anim/src/plugin.rs b/crates/render_anim/src/plugin.rs index c6ee5a9..1319455 100644 --- a/crates/render_anim/src/plugin.rs +++ b/crates/render_anim/src/plugin.rs @@ -1,30 +1,32 @@ use bevy::prelude::*; +use frame::ScopeApp; pub struct RenderAnimPlugin; impl Plugin for RenderAnimPlugin { fn build(&self, app: &mut App) { - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .add_message::(); + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped_message::( + frame::MatchScope::Live, + ); crate::occupancy::fpv_present::register_fpv_present_systems(app); crate::occupancy::held_sync::register_held_sync_systems(app); - crate::occupancy::match_reset::register_match_reset_systems(app); crate::occupancy::remote_body::register_remote_body_systems(app); crate::occupancy::script_model::register_script_model_systems(app); crate::occupancy::missile::register_missile_systems(app); diff --git a/crates/render_frontend/Cargo.toml b/crates/render_frontend/Cargo.toml index 02b5cc3..344374b 100644 --- a/crates/render_frontend/Cargo.toml +++ b/crates/render_frontend/Cargo.toml @@ -43,7 +43,7 @@ render_scene.workspace = true sim.workspace = true weapon_iw4.workspace = true bytemuck = { version = "1", features = ["derive"] } -naga = { version = "29", features = ["wgsl-in"] } +naga = { version = "30", features = ["wgsl-in"] } [lints] workspace = true diff --git a/crates/render_frontend/src/adapters/anim/dyn_ent.rs b/crates/render_frontend/src/adapters/anim/dyn_ent.rs index b9a9f09..86a4608 100644 --- a/crates/render_frontend/src/adapters/anim/dyn_ent.rs +++ b/crates/render_frontend/src/adapters/anim/dyn_ent.rs @@ -23,6 +23,7 @@ pub fn register_dyn_ent_frontend(app: &mut App) { app.add_systems( Update, drain_cell_dyn_model_cmds + .in_set(frame::InMatch) .in_set(frame::RenderSet::Anim) .in_set(frame::WorkerCmdSet::CellDynModel), ); diff --git a/crates/render_frontend/src/adapters/anim/dyn_ent_brush.rs b/crates/render_frontend/src/adapters/anim/dyn_ent_brush.rs index 96668fa..87db92c 100644 --- a/crates/render_frontend/src/adapters/anim/dyn_ent_brush.rs +++ b/crates/render_frontend/src/adapters/anim/dyn_ent_brush.rs @@ -1,4 +1,5 @@ use bevy::platform::collections::HashSet; +use frame::ScopeApp; use bevy::prelude::*; use dpvs_iw4::{ @@ -56,14 +57,15 @@ impl SceneEntCellBits { } pub fn register_dyn_ent_brush_systems(app: &mut App) { - app.init_resource::() - .init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, ( - link_dyn_ent_brush_cells, + link_dyn_ent_brush_cells.in_set(frame::InMatch), exec_cell_dyn_brush_cmds + .in_set(frame::InMatch) .after(link_dyn_ent_brush_cells) .after(frame::WorkerCmdSet::CellStatic), ) @@ -73,11 +75,13 @@ pub fn register_dyn_ent_brush_systems(app: &mut App) { .add_systems( Update, ( - size_scene_ent_cell_bits, + size_scene_ent_cell_bits.in_set(frame::InMatch), link_scene_ents + .in_set(frame::InMatch) .after(size_scene_ent_cell_bits) .after(crate::prepare::scene::gfx_scene::GfxSceneAdd), exec_cell_scene_ent_cmds + .in_set(frame::InMatch) .after(link_scene_ents) .after(frame::WorkerCmdSet::CellStatic), ) @@ -87,6 +91,7 @@ pub fn register_dyn_ent_brush_systems(app: &mut App) { .add_systems( Update, drain_dpvs_ent_cmds + .in_set(frame::InMatch) .after(frame::WorkerCmdSet::CellSceneEnt) .in_set(frame::WorkerCmdSet::DpvsEnt), ); @@ -496,8 +501,6 @@ fn exec_cell_scene_ent_cmds( scene.scene_ent_walked = true; let bit_count = scene_ent_cell_walk_bits(GFX_CFG_ENT_COUNT); - // The planes this cell was seen through on this command's portal visit (see - // `CellFrustumWorkerCmd::visit`); the camera frustum when there are none. let cell_planes: Vec<[f32; 4]> = stats .as_ref() .and_then(|s| match cmd.visit { @@ -639,13 +642,8 @@ fn drain_dpvs_ent_cmds( } dobj.cull_gate = dpvs_iw4::SCENE_DOBJ_GATE_BOUNDED; } else if dobj.cull_gate != dpvs_iw4::SCENE_DOBJ_GATE_BOUNDED { - // Skinned already, or its bounds failed. return; } - // An entity seen through several cells gets a command per cell visit. One whose box - // does not reach the first command's cell must still be tried in the others' cells: - // Terminal's atrium pine is queued first for the eye's cell, which it does not reach, - // and vanished from the spots where that cell was walked first. let Some(bounds) = dobj.posed_bounds else { return; }; diff --git a/crates/render_frontend/src/adapters/anim/dyn_ent_wake.rs b/crates/render_frontend/src/adapters/anim/dyn_ent_wake.rs index 027bc13..3b3709b 100644 --- a/crates/render_frontend/src/adapters/anim/dyn_ent_wake.rs +++ b/crates/render_frontend/src/adapters/anim/dyn_ent_wake.rs @@ -1,3 +1,4 @@ +use frame::ScopeApp; use std::collections::HashMap; use std::time::Instant; @@ -862,9 +863,6 @@ pub(crate) fn wake_player_overlap( let maxs = origin + PLAYER_MAXS; let catalog = catalog.as_deref(); for (entity, inst, transform) in &instances { - // Walking past moves clutter (cans, bottles, luggage) only: a breakable prop's sphere - // takes in a potted plant's leaves, so walking near one pushed the pot along at the - // player's speed. Bullets and explosions still move them. if inst.ty != asset_world::DynEntType::Clutter || world.is_awake(entity) { continue; } @@ -918,18 +916,21 @@ fn on_physics_sphere( } pub fn register_dyn_ent_wake(app: &mut App) { - app.init_resource::() + app.scoped::(frame::MatchScope::Live) .add_observer(on_entity_explosion) .add_observer(on_physics_sphere) .add_observer(on_entity_bullet_hit) .add_observer(on_entity_event_sound) .add_systems( Update, - rebuild_dyn_ent_wake_broadphase.in_set(net::ClientSet::Receive), + rebuild_dyn_ent_wake_broadphase + .in_set(frame::InMatch) + .in_set(net::ClientSet::Receive), ) .add_systems( Update, wake_player_overlap + .in_set(frame::InMatch) .in_set(frame::WorkerCmdSet::Physics) .before(render_anim::occupancy::dyn_ent_phys::step_phys_world0), ); diff --git a/crates/render_frontend/src/adapters/anim/mod.rs b/crates/render_frontend/src/adapters/anim/mod.rs index 1d3a8eb..527dea0 100644 --- a/crates/render_frontend/src/adapters/anim/mod.rs +++ b/crates/render_frontend/src/adapters/anim/mod.rs @@ -3,8 +3,8 @@ pub use render_anim::anim::{ view_kick_state, view_sway, viewmodel_controller, xmodel_pose, }; pub use render_anim::occupancy::{ - dyn_ent_phys, fpv_present, held_sync, item, match_reset, missile, remote_body, script_model, - third_person, view_kick, + dyn_ent_phys, fpv_present, held_sync, item, missile, remote_body, script_model, third_person, + view_kick, }; pub mod dyn_ent; diff --git a/crates/render_frontend/src/adapters/fx/system.rs b/crates/render_frontend/src/adapters/fx/system.rs index e5a603a..367fe09 100644 --- a/crates/render_frontend/src/adapters/fx/system.rs +++ b/crates/render_frontend/src/adapters/fx/system.rs @@ -1,3 +1,4 @@ +use frame::ScopeApp; use std::collections::HashMap; use std::sync::Arc; @@ -102,17 +103,17 @@ impl FxSceneAccess<'_> { pub(crate) fn register_combat_fx_systems(app: &mut App) { app.add_message::() .add_message::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, ( - boot_createfx_oneshots, - sync_script_fx, - crate::assemble::drawsurf::tess::glass::apply_glass_host, - tick_fx_non_dependent_update, + boot_createfx_oneshots.in_set(frame::InMatch), + sync_script_fx.in_set(frame::InMatch), + crate::assemble::drawsurf::tess::glass::apply_glass_host.in_set(frame::InMatch), + tick_fx_non_dependent_update.in_set(frame::InMatch), ) .chain() .after(stamp_fx_camera_origin) @@ -121,6 +122,7 @@ pub(crate) fn register_combat_fx_systems(app: &mut App) { .add_systems( Update, tick_fx_remaining_update + .in_set(frame::InMatch) .after(frame::WorkerCmdSet::SkinModel) .in_set(frame::WorkerCmdSet::FxRemaining), ) @@ -135,12 +137,12 @@ pub(crate) fn register_combat_fx_systems(app: &mut App) { .add_systems( Update, ( - maintain_fire_fx, - drain_weapon_fire_fx, - drain_bullet_hit_fx, - drain_pellet_fx, - present_tracker_light, - drain_tag_fx, + maintain_fire_fx.in_set(frame::InMatch), + drain_weapon_fire_fx.in_set(frame::InMatch), + drain_bullet_hit_fx.in_set(frame::InMatch), + drain_pellet_fx.in_set(frame::InMatch), + present_tracker_light.in_set(frame::InMatch), + drain_tag_fx.in_set(frame::InMatch), ) .chain() .after(render_anim::occupancy::fpv_present::publish_fpv_dobj_pose) @@ -150,14 +152,16 @@ pub(crate) fn register_combat_fx_systems(app: &mut App) { ) .add_systems( Update, - tick_missile_present_state.in_set(ClientSet::Effects), + tick_missile_present_state + .in_set(frame::InMatch) + .in_set(ClientSet::Effects), ) .add_observer(publish_weapon_fire) .add_observer(eject_brass) .add_observer(explosion) - .init_resource::() + .scoped::(frame::MatchScope::Live) .add_observer(reset_killcam_fx) - .init_resource::() + .scoped::(frame::MatchScope::Live) .add_observer(play_fx) .add_observer(play_fx_bullet_hit) .add_observer(melee_blood); @@ -165,12 +169,7 @@ pub(crate) fn register_combat_fx_systems(app: &mut App) { #[derive(Default)] struct TrackerLight { - owner: Option<( - frame::WorldGeneration, - sim::ClientId, - sim::LifeSequence, - u32, - )>, + owner: Option<(frame::WorldStamp, sim::ClientId, sim::LifeSequence, u32)>, bolt: Option<(u32, u16)>, } @@ -195,10 +194,10 @@ fn present_tracker_light( (meta.lifecycle == sim::ClientLifecycle::Alive && ps.other_flags & 0x400 == 0 && prepared.table()?.motion_tracker(weapon, ps.weapon_primary)) - .then_some((*generation, local.0, meta.life_sequence, weapon)) + .then_some((generation.stamp(), local.0, meta.life_sequence, weapon)) }); if owner != light.owner { - if light.owner.is_some_and(|old| old.0 == *generation) { + if light.owner.is_some_and(|old| old.0 == generation.stamp()) { if let Some((dobj, bone)) = light.bolt { host.0.stop_bolted(EFFECT, dobj, bone); } @@ -1214,7 +1213,6 @@ fn fill_fx_model_plan( continue; }; let asset_surfaces = asset.surfaces.clone(); - // A draw without exact packed rows drops the whole merged xmodel lane. if !plan.packed_exact() { plan.skipped_no_material = plan.skipped_no_material.saturating_add(1); continue; @@ -2145,7 +2143,7 @@ fn publish_weapon_fire( audio::emit_audio_diagnostic(format!( "audio diag: fire producer weapon={} alias={alias} player_view={player_view} event={:?}", fire.event.payload.weapon, - audio::AudioEvent::from_entity(*generation, fire.entity, &fire.event, 0).id + audio::AudioEvent::from_entity(generation.stamp(), fire.entity, &fire.event, 0).id )); } combat.last_fire_alias = Some(alias.to_owned()); @@ -2161,7 +2159,7 @@ fn publish_weapon_fire( }; sounds.write(audio::WeaponSound { event: Some(audio::AudioEvent::from_entity( - *generation, + generation.stamp(), fire.entity, &fire.event, 0, @@ -2178,7 +2176,7 @@ fn publish_weapon_fire( audio::emit_audio_diagnostic(format!( "audio diag: fire producer gap weapon={} player_view={player_view} event={:?}", fire.event.payload.weapon, - audio::AudioEvent::from_entity(*generation, fire.entity, &fire.event, 0).id + audio::AudioEvent::from_entity(generation.stamp(), fire.entity, &fire.event, 0).id )); } } @@ -2274,7 +2272,7 @@ fn drain_weapon_fire_fx( let mut muzzle_gap = cursor.muzzle_gap; occurrences.execute( - *generation, + generation.stamp(), timeline.timeline(), FireFxRequest::event(&fire.event, FireFxOccurrence::Muzzle, msec), &verdicts, @@ -2311,7 +2309,7 @@ fn drain_weapon_fire_fx( .is_some_and(|facts| facts.bolt_action); if !delayed_brass { occurrences.execute( - *generation, + generation.stamp(), timeline.timeline(), FireFxRequest::event(&fire.event, FireFxOccurrence::Brass, msec), &verdicts, @@ -2334,7 +2332,7 @@ fn drain_weapon_fire_fx( } else { for product in [FireFxOccurrence::Muzzle, FireFxOccurrence::Brass] { occurrences.execute( - *generation, + generation.stamp(), timeline.timeline(), FireFxRequest::event(&fire.event, product, msec), &verdicts, @@ -2429,7 +2427,7 @@ fn eject_brass( .and_then(|(_, bolts)| bolts.brass) }; occurrences.execute( - *generation, + generation.stamp(), timeline.timeline(), FireFxRequest::event(&brass.event, FireFxOccurrence::Brass, msec), &verdicts, @@ -2745,7 +2743,7 @@ fn explosion( sound_submitted = true; sounds.write(audio::WeaponSound { event: Some(audio::AudioEvent::from_entity( - *generation, + generation.stamp(), explosion.entity, &explosion.event, 1, @@ -2776,7 +2774,7 @@ fn explosion( }; sounds.write(audio::WeaponSound { event: Some(audio::AudioEvent::from_entity( - *generation, + generation.stamp(), explosion.entity, &explosion.event, 0, @@ -3152,7 +3150,7 @@ fn maintain_fire_fx( mut occurrences: ResMut, mut gate: ResMut, ) { - if occurrences.maintain(*generation, timeline.timeline(), host.0.msec_now) { + if occurrences.maintain(generation.stamp(), timeline.timeline(), host.0.msec_now) { *gate = TracerDrawGate::default(); } gate.maintain(host.0.msec_now); @@ -3204,7 +3202,7 @@ fn drain_bullet_hit_fx( .and_then(|weapons| weapons.for_event(hit.0.world).ok()); present_pellet_segment( - *generation, + generation.stamp(), timeline.timeline(), FireFxRequest::event( &hit.0, @@ -3279,7 +3277,7 @@ fn drain_pellet_fx( return; } for (world, domain, record) in pending.take() { - if world != *generation { + if world != generation.stamp() { continue; } let now = host.0.msec_now; @@ -3299,7 +3297,7 @@ fn drain_pellet_fx( .as_deref() .and_then(|weapons| weapons.for_event(world).ok()); present_pellet_segment( - *generation, + generation.stamp(), timeline.timeline(), request, record.segment, diff --git a/crates/render_frontend/src/adapters/motion_tracker.rs b/crates/render_frontend/src/adapters/motion_tracker.rs index 5058d12..564de0f 100644 --- a/crates/render_frontend/src/adapters/motion_tracker.rs +++ b/crates/render_frontend/src/adapters/motion_tracker.rs @@ -1,3 +1,4 @@ +use frame::ScopeApp; use std::collections::HashMap; use bevy::prelude::*; @@ -42,7 +43,7 @@ pub(crate) struct MotionTracker { } pub(crate) fn register(app: &mut App) { - app.init_resource::(); + app.scoped::(frame::MatchScope::Live); } #[allow(clippy::too_many_arguments)] diff --git a/crates/render_frontend/src/assemble/drawsurf/dof.rs b/crates/render_frontend/src/assemble/drawsurf/dof.rs index a1fe5e3..5bd4119 100644 --- a/crates/render_frontend/src/assemble/drawsurf/dof.rs +++ b/crates/render_frontend/src/assemble/drawsurf/dof.rs @@ -109,6 +109,7 @@ pub fn register(app: &mut App) { .add_systems( Update, update_dof + .in_set(frame::InMatch) .after(crate::prepare::scene::view_parms::stamp_prepared_scene_view) .in_set(net::ClientSet::Present), ); diff --git a/crates/render_frontend/src/assemble/drawsurf/film_vision_view.rs b/crates/render_frontend/src/assemble/drawsurf/film_vision_view.rs index da98d82..30ccfd8 100644 --- a/crates/render_frontend/src/assemble/drawsurf/film_vision_view.rs +++ b/crates/render_frontend/src/assemble/drawsurf/film_vision_view.rs @@ -334,6 +334,7 @@ pub fn register(app: &mut App) { app.init_resource::().add_systems( Update, update_film_vision_view + .in_set(frame::InMatch) .after(crate::prepare::scene::view_parms::stamp_prepared_scene_view) .after(frame::ScreenEffectsPublished) .in_set(net::ClientSet::Present), diff --git a/crates/render_frontend/src/assemble/drawsurf/fog.rs b/crates/render_frontend/src/assemble/drawsurf/fog.rs index 5f4e103..4ed3fb3 100644 --- a/crates/render_frontend/src/assemble/drawsurf/fog.rs +++ b/crates/render_frontend/src/assemble/drawsurf/fog.rs @@ -167,13 +167,17 @@ pub(crate) fn sync_script_fog( if let Some(map_fog) = map_fog.as_deref_mut() { *map_fog = target; } else { - commands.insert_resource(target); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, target, frame::MatchScope::Live) + }); } } else if let Some(baseline) = state.baseline.take() { if let Some(map_fog) = map_fog.as_deref_mut() { *map_fog = baseline; } else { - commands.insert_resource(baseline); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, baseline, frame::MatchScope::Live) + }); } } else { commands.remove_resource::(); diff --git a/crates/render_frontend/src/assemble/drawsurf/frame_products.rs b/crates/render_frontend/src/assemble/drawsurf/frame_products.rs index 88018b0..3655711 100644 --- a/crates/render_frontend/src/assemble/drawsurf/frame_products.rs +++ b/crates/render_frontend/src/assemble/drawsurf/frame_products.rs @@ -885,7 +885,7 @@ fn fill_product_list( pub struct FrameAssemblyInputs { pub frame_id: u64, - pub world_generation: frame::WorldGeneration, + pub world_generation: frame::WorldStamp, pub catalog_generation: MaterialGenerationId, pub inv_image_height: Option, @@ -920,7 +920,7 @@ pub(crate) fn open_frame_products( let inputs = inputs.as_mut(); inputs.frame_id = inputs.frame_id.wrapping_add(1); inputs.world_generation = world_generation - .map(|generation| *generation) + .map(|generation| generation.stamp()) .unwrap_or_default(); inputs.catalog_generation = generation.catalog.generation_id(); inputs.inv_image_height = lighting.as_ref().and_then(|lighting| { diff --git a/crates/render_frontend/src/assemble/drawsurf/material_admit.rs b/crates/render_frontend/src/assemble/drawsurf/material_admit.rs index 351c757..db26044 100644 --- a/crates/render_frontend/src/assemble/drawsurf/material_admit.rs +++ b/crates/render_frontend/src/assemble/drawsurf/material_admit.rs @@ -7,19 +7,7 @@ use super::sm3_wgsl::ValidatedPassWgsl; use super::{MaterialProgramCompile, RuntimeProgramRegistry}; fn shader_from_generated_wgsl(source: String, path: String) -> Shader { - if source.contains('#') { - return Shader::from_wgsl(source, path); - } - Shader { - import_path: bevy::shader::ShaderImport::AssetPath(path.clone()), - path, - source: bevy::shader::Source::Wgsl(source.into()), - imports: Vec::new(), - additional_imports: Vec::new(), - shader_defs: Vec::new(), - file_dependencies: Vec::new(), - validate_shader: bevy::shader::ValidateShader::Disabled, - } + Shader::from_wgsl(source, path) } enum PendingShader { diff --git a/crates/render_frontend/src/assemble/drawsurf/material_compile.rs b/crates/render_frontend/src/assemble/drawsurf/material_compile.rs index 316d677..2784d9c 100644 --- a/crates/render_frontend/src/assemble/drawsurf/material_compile.rs +++ b/crates/render_frontend/src/assemble/drawsurf/material_compile.rs @@ -86,6 +86,7 @@ fn compile_jobs_parallel( pass.t5_custom_sampler_flags, pass.color_space, pass.hardware_shadow_compare, + pass.depth_to_colour, pass.arguments.as_slice(), job.vertex_type, ); diff --git a/crates/render_frontend/src/assemble/drawsurf/material_runtime.rs b/crates/render_frontend/src/assemble/drawsurf/material_runtime.rs index 947bec5..2ede9d7 100644 --- a/crates/render_frontend/src/assemble/drawsurf/material_runtime.rs +++ b/crates/render_frontend/src/assemble/drawsurf/material_runtime.rs @@ -216,6 +216,7 @@ impl RuntimeProgramPort { && pass.arguments == self.arguments && pass.color_space == self.color_space && pass.hardware_shadow_compare == self.hardware_shadow_compare + && pass.depth_to_colour == self.abi.depth_to_colour && self.abi.vertex_type == vertex_type } } @@ -253,6 +254,7 @@ impl RuntimeProgramRegistry { u8, render_material::PassColorSpace, bool, + bool, Vec, ), usize, @@ -274,6 +276,7 @@ impl RuntimeProgramRegistry { pass.t5_custom_sampler_flags, pass.color_space, pass.hardware_shadow_compare, + pass.depth_to_colour, pass.arguments.clone(), ); if seen.contains_key(&key) { diff --git a/crates/render_frontend/src/assemble/drawsurf/retained_list/static_lane.rs b/crates/render_frontend/src/assemble/drawsurf/retained_list/static_lane.rs index ee30ccd..0f6de9c 100644 --- a/crates/render_frontend/src/assemble/drawsurf/retained_list/static_lane.rs +++ b/crates/render_frontend/src/assemble/drawsurf/retained_list/static_lane.rs @@ -20,7 +20,7 @@ use crate::prepare::scene::smodel_lighting::WorldSmodelLighting; use crate::prepare::scene::view_parms::PreparedSceneView; use crate::prepare::scene::world::WorldScene; use bevy::prelude::*; -use frame::WorldGeneration; +use frame::{WorldGeneration, WorldStamp}; use render_frame::{BspCameraLane, RetainedDrawItem, RetainedDrawKind}; use std::sync::Arc; use std::time::Instant; @@ -98,7 +98,7 @@ pub struct StaticDrawLane { logged_buckets: Option<[u32; 5]>, - pub(crate) world_generation: WorldGeneration, + pub(crate) world_generation: WorldStamp, pub(crate) colour: Vec, pub(crate) emissive: Vec, pub(crate) distortion: Vec, @@ -128,7 +128,7 @@ pub struct StaticDrawLane { impl StaticDrawLane { pub(crate) fn live_for( &self, - current: WorldGeneration, + current: WorldStamp, ) -> ( &[RetainedDrawItem], &[RetainedDrawItem], @@ -631,7 +631,7 @@ pub(crate) fn rebuild_static_draw_lane( .unwrap_or(&[]); let world_id = world_list.draw_items_id; let vis_id = dpvs.as_ref().map(|stats| stats.smodel_vis_id).unwrap_or(0); - let world_generation = world_generation.map(|g| *g).unwrap_or_default(); + let world_generation = world_generation.map(|g| g.stamp()).unwrap_or_default(); let eye = prepared.as_ref().filter(|v| v.ready).map(|v| v.eye); let lod_args = lod_ramp.args(); let eye_key = eye_lod_reuse_key(eye, lod_args); diff --git a/crates/render_frontend/src/assemble/drawsurf/sm3_wgsl.rs b/crates/render_frontend/src/assemble/drawsurf/sm3_wgsl.rs index df08d97..721c7e5 100644 --- a/crates/render_frontend/src/assemble/drawsurf/sm3_wgsl.rs +++ b/crates/render_frontend/src/assemble/drawsurf/sm3_wgsl.rs @@ -88,6 +88,7 @@ pub fn pass_lowering_abi(abi: &PassProgramAbi) -> PassLoweringAbi { }) .collect(), alpha_tests: MATERIAL_ALPHA_TESTS.to_vec(), + depth_to_colour: abi.depth_to_colour, } } diff --git a/crates/render_frontend/src/assemble/drawsurf/tess/glass.rs b/crates/render_frontend/src/assemble/drawsurf/tess/glass.rs index 9b5bdb8..4f368d4 100644 --- a/crates/render_frontend/src/assemble/drawsurf/tess/glass.rs +++ b/crates/render_frontend/src/assemble/drawsurf/tess/glass.rs @@ -763,9 +763,6 @@ pub(crate) fn enqueue_glass_model_lighting( let owner = ModelLightingOwner::Glass(draw.piece); draw.lighting_prev = u32::from(cache.handle_for(owner)); draw.pending_lighting = None; - // The pane's own extent: a window much wider than 64 units can show while its centre - // is off screen, and a pane drawn without lighting takes probe 0 and the default - // lighting (the red-tinted windows on Terminal). let radius = draw.radius.max(GLASS_FRUSTUM_LIGHT_RADIUS); if sphere_behind_frustum(draw.origin, radius, &planes) { draw.lighting_handle = 0; diff --git a/crates/render_frontend/src/assemble/drawsurf/tess/sky.rs b/crates/render_frontend/src/assemble/drawsurf/tess/sky.rs index 20bcc71..75c7d93 100644 --- a/crates/render_frontend/src/assemble/drawsurf/tess/sky.rs +++ b/crates/render_frontend/src/assemble/drawsurf/tess/sky.rs @@ -116,11 +116,17 @@ pub fn build_sky_model_draw_plan( ); revisions.bump_surfaces(); revisions.bump_draws(); - commands.insert_resource(SkyModelDrawPlan { - geometry, - draws, - refusals, - generation: 1, - revisions, + commands.queue(move |world: &mut World| { + frame::scope::insert( + world, + SkyModelDrawPlan { + geometry, + draws, + refusals, + generation: 1, + revisions, + }, + frame::MatchScope::Live, + ); }); } diff --git a/crates/render_frontend/src/assemble/drawsurf/tess/smodel.rs b/crates/render_frontend/src/assemble/drawsurf/tess/smodel.rs index a35f597..f5b5661 100644 --- a/crates/render_frontend/src/assemble/drawsurf/tess/smodel.rs +++ b/crates/render_frontend/src/assemble/drawsurf/tess/smodel.rs @@ -793,5 +793,6 @@ pub(crate) fn build_smodel_gpu_plan( plan.materials.len(), ); plan.publish_extract_shares(); - commands.insert_resource(plan); + commands + .queue(move |world: &mut World| frame::scope::insert(world, plan, frame::MatchScope::Live)); } diff --git a/crates/render_frontend/src/assemble/drawsurf/tess/world.rs b/crates/render_frontend/src/assemble/drawsurf/tess/world.rs index 3ff1eaf..0e2834a 100644 --- a/crates/render_frontend/src/assemble/drawsurf/tess/world.rs +++ b/crates/render_frontend/src/assemble/drawsurf/tess/world.rs @@ -357,5 +357,6 @@ pub(crate) fn build_world_draw_gpu_plan( scene.retained_lightmap_uvs.len(), scene.retained_normals.len(), ); - commands.insert_resource(plan); + commands + .queue(move |world: &mut World| frame::scope::insert(world, plan, frame::MatchScope::Live)); } diff --git a/crates/render_frontend/src/assemble/drawsurf/tess/xmodel.rs b/crates/render_frontend/src/assemble/drawsurf/tess/xmodel.rs index bb9b174..b968308 100644 --- a/crates/render_frontend/src/assemble/drawsurf/tess/xmodel.rs +++ b/crates/render_frontend/src/assemble/drawsurf/tess/xmodel.rs @@ -360,8 +360,6 @@ fn xmodel_merge_stamp( for draw in items.draws() { scene_ent_admitted(scene, draw.scene_entnum).hash(&mut occupancy); } - // The concat patch cannot add or drop a source, so a source starting or - // stopping to draw changes the topology. (!fx_models.draws().is_empty()).hash(&mut occupancy); (!dynents.draws().is_empty()).hash(&mut occupancy); let sky_eye = sky.map(|(_, eye)| [eye.x.to_bits(), eye.y.to_bits(), eye.z.to_bits()]); @@ -624,8 +622,6 @@ pub fn merge_xmodel_draw_plan( .copied() .filter(|d| scene_ent_admitted(scene, d.scene_entnum)), ); - // Whole, not compacted: compacting drops the concat layout, so every frame - // with a live piece would re-merge and re-upload. if !fx_draws.is_empty() { append_source_plan( merged, diff --git a/crates/render_frontend/src/assemble/drawsurf/wgsl_disk_cache.rs b/crates/render_frontend/src/assemble/drawsurf/wgsl_disk_cache.rs index 73db306..cdf3223 100644 --- a/crates/render_frontend/src/assemble/drawsurf/wgsl_disk_cache.rs +++ b/crates/render_frontend/src/assemble/drawsurf/wgsl_disk_cache.rs @@ -5,7 +5,7 @@ use d3d9_sm3::{PassLoweringAbi, PassWgsl}; use super::material_runtime::RuntimeShaderPair; // Bump when emitted WGSL changes as well as when the binary format changes. -pub const WGSL_CACHE_FORMAT: u32 = 6; +pub const WGSL_CACHE_FORMAT: u32 = 7; const MAGIC: &[u8; 8] = b"IWLWGSL\n"; diff --git a/crates/render_frontend/src/assemble/match_reset.rs b/crates/render_frontend/src/assemble/match_reset.rs deleted file mode 100644 index 7158b2c..0000000 --- a/crates/render_frontend/src/assemble/match_reset.rs +++ /dev/null @@ -1,46 +0,0 @@ -use bevy::prelude::*; -use frame::{MatchTornDown, SessionSwapApplied}; -use net::ClientSet; - -use super::drawsurf::{ - DrawSurfList, FxCodeMeshPlan, FxModelDrawPlan, FxParticleCloudPlan, GfxGlassMeshPlan, - GfxMarkMeshPlan, GlassTable, -}; - -pub(crate) fn reset_world_draw_plans_on_teardown( - mut torn: MessageReader, - mut commands: Commands, - mut dfog: ResMut, - mut draw_list: ResMut, - mut glass: ResMut, - mut cg_glass: ResMut, - mut fx_code: ResMut, - mut spark: ResMut, - mut marks: ResMut, - mut fx_models: ResMut, -) { - if torn.read().count() == 0 { - return; - } - commands.remove_resource::(); - commands.insert_resource(super::drawsurf::fog::ScriptFogPresentation::default()); - dfog.0 = false; - *draw_list = DrawSurfList::default(); - *glass = GfxGlassMeshPlan::default(); - cg_glass.reset(); - *fx_code = FxCodeMeshPlan::default(); - *spark = FxParticleCloudPlan::default(); - *marks = GfxMarkMeshPlan::default(); - *fx_models = FxModelDrawPlan::default(); -} - -pub(crate) fn register_match_reset_systems(app: &mut App) { - app.add_systems( - Update, - reset_world_draw_plans_on_teardown - .after(SessionSwapApplied) - .after(crate::prepare::scene::spawn::reset_world_spawn_on_teardown) - .before(crate::prepare::scene::spawn::shutdown_world_on_teardown) - .in_set(ClientSet::Load), - ); -} diff --git a/crates/render_frontend/src/assemble/mod.rs b/crates/render_frontend/src/assemble/mod.rs index 18fcb8d..97d0c7e 100644 --- a/crates/render_frontend/src/assemble/mod.rs +++ b/crates/render_frontend/src/assemble/mod.rs @@ -1,7 +1,7 @@ use bevy::prelude::*; +use frame::ScopeApp; pub mod drawsurf; -mod match_reset; pub mod pack; #[derive(bevy::ecs::schedule::ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] @@ -23,8 +23,7 @@ pub struct RenderAssemblePlugin; impl Plugin for RenderAssemblePlugin { fn build(&self, app: &mut App) { - match_reset::register_match_reset_systems(app); - app.init_resource::() + app.scoped::(frame::MatchScope::Live) .add_systems(StaticSunAndFx, drawsurf::bake_static_sun_shadow_casters) .edit_schedule(StaticSunAndFx, |schedule| { schedule.set_executor(bevy::ecs::schedule::MultiThreadedExecutor::new()); @@ -32,6 +31,7 @@ impl Plugin for RenderAssemblePlugin { .add_systems( Update, prepare_static_sun_and_fx + .in_set(frame::InMatch) .in_set(frame::WorkerCmdSet::FxVerts) .after(frame::WorkerCmdSet::FxRemaining) .after(frame::WorkerCmdSet::SmodelCache) @@ -43,6 +43,7 @@ impl Plugin for RenderAssemblePlugin { app.add_systems( Update, crate::assemble::drawsurf::tess::world::build_world_draw_gpu_plan + .in_set(frame::InMatch) .after(crate::prepare::scene::spawn::spawn_world) .before(crate::prepare::scene::spawn::spawn_world_finish) .in_set(net::ClientSet::Present), @@ -50,6 +51,7 @@ impl Plugin for RenderAssemblePlugin { app.add_systems( Update, crate::assemble::drawsurf::tess::smodel::build_smodel_gpu_plan + .in_set(frame::InMatch) .after(crate::prepare::scene::spawn::spawn_world_finish) .before(frame::WorkerCmdSet::CellStatic) .in_set(net::ClientSet::Present), @@ -65,6 +67,7 @@ impl Plugin for RenderAssemblePlugin { app.add_systems( Update, crate::assemble::drawsurf::tess::xmodel::apply_resolved_xmodel_lighting + .in_set(frame::InMatch) .after(crate::prepare::scene::model_lighting_cache::update_dirty_model_lighting) .in_set(frame::WorkerCmdSet::AddSceneEnt), ); @@ -72,6 +75,7 @@ impl Plugin for RenderAssemblePlugin { app.add_systems( Update, crate::assemble::drawsurf::update_command_context_code_sources + .in_set(frame::InMatch) .after(crate::prepare::scene::view_parms::stamp_prepared_scene_view) .in_set(net::ClientSet::Present), ); @@ -89,6 +93,7 @@ impl Plugin for RenderAssemblePlugin { .add_systems( Update, rebuild_draw_lanes + .in_set(frame::InMatch) .after(frame::ScreenEffectsPublished) .after(crate::adapters::anim::fpv_present::FpvPlacementSet) .after(crate::adapters::anim::fpv_present::FpvGeometrySet) @@ -109,9 +114,11 @@ impl Plugin for RenderAssemblePlugin { Update, ( crate::assemble::drawsurf::open_frame_products + .in_set(frame::InMatch) .after(frame::WorkerCmdSet::FxVerts) .after(crate::assemble::drawsurf::update_command_context_code_sources), crate::assemble::drawsurf::bake_sun_shadow_casters + .in_set(frame::InMatch) .after(prepare_static_sun_and_fx) .after(crate::assemble::drawsurf::open_frame_products) .after(rebuild_draw_lanes) @@ -119,24 +126,29 @@ impl Plugin for RenderAssemblePlugin { .after(crate::prepare::scene::cull::apply_dpvs_cull) .after(crate::prepare::scene::smodel_lighting::update_smodel_lighting), crate::assemble::drawsurf::bake_spot_shadow_casters + .in_set(frame::InMatch) .after(crate::assemble::drawsurf::open_frame_products) .after(rebuild_draw_lanes) .after(crate::prepare::scene::gfx_scene::snapshot_spot_shadow_occupancy) .after(crate::prepare::scene::cull::apply_dpvs_cull) .after(rebuild_draw_lanes), crate::assemble::drawsurf::execute_sun_product + .in_set(frame::InMatch) .after(crate::assemble::drawsurf::bake_sun_shadow_casters) .after(rebuild_draw_lanes), crate::assemble::drawsurf::execute_spot_product + .in_set(frame::InMatch) .after(crate::assemble::drawsurf::bake_spot_shadow_casters) .after(rebuild_draw_lanes), crate::assemble::drawsurf::execute_camera_products + .in_set(frame::InMatch) .after(crate::assemble::drawsurf::bake_sun_shadow_casters) .after(crate::assemble::drawsurf::bake_spot_shadow_casters) .after(rebuild_draw_lanes) .after(rebuild_draw_lanes) .after(rebuild_draw_lanes), crate::assemble::drawsurf::publish_frame_products + .in_set(frame::InMatch) .after(crate::assemble::drawsurf::execute_camera_products) .after(crate::assemble::drawsurf::execute_sun_product) .after(crate::assemble::drawsurf::execute_spot_product), @@ -146,6 +158,7 @@ impl Plugin for RenderAssemblePlugin { app.add_systems( PostUpdate, crate::assemble::drawsurf::log_probe_index_census + .in_set(frame::InMatch) .after(bevy::transform::TransformSystems::Propagate), ); } diff --git a/crates/render_frontend/src/prepare/mod.rs b/crates/render_frontend/src/prepare/mod.rs index 3ad6e28..fb711db 100644 --- a/crates/render_frontend/src/prepare/mod.rs +++ b/crates/render_frontend/src/prepare/mod.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; -use frame::{HasWorld, ModelLightingSeated, SessionSwapApplied, WorkerCmdSet}; +use frame::ScopeApp; +use frame::{ModelLightingSeated, WorkerCmdSet}; use net::ClientSet; pub mod gfx_scene; @@ -9,8 +10,8 @@ pub mod worker_cmds; use crate::adapters::anim::view_kick::sync_camera_from_presented; use crate::assemble::drawsurf::{ - ColourDrawMethod, DrawSurfList, FpvDrawPlan, MaterialFrameInputs, MaterialGeneration, - RenderFrameProducts, StaticDrawLane, + ColourDrawMethod, DrawSurfList, MaterialFrameInputs, MaterialGeneration, RenderFrameProducts, + StaticDrawLane, }; use crate::prepare::scene::camera::fly_camera; use crate::prepare::scene::cull::{ @@ -18,9 +19,8 @@ use crate::prepare::scene::cull::{ }; use crate::prepare::scene::smodel_lighting::update_smodel_lighting; use crate::prepare::scene::spawn::{ - WorldSpawnJob, arm_world_spawn_on_install, despawn_fly_cameras_on_teardown, - despawn_world_entities_on_teardown, register_world_gpu_ready, reset_world_spawn_on_teardown, - shutdown_world_on_teardown, spawn_world, spawn_world_finish, + WorldSpawnJob, arm_world_spawn_on_install, register_world_gpu_ready, spawn_world, + spawn_world_finish, }; use crate::prepare::scene::view_parms::stamp_prepared_scene_view; use crate::prepare::scene::world::WorldScene; @@ -31,21 +31,21 @@ impl Plugin for RenderPreparePlugin { fn build(&self, app: &mut App) { super::assemble::drawsurf::dof::register(app); super::assemble::drawsurf::film_vision_view::register(app); - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + app + + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) .configure_sets( Update, ( @@ -60,46 +60,45 @@ impl Plugin for RenderPreparePlugin { ) .in_set(ClientSet::Present), ) - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + + + + + + .scoped::(frame::MatchScope::Live) + + .scoped::(frame::MatchScope::Live) .init_resource::() .add_systems( Update, ( - arm_world_spawn_on_install.before(spawn_world), - spawn_world, - spawn_world_finish.after(spawn_world), - render_anim::prepare_fpv_compositions.after(spawn_world_finish), - render_anim::prepare_model_materials.after(spawn_world_finish), - crate::assemble::drawsurf::tess::glass::apply_cg_glass_tess + spawn_world.in_set(frame::InMatch), + spawn_world_finish.in_set(frame::InMatch).after(spawn_world), + render_anim::prepare_fpv_compositions.in_set(frame::InMatch).after(spawn_world_finish), + render_anim::prepare_model_materials.in_set(frame::InMatch).after(spawn_world_finish), + crate::assemble::drawsurf::tess::glass::apply_cg_glass_tess.in_set(frame::InMatch) .after(spawn_world) .after(WorkerCmdSet::FxNonDependent), - crate::assemble::drawsurf::fog::sync_script_fog + crate::assemble::drawsurf::fog::sync_script_fog.in_set(frame::InMatch) .after(spawn_world_finish) .before(stamp_prepared_scene_view), fly_camera, @@ -108,37 +107,37 @@ impl Plugin for RenderPreparePlugin { .after(sync_camera_from_presented) .after(crate::adapters::anim::fpv_present::spawn_pending_fpv) .after(crate::adapters::anim::fpv_present::tick_fpv_viewmodel), - crate::assemble::drawsurf::publish_sun_effects_frame + crate::assemble::drawsurf::publish_sun_effects_frame.in_set(frame::InMatch) .after(stamp_prepared_scene_view), - crate::assemble::drawsurf::ingest_drawsurf_list + crate::assemble::drawsurf::ingest_drawsurf_list.in_set(frame::InMatch) .after(WorkerCmdSet::CellStatic) .after(WorkerCmdSet::CellDynModel) .after(WorkerCmdSet::CellDynBrush), - update_smodel_lighting + update_smodel_lighting.in_set(frame::InMatch) .after(WorkerCmdSet::CellStatic) .after(WorkerCmdSet::SkinModel) .after(crate::adapters::anim::script_model::ScriptModelSkinSet), - crate::prepare::scene::smodel_geom_cache::cache_visible_smodel_surfaces + crate::prepare::scene::smodel_geom_cache::cache_visible_smodel_surfaces.in_set(frame::InMatch) .after(update_smodel_lighting), - crate::prepare::scene::gfx_scene::snapshot_spot_shadow_occupancy + crate::prepare::scene::gfx_scene::snapshot_spot_shadow_occupancy.in_set(frame::InMatch) .after(crate::prepare::scene::gfx_scene::GfxSceneAdd) .after(WorkerCmdSet::SkinModel), - crate::prepare::scene::model_lighting_cache::begin_dyn_model_lighting_frame + crate::prepare::scene::model_lighting_cache::begin_dyn_model_lighting_frame.in_set(frame::InMatch) .after(spawn_world), - crate::prepare::scene::model_lighting_cache::enqueue_fpv_model_lighting + crate::prepare::scene::model_lighting_cache::enqueue_fpv_model_lighting.in_set(frame::InMatch) .after(render_anim::spawn_pending_fpv) .after(render_anim::tick_fpv_viewmodel) .after(crate::prepare::scene::model_lighting_cache::begin_dyn_model_lighting_frame) .after(render_anim::ScriptModelDrawSet) .before(WorkerCmdSet::CellDynModel), - crate::assemble::drawsurf::tess::glass::enqueue_glass_model_lighting + crate::assemble::drawsurf::tess::glass::enqueue_glass_model_lighting.in_set(frame::InMatch) .after(crate::prepare::scene::model_lighting_cache::begin_dyn_model_lighting_frame) .after(crate::assemble::drawsurf::tess::glass::apply_cg_glass_tess) .after(WorkerCmdSet::SkinModel) .after(crate::adapters::anim::script_model::ScriptModelSkinSet) .after(WorkerCmdSet::CellDynModel) .after(crate::prepare::scene::model_lighting_cache::update_dirty_model_lighting), - log_dpvs_stats_once.after(WorkerCmdSet::CellStatic), + log_dpvs_stats_once.in_set(frame::InMatch).after(WorkerCmdSet::CellStatic), log_script_model_gaps_once, ) .in_set(ClientSet::Present), @@ -146,7 +145,7 @@ impl Plugin for RenderPreparePlugin { .add_systems( Update, - crate::prepare::scene::model_lighting_cache::update_dirty_model_lighting + crate::prepare::scene::model_lighting_cache::update_dirty_model_lighting.in_set(frame::InMatch) .after(crate::prepare::scene::model_lighting_cache::begin_dyn_model_lighting_frame) .after(WorkerCmdSet::SkinModel) .after(WorkerCmdSet::CellDynModel) @@ -158,15 +157,15 @@ impl Plugin for RenderPreparePlugin { .add_systems( Update, ( - crate::prepare::scene::model_lighting_cache::update_glass_dyn_lighting + crate::prepare::scene::model_lighting_cache::update_glass_dyn_lighting.in_set(frame::InMatch) .after(crate::assemble::drawsurf::tess::glass::enqueue_glass_model_lighting) .before(WorkerCmdSet::FxVerts), - crate::assemble::drawsurf::tess::glass::apply_glass_model_lighting + crate::assemble::drawsurf::tess::glass::apply_glass_model_lighting.in_set(frame::InMatch) .after(crate::prepare::scene::model_lighting_cache::update_glass_dyn_lighting), - crate::prepare::scene::model_lighting_cache::update_fx_dyn_lighting + crate::prepare::scene::model_lighting_cache::update_fx_dyn_lighting.in_set(frame::InMatch) .after(WorkerCmdSet::FxVerts) .after(crate::prepare::scene::model_lighting_cache::update_glass_dyn_lighting), - crate::assemble::drawsurf::tess::xmodel::apply_resolved_fx_model_lighting + crate::assemble::drawsurf::tess::xmodel::apply_resolved_fx_model_lighting.in_set(frame::InMatch) .after(crate::prepare::scene::model_lighting_cache::update_fx_dyn_lighting), ) .in_set(ClientSet::Present), @@ -174,24 +173,24 @@ impl Plugin for RenderPreparePlugin { .add_systems( Update, ( - crate::prepare::scene::smodel_geom_cache::skin_cached_static_model_cmd + crate::prepare::scene::smodel_geom_cache::skin_cached_static_model_cmd.in_set(frame::InMatch) .in_set(WorkerCmdSet::SmodelCache) .after(crate::prepare::scene::smodel_geom_cache::cache_visible_smodel_surfaces), - crate::prepare::scene::gfx_scene::clear_host_gfx_scene + crate::prepare::scene::gfx_scene::clear_host_gfx_scene.in_set(frame::InMatch) .in_set(crate::prepare::scene::gfx_scene::GfxSceneClear), - crate::prepare::scene::gfx_scene::occupy_script_brush_scene + crate::prepare::scene::gfx_scene::occupy_script_brush_scene.in_set(frame::InMatch) .after(spawn_world) .after(crate::adapters::anim::script_model::ScriptModelDrawSet) .after(crate::prepare::scene::gfx_scene::apply_anim_dobj_scene_submissions) .in_set(crate::prepare::scene::gfx_scene::GfxSceneAdd), - crate::prepare::scene::gfx_scene::apply_anim_dobj_scene_submissions + crate::prepare::scene::gfx_scene::apply_anim_dobj_scene_submissions.in_set(frame::InMatch) .after(crate::adapters::anim::scene_submission::AnimSceneSubmit) .in_set(crate::prepare::scene::gfx_scene::GfxSceneAdd), ), ) .add_systems( Update, - apply_dpvs_cull + apply_dpvs_cull.in_set(frame::InMatch) .after(fly_camera) .after(stamp_prepared_scene_view) .after(crate::prepare::scene::sun_stage::update_active_sun_stage) @@ -199,31 +198,17 @@ impl Plugin for RenderPreparePlugin { ) .add_systems( Update, - crate::prepare::scene::sun_stage::update_active_sun_stage + crate::prepare::scene::sun_stage::update_active_sun_stage.in_set(frame::InMatch) .after(stamp_prepared_scene_view) .before(crate::assemble::drawsurf::update_command_context_code_sources) .in_set(ClientSet::Present), ) - .init_resource::() - .add_systems( - Update, - ( - despawn_world_entities_on_teardown, - reset_world_spawn_on_teardown, - shutdown_world_on_teardown, - despawn_fly_cameras_on_teardown, - ) - .chain() - .after(SessionSwapApplied) - .in_set(ClientSet::Load), - ); + ; app.add_systems( - Update, - publish_dyn_atpoint_lookup - .after(SessionSwapApplied) - .after(shutdown_world_on_teardown) - .in_set(ClientSet::Load), + OnEnter(frame::MatchScope::Live), + (arm_world_spawn_on_install, publish_dyn_atpoint_lookup) + .in_set(frame::ScopeSet::Derive), ); register_world_gpu_ready(app); crate::prepare::scene::cell_frustum_cmds::register_cell_frustum_cmds(app); @@ -231,14 +216,11 @@ impl Plugin for RenderPreparePlugin { } fn publish_dyn_atpoint_lookup( - mut installed: MessageReader, scene: Res, mut lookup: ResMut, mut cells: ResMut, ) { // Static tables are complete when the install transaction publishes this message. - if installed.read().count() != 0 { - scene.publish_dyn_atpoint(&mut lookup); - scene.publish_dpvs_cells(&mut cells); - } + scene.publish_dyn_atpoint(&mut lookup); + scene.publish_dpvs_cells(&mut cells); } diff --git a/crates/render_frontend/src/prepare/scene/cell_frustum_cmds.rs b/crates/render_frontend/src/prepare/scene/cell_frustum_cmds.rs index 8fbb665..a0321be 100644 --- a/crates/render_frontend/src/prepare/scene/cell_frustum_cmds.rs +++ b/crates/render_frontend/src/prepare/scene/cell_frustum_cmds.rs @@ -29,6 +29,7 @@ pub fn register_cell_frustum_cmds(app: &mut App) { app.add_systems( Update, enqueue_cell_frustum_cmds_after_vis + .in_set(frame::InMatch) .after(crate::prepare::scene::cull::apply_dpvs_cull) .in_set(frame::WorkerCmdSet::CellStatic), ); @@ -52,8 +53,6 @@ fn enqueue_cell_frustum_cmds_after_vis( Err(_) => false, Ok(_) => true, }; - // One command per portal visit, each culling the cell's entities against the planes it - // was seen through, as IW4 does: an entity seen through any of a cell's portals is drawn. let visits = &stats.cell_clip_visits; if !visits.is_empty() && visits.len() <= CELL_VISIT_BUDGET { for (index, (cell, clip)) in visits.iter().enumerate() { @@ -73,9 +72,6 @@ fn enqueue_cell_frustum_cmds_after_vis( } return; } - // No walk this frame (single cell, all cells, no eye cell), or too many visits to queue: - // one command per cell. With visits it falls back to the camera frustum, never to one - // visit's planes, so nothing a later portal shows is culled. for cell in cells { let first = stats .cell_clips @@ -103,6 +99,4 @@ fn enqueue_cell_frustum_cmds_after_vis( } } -/// Portal visits queued as one cell command each; the cell queues hold 512 commands -/// (`0x1800` bytes of 12), so a frame with more visits falls back to one command per cell. const CELL_VISIT_BUDGET: usize = 480; diff --git a/crates/render_frontend/src/prepare/scene/cull.rs b/crates/render_frontend/src/prepare/scene/cull.rs index e3d9c3e..e3efa6f 100644 --- a/crates/render_frontend/src/prepare/scene/cull.rs +++ b/crates/render_frontend/src/prepare/scene/cull.rs @@ -89,8 +89,6 @@ pub struct DpvsFrameStats { pub cell_clips: Vec, - /// Every portal visit of the walk, in order: the cell and the planes it was seen through. - /// A cell seen through several portals appears once per portal. pub cell_clip_visits: Vec<(u32, CellClipPlanes)>, pub cell_vis_all: bool, @@ -109,9 +107,11 @@ fn single_cell_from_env() -> bool { pub struct WorldMeshEntity; #[derive(Component)] +#[require(bevy::prelude::DespawnOnExit = bevy::prelude::DespawnOnExit(frame::MatchScope::Live))] pub struct StaticModelEntity; #[derive(Component)] +#[require(bevy::prelude::DespawnOnExit = bevy::prelude::DespawnOnExit(frame::MatchScope::Live))] pub struct ScriptModelEntity; pub use render_scene::DynEntModelEntity; diff --git a/crates/render_frontend/src/prepare/scene/spawn.rs b/crates/render_frontend/src/prepare/scene/spawn.rs index 88ae9d7..2a8adf1 100644 --- a/crates/render_frontend/src/prepare/scene/spawn.rs +++ b/crates/render_frontend/src/prepare/scene/spawn.rs @@ -1,14 +1,12 @@ +use frame::ScopeApp; use std::sync::Arc; use std::sync::Mutex; use bevy::prelude::*; use bevy::render::render_resource::TextureFormat; -use crate::adapters::anim::dyn_ent::DynEntCellBits; -use crate::prepare::scene::camera::FlyCamera; -use crate::prepare::scene::cull::{DynEntModelEntity, ScriptModelEntity, StaticModelEntity}; use crate::prepare::scene::world::WorldScene; -use frame::{LaunchReport, MatchInstalled, MatchTornDown, WorldGeneration}; +use frame::{LaunchReport, WorldGeneration, WorldStamp}; use render_fx::{HostFxSystem, PreparedFxCatalog, PreparedTracers}; pub(crate) use super::world_gpu::WorldGpuReady; @@ -122,7 +120,7 @@ pub struct WorldSpawnJob { pub gpu_wait: super::world_gpu::WorldGpuWait, - pub spawn: WorldGeneration, + pub spawn: WorldStamp, last_slice_at: Option, } @@ -395,14 +393,22 @@ pub(crate) fn spawn_world( programs, exact_shaders, ); - commands.insert_resource( - render_scene::TessMaterials::new( + { + let value = render_scene::TessMaterials::new( std::sync::Arc::clone(&generation.catalog), std::sync::Arc::clone(&generation.prepared), ) - .expect("admitted material publication"), - ); - commands.insert_resource(scene.map_xmodel_scene_assets.clone()); + .expect("admitted material publication"); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; + { + let value = scene.map_xmodel_scene_assets.clone(); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; match &generation.postfx { crate::assemble::drawsurf::RuntimePostFxResources::Ready(film) => diag::info!( World, @@ -416,8 +422,19 @@ pub(crate) fn spawn_world( diag::warn!(World, "post-fx film material: RED cause=Uninstalled") } } - commands.insert_resource(generation); - commands.insert_resource(crate::assemble::drawsurf::MaterialFrameInputs::default()); + { + let value = generation; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; + commands.queue(move |world: &mut World| { + frame::scope::insert( + world, + crate::assemble::drawsurf::MaterialFrameInputs::default(), + frame::MatchScope::Live, + ); + }); match scene.exp_fog { Some(fog) => { diag::info!( @@ -428,7 +445,13 @@ pub(crate) fn spawn_world( fog.max_opacity, fog.sun.is_some() ); - commands.insert_resource(crate::assemble::drawsurf::MapFrameFog::new(fog)); + commands.queue(move |world: &mut World| { + frame::scope::insert( + world, + crate::assemble::drawsurf::MapFrameFog::new(fog), + frame::MatchScope::Live, + ); + }); } None => { commands.remove_resource::(); @@ -451,7 +474,12 @@ pub(crate) fn spawn_world( light.color[1], light.color[2] ); - commands.insert_resource(light); + { + let value = light; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; } None => { commands.remove_resource::(); @@ -468,8 +496,13 @@ pub(crate) fn spawn_world( "drawsurf T5 sunParse exposure: READY {exposure:.4} volumes={}", scene.t5_exposure_volume_count ); - commands - .insert_resource(crate::assemble::drawsurf::MapT5SunParseExposure { exposure }); + commands.queue(move |world: &mut World| { + frame::scope::insert( + world, + crate::assemble::drawsurf::MapT5SunParseExposure { exposure }, + frame::MatchScope::Live, + ) + }); } None => { commands.remove_resource::(); @@ -480,24 +513,36 @@ pub(crate) fn spawn_world( scene.t5_tree_scatter_amount, ) { (Some(intensity), Some(amount)) => { - commands.insert_resource(crate::assemble::drawsurf::MapT5TreeScatter { - intensity, - amount, - }); + { + let value = crate::assemble::drawsurf::MapT5TreeScatter { intensity, amount }; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; } _ => { commands.remove_resource::(); } } - commands.insert_resource(crate::assemble::drawsurf::MapOutdoor { - image: scene - .outdoor_image - .map(|id| crate::assemble::drawsurf::RuntimeImageId(id)), - lookup: scene.outdoor_lookup, - }); - commands.insert_resource(crate::assemble::drawsurf::MapSunEffects { - def: scene.sun_effects, - }); + { + let value = crate::assemble::drawsurf::MapOutdoor { + image: scene + .outdoor_image + .map(|id| crate::assemble::drawsurf::RuntimeImageId(id)), + lookup: scene.outdoor_lookup, + }; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; + { + let value = crate::assemble::drawsurf::MapSunEffects { + def: scene.sun_effects, + }; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; match scene.sun_effects { Some(sun) => diag::info!( World, @@ -528,16 +573,26 @@ pub(crate) fn spawn_world( f32::from_bits(scene.outdoor_lookup[12]) ), } - commands.insert_resource(crate::assemble::drawsurf::MapPrimaryLightTypes { - types: scene.primary_light_types.clone(), - }); - commands.insert_resource(crate::assemble::drawsurf::MapPrimaryLights { - lights: scene.primary_light_pack.clone(), - attenuation: scene.primary_light_attenuation.clone(), - overrides: scene.primary_light_overrides.clone(), - reflection_probe_sh: scene.reflection_probe_sh.clone(), - dynamic: scene.dynamic_light, - }); + { + let value = crate::assemble::drawsurf::MapPrimaryLightTypes { + types: scene.primary_light_types.clone(), + }; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; + { + let value = crate::assemble::drawsurf::MapPrimaryLights { + lights: scene.primary_light_pack.clone(), + attenuation: scene.primary_light_attenuation.clone(), + overrides: scene.primary_light_overrides.clone(), + reflection_probe_sh: scene.reflection_probe_sh.clone(), + dynamic: scene.dynamic_light, + }; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; { let n = scene.primary_light_pack.len(); let falloff_w = scene @@ -659,7 +714,12 @@ pub(crate) fn spawn_world( scene.reflection_probe_origins.len(), ); } - commands.insert_resource(crate::assemble::drawsurf::DrawMethodDfog(false)); + { + let value = crate::assemble::drawsurf::DrawMethodDfog(false); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; if let Some(stage) = job.admit.take_stage() { stage.done(); } @@ -697,8 +757,18 @@ pub(crate) fn spawn_world( fx_catalog.as_deref(), ); let fx_geometry = super::world_plan::empty_fx_model_geometry(&scene); - commands.insert_resource(fx_geometry.0.clone()); - commands.insert_resource(fx_geometry); + { + let value = fx_geometry.0.clone(); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; + { + let value = fx_geometry; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; tess.material_images = std::sync::Arc::new(job.images.exact_handles().to_vec()); if let Some(host) = fx_host.as_mut() { match scene.fx_glass.as_ref() { @@ -746,10 +816,19 @@ pub(crate) fn spawn_world_finish( job.images.probe_handles().to_vec(), job.images.lightmap_handles().to_vec(), ); - // Needs the model lighting atlas, which world_occupancy::place builds. let fx_geometry = super::world_plan::prepare_fx_model_geometry(&scene, fx_models.as_deref()); - commands.insert_resource(fx_geometry.0.clone()); - commands.insert_resource(fx_geometry); + { + let value = fx_geometry.0.clone(); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; + { + let value = fx_geometry; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; tess.material_images = std::sync::Arc::new(job.images.exact_handles().to_vec()); job.last_work_ms = frame_started.elapsed().as_secs_f32() * 1000.0; if paced { @@ -811,10 +890,10 @@ fn record_first_world_frame( } #[derive(Resource, Clone, Default)] -pub(crate) struct WorldPresentAck(Arc>>); +pub(crate) struct WorldPresentAck(Arc>>); impl WorldPresentAck { - fn presented_at(&self, generation: WorldGeneration) -> Option { + fn presented_at(&self, generation: WorldStamp) -> Option { self.0.lock().ok().and_then(|ack| { ack.as_ref() .filter(|(seen, _)| *seen == generation) @@ -824,7 +903,7 @@ impl WorldPresentAck { } #[derive(Resource, Default)] -struct ExtractedWorldPresent(Option); +struct ExtractedWorldPresent(Option); fn log_load_ledger( job: &WorldSpawnJob, @@ -864,119 +943,21 @@ fn finish_world_spawn(scene: &mut WorldScene, job: &mut WorldSpawnJob, commands: job.phase = WorldSpawnPhase::Done; scene.spawned = true; scene.readiness.state = frame::ReadinessState::Ready; - commands.insert_resource(render_scene::WorldPresentFacts { spawned: true }); + { + let value = render_scene::WorldPresentFacts { spawned: true }; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; perf::world_ready(1); } -pub(crate) fn despawn_world_entities_on_teardown( - mut torn: MessageReader, - smodels: Query>, - scripts: Query>, - dynents: Query>, - mut commands: Commands, -) { - if torn.read().count() == 0 { - return; - } - for entity in smodels.iter().chain(scripts.iter()).chain(dynents.iter()) { - commands.entity(entity).despawn(); - } -} - -pub(crate) fn reset_world_spawn_on_teardown( - mut torn: MessageReader, - mut job: ResMut, - mut gpu: ResMut, - mut demand: ResMut, - mut image_handles: Option>, - mut retiring: ResMut, - images: Res>, - mut commands: Commands, -) { - if torn.read().count() == 0 { - return; - } - let live_before = images.len(); - retiring.hand_over(std::mem::take(&mut *job)); - *gpu = WorldGpuReady::default(); - *demand = super::world_gpu::PipelineDemandTracker::default(); - if let Some(handles) = image_handles.as_mut() { - **handles = crate::assemble::drawsurf::RuntimeImageHandles::default(); - } - diag::info!( - World, - "world: render side let go of its images ({live_before} live in Assets at the \ - moment of release; what survives is what another owner still holds)" - ); - commands.insert_resource(crate::assemble::drawsurf::MapSunEffects::default()); - commands.queue(|world: &mut World| { - frame::retire::retire_resources(world, |batch| { - batch - .reset::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::() - .resource::(); - }); - }); -} - -pub(crate) fn shutdown_world_on_teardown( - mut torn: MessageReader, - mut retiring: ResMut, - mut scene: Option>, - mut membership: Option>, - mut present: ResMut, - mut tess: ResMut, - mut lookup: ResMut, - mut cells: ResMut, -) { - if torn.read().count() == 0 { - return; - } - if let Some(scene) = scene.as_mut() { - retiring.hand_over(std::mem::take(&mut **scene)); - } - if let Some(membership) = membership.as_mut() { - **membership = DynEntCellBits::default(); - } - *present = render_scene::WorldPresentFacts::default(); - *tess = render_scene::TessMaterials::default(); - lookup.clear(); - *cells = render_scene::WorldDpvsCells::default(); - let spawned = scene.as_ref().map(|s| i64::from(s.spawned)).unwrap_or(0); - perf::world_hold(spawned, 0, 0); - diag::info!( - World, - "world: shutdown — leftover geometry, tess plans, and model lighting dropped" - ); -} - -pub(crate) fn despawn_fly_cameras_on_teardown( - mut torn: MessageReader, - cameras: Query>, - mut commands: Commands, -) { - if torn.read().count() == 0 { - return; - } - for entity in &cameras { - commands.entity(entity).despawn(); - } -} - pub(crate) fn arm_world_spawn_on_install( - mut installed: MessageReader, + generation: Res, mut job: ResMut, mut gpu: ResMut, ) { - let Some(install) = installed.read().last().cloned() else { - return; - }; - let spawn = WorldGeneration::from_install(install.request_id); + let spawn = generation.stamp(); *job = WorldSpawnJob { spawn, ..Default::default() @@ -989,8 +970,14 @@ pub(crate) fn arm_world_spawn_on_install( pub(crate) fn register_world_gpu_ready(app: &mut App) { super::world_gpu::register_resources(app); - app.init_resource::(); - app.add_systems(Update, supply_requested_shaders.before(spawn_world)); + app.scoped::(frame::MatchScope::Live); + app.add_systems( + Update, + supply_requested_shaders + .in_set(frame::InMatch) + .in_set(frame::ClientSet::Present) + .before(spawn_world), + ); let present_ack = WorldPresentAck::default(); app.insert_resource(present_ack.clone()); let Some(render_app) = app.get_sub_app_mut(bevy::render::RenderApp) else { @@ -1020,8 +1007,8 @@ fn extract_world_present( ) { let generation = main_world .get_resource::() - .copied() - .unwrap_or(WorldGeneration(None)); + .map(|generation| generation.stamp()) + .unwrap_or(WorldStamp(None)); let spawned = main_world .get_resource::() .is_some_and(|scene| scene.spawned && scene.readiness.ready_for(generation)); diff --git a/crates/render_frontend/src/prepare/scene/world_gpu.rs b/crates/render_frontend/src/prepare/scene/world_gpu.rs index 6f60595..f6ff2ff 100644 --- a/crates/render_frontend/src/prepare/scene/world_gpu.rs +++ b/crates/render_frontend/src/prepare/scene/world_gpu.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; -use frame::WorldGeneration; +use frame::ScopeApp; +use frame::{WorldGeneration, WorldStamp}; use super::spawn::{WorldSpawnJob, WorldSpawnPhase}; use std::collections::HashSet; @@ -65,7 +66,7 @@ impl WorldGpuWait { #[derive(Clone, Copy, Debug, Default)] pub struct OverlayWarmup { - pub generation: WorldGeneration, + pub generation: WorldStamp, pub initialized: bool, pub total: u32, pub ready: u32, @@ -73,7 +74,7 @@ pub struct OverlayWarmup { #[derive(Resource, Debug, Default, Clone, Copy)] pub struct WorldGpuReady { - pub spawn: WorldGeneration, + pub spawn: WorldStamp, pub images: bool, pub pipelines: bool, pub waiting_n: u32, @@ -87,14 +88,14 @@ pub struct WorldGpuReady { } impl WorldGpuReady { - fn live_for(&self, spawn: WorldGeneration) -> bool { + fn live_for(&self, spawn: WorldStamp) -> bool { self.spawn.0.is_some() && self.spawn == spawn && self.images && self.pipelines } } #[derive(Resource, Clone, Debug, Default)] pub struct GpuSubmitDemand { - pub world_generation: WorldGeneration, + pub world_generation: WorldStamp, pub warm_pipelines: bool, @@ -136,7 +137,7 @@ impl PipelineDemandTracker { #[derive(Resource, Clone, Debug, Default)] pub struct GpuLoadProgress { - pub generation: WorldGeneration, + pub generation: WorldStamp, pub waiting_n: u32, pub pipeline_n: u32, pub warmup: OverlayWarmup, @@ -156,10 +157,10 @@ pub struct GpuImageResidency { } pub fn spawn_gpu_extract_live( - gpu_spawn: WorldGeneration, - job_spawn: Option, + gpu_spawn: WorldStamp, + job_spawn: Option, overlay_gpu_wait: bool, - world_gen: WorldGeneration, + world_gen: WorldStamp, ) -> bool { world_gen.0.is_some() && gpu_spawn == world_gen @@ -183,7 +184,7 @@ pub(crate) fn publish_gpu_submit_demand( | WorldSpawnPhase::Gpu ) }); - let world_generation = generation.map(|g| *g).unwrap_or_default(); + let world_generation = generation.map(|g| g.stamp()).unwrap_or_default(); let overlay_gpu_wait = spawn .as_ref() .is_some_and(|job| job.phase == WorldSpawnPhase::Gpu); @@ -216,7 +217,7 @@ pub(crate) fn publish_gpu_submit_demand( pub(crate) fn poll( wait: &mut WorldGpuWait, - spawn: WorldGeneration, + spawn: WorldStamp, gpu: Option<&WorldGpuReady>, gap_ms: f32, ) -> bool { @@ -286,16 +287,21 @@ pub(crate) fn poll( } pub(crate) fn register_resources(app: &mut App) { - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .add_systems( Update, ( - consume_gpu_load_progress.before(super::spawn::spawn_world), - publish_gpu_submit_demand.after(super::spawn::spawn_world), - ), + consume_gpu_load_progress + .in_set(frame::InMatch) + .before(super::spawn::spawn_world), + publish_gpu_submit_demand + .in_set(frame::InMatch) + .after(super::spawn::spawn_world), + ) + .in_set(frame::ClientSet::Present), ); } @@ -342,7 +348,7 @@ pub(crate) fn consume_gpu_load_progress( if let Some(job) = job.as_mut() { job.request_pending_shaders(&progress.pending_shaders); } - let world_gen = generation.map(|g| *g).unwrap_or(WorldGeneration(None)); + let world_gen = generation.map(|g| g.stamp()).unwrap_or(WorldStamp(None)); ready.waiting_n = progress.waiting_n; ready.pipeline_n = progress.pipeline_n; ready.warmup = if progress.generation == world_gen { diff --git a/crates/render_frontend/src/prepare/scene/world_images.rs b/crates/render_frontend/src/prepare/scene/world_images.rs index ed11191..f5e469b 100644 --- a/crates/render_frontend/src/prepare/scene/world_images.rs +++ b/crates/render_frontend/src/prepare/scene/world_images.rs @@ -235,6 +235,8 @@ pub struct WorldImageUpload { /// texture twice and keeps two copies of it resident, so the second slot /// takes a clone of the first one's handle and drops its own copy. exact_by_variant: std::collections::HashMap>, + // Weak headers prevent pointer reuse without retaining pixel buffers. + exact_by_image: std::collections::HashMap, Handle)>, /// Slots that took another slot's handle, and the bytes that saved. pub reused_handles: u32, pub reused_handle_bytes: u64, @@ -333,6 +335,7 @@ impl WorldImageUpload { self.exact_variants = std::mem::take(&mut scene.exact_material_variants); self.exact_variants.resize(self.exact_images.len(), None); self.exact_by_variant.clear(); + self.exact_by_image.clear(); self.reused_handles = 0; self.reused_handle_bytes = 0; self.exact_handles = vec![None; self.exact_images.len()]; @@ -473,11 +476,28 @@ impl WorldImageUpload { let variant = self.exact_variants[self.exact_at]; let common_owned = self.exact_common[self.exact_at]; self.exact_handles[self.exact_at] = image.map(|image| { + let identity = Arc::as_ptr(&image) as usize; + if let Some((_, handle)) = self.exact_by_image.get(&identity) { + let handle = handle.clone(); + if let Some(variant) = variant { + self.exact_by_variant.insert(variant, handle.clone()); + if common_owned && self.common_profile_id != 0 { + common.by_variant.insert(variant, handle.clone()); + } + } + self.reused_handles = self.reused_handles.saturating_add(1); + self.reused_handle_bytes = self.reused_handle_bytes.saturating_add(bytes); + handed = 0; + return handle; + } // A slot whose variant is already an asset takes that handle // and lets its own copy go: the two are the same texels under // the same sampler, and a second `add` is a second texture. let Some(variant) = variant else { - return images.add((*image).clone()); + let handle = images.add((*image).clone()); + self.exact_by_image + .insert(identity, (Arc::downgrade(&image), handle.clone())); + return handle; }; if let Some(handle) = self.exact_by_variant.get(&variant) { if common_owned && self.common_profile_id != 0 { @@ -488,6 +508,8 @@ impl WorldImageUpload { // Nothing was handed to the asset server: the slot took a // handle that already existed. handed = 0; + self.exact_by_image + .insert(identity, (Arc::downgrade(&image), handle.clone())); return handle.clone(); } if common_owned @@ -500,9 +522,13 @@ impl WorldImageUpload { self.reused_common_bytes = self.reused_common_bytes.saturating_add(bytes); handed = 0; self.exact_by_variant.insert(variant, handle.clone()); + self.exact_by_image + .insert(identity, (Arc::downgrade(&image), handle.clone())); return handle; } let handle = images.add((*image).clone()); + self.exact_by_image + .insert(identity, (Arc::downgrade(&image), handle.clone())); if common_owned && self.common_profile_id != 0 { common.by_variant.insert(variant, handle.clone()); } diff --git a/crates/render_frontend/src/prepare/scene/world_occupancy.rs b/crates/render_frontend/src/prepare/scene/world_occupancy.rs index 0bd17ea..0a64ce1 100644 --- a/crates/render_frontend/src/prepare/scene/world_occupancy.rs +++ b/crates/render_frontend/src/prepare/scene/world_occupancy.rs @@ -146,7 +146,12 @@ pub fn place( sample_without_technique, technique_without_sample, ); - commands.insert_resource(lighting); + { + let value = lighting; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; } else if !scene.smodel_lighting_samples.is_empty() { let dims = lighting_iw4::model_lighting_atlas_dims(SMODEL_LIGHTING_MAX_CLIENT_VIEWS); diag::info!( @@ -158,9 +163,13 @@ pub fn place( ); } - commands.insert_resource( - crate::prepare::scene::smodel_geom_cache::WorldStaticModelCache::new(slot_count), - ); + { + let value = + crate::prepare::scene::smodel_geom_cache::WorldStaticModelCache::new(slot_count); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; diag::info!( World, @@ -247,27 +256,47 @@ pub fn place( dyn_ent_brush_n, ); - commands.insert_resource(map_xmodel_scene_assets); + { + let value = map_xmodel_scene_assets; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; if let Some(atlas) = model_lighting_atlas { let cache = crate::prepare::scene::model_lighting_cache::WorldModelLightingCache::new(atlas.dims); scene.model_lighting_image = Some(atlas.image.clone()); scene.model_lighting_dims = Some(atlas.dims); - commands.insert_resource(crate::assemble::drawsurf::RuntimeImageHandles::from_pools( - scene.runtime_material_catalog.generation_id(), - exact_material_handles, - scene.exact_material_names.clone(), - reflection_probe_handles, - lightmap_handles, - Some(atlas.image.clone()), - )); + { + let value = crate::assemble::drawsurf::RuntimeImageHandles::from_pools( + scene.runtime_material_catalog.generation_id(), + exact_material_handles, + scene.exact_material_names.clone(), + reflection_probe_handles, + lightmap_handles, + Some(atlas.image.clone()), + ); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; diag::info!( World, "drawsurf typed image registry: model_lighting=READY code_texture=3 sampler=0xe2 volume=D3" ); - commands.insert_resource(cache); - commands.insert_resource(atlas); + { + let value = cache; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; + { + let value = atlas; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; } let transform = if let Some(view) = scene.intermission_view { @@ -313,8 +342,9 @@ pub fn place( .into(), ..default() }, - CompositingSpace::Srgb, + frame::DisplayEncodedCamera, Tonemapping::None, + bevy::render::view::DebandDither::Disabled, Msaa::Off, Transform::IDENTITY, Projection::Perspective(PerspectiveProjection { diff --git a/crates/render_frontend/src/prepare/scene/world_plan.rs b/crates/render_frontend/src/prepare/scene/world_plan.rs index 386224f..282f5ac 100644 --- a/crates/render_frontend/src/prepare/scene/world_plan.rs +++ b/crates/render_frontend/src/prepare/scene/world_plan.rs @@ -380,7 +380,12 @@ pub fn install( fx_catalog: Option<&PreparedFxCatalog>, ) { let fx_images = world_color_images(scene, &exact_material_handles, tracers, fx_catalog); - commands.insert_resource(fx_images); + { + let value = fx_images; + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; let exact_material_views = exact_material_handles.iter().flatten().count(); let exact_probe_views = reflection_probe_handles.iter().flatten().count(); let exact_lightmap_views = lightmap_handles @@ -388,14 +393,19 @@ pub fn install( .flatten() .filter(|page| page.primary.is_some() && page.secondary.is_some()) .count(); - commands.insert_resource(RuntimeImageHandles::from_pools( - scene.runtime_material_catalog.generation_id(), - exact_material_handles.clone(), - scene.exact_material_names.clone(), - reflection_probe_handles.clone(), - lightmap_handles.clone(), - None, - )); + { + let value = RuntimeImageHandles::from_pools( + scene.runtime_material_catalog.generation_id(), + exact_material_handles.clone(), + scene.exact_material_names.clone(), + reflection_probe_handles.clone(), + lightmap_handles.clone(), + None, + ); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, value, frame::MatchScope::Live); + }); + }; log_image_asset_memory(images); diag::info!( World, diff --git a/crates/render_frontend/src/prepare/worker_cmds.rs b/crates/render_frontend/src/prepare/worker_cmds.rs index 02a27a3..97f023d 100644 --- a/crates/render_frontend/src/prepare/worker_cmds.rs +++ b/crates/render_frontend/src/prepare/worker_cmds.rs @@ -169,9 +169,6 @@ pub struct SkinCachedStaticModelCmd { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CellFrustumWorkerCmd { - /// Which planes cull the cell's entities: [`CELL_VISIT_FIRST`] the cell's first portal visit - /// (`DpvsFrameStats::cell_clips`), [`CELL_VISIT_FRUSTUM`] the camera frustum, else the - /// 1-based index of one portal visit in `DpvsFrameStats::cell_clip_visits`. pub visit: u32, pub cell: u32, pub plane_count: u8, @@ -179,9 +176,7 @@ pub struct CellFrustumWorkerCmd { pub view: u16, } -/// [`CellFrustumWorkerCmd::visit`]: the cell's first-visit planes. pub const CELL_VISIT_FIRST: u32 = 0; -/// [`CellFrustumWorkerCmd::visit`]: the camera frustum alone. pub const CELL_VISIT_FRUSTUM: u32 = u32::MAX; impl CellFrustumWorkerCmd { diff --git a/crates/render_fx/src/combat.rs b/crates/render_fx/src/combat.rs index 2df9d3f..b895e6c 100644 --- a/crates/render_fx/src/combat.rs +++ b/crates/render_fx/src/combat.rs @@ -182,7 +182,7 @@ pub struct PelletFxReport { #[allow(clippy::too_many_arguments)] pub fn present_pellet_segment( - generation: frame::WorldGeneration, + generation: frame::WorldStamp, timeline: u64, request: FireFxRequest, segment: u16, diff --git a/crates/render_fx/src/fire_occurrence.rs b/crates/render_fx/src/fire_occurrence.rs index 2b5dc59..7dbda2b 100644 --- a/crates/render_fx/src/fire_occurrence.rs +++ b/crates/render_fx/src/fire_occurrence.rs @@ -55,7 +55,7 @@ impl FireFxOutcome { #[derive(Clone, Copy)] pub struct FireFxRequest { - pub world: frame::WorldGeneration, + pub world: frame::WorldStamp, pub timeline: u64, pub domain: net::EntityEventDomain, pub cause: Option, @@ -133,7 +133,7 @@ pub struct PresentedFireFx { } impl PresentedFireFx { - fn adopt_scope(&mut self, generation: frame::WorldGeneration, timeline: u64, now: i32) -> bool { + fn adopt_scope(&mut self, generation: frame::WorldStamp, timeline: u64, now: i32) -> bool { let scope = (generation.0, timeline); let changed = self.scope != Some(scope) || self @@ -150,12 +150,7 @@ impl PresentedFireFx { changed } - pub fn maintain( - &mut self, - generation: frame::WorldGeneration, - timeline: u64, - now: i32, - ) -> bool { + pub fn maintain(&mut self, generation: frame::WorldStamp, timeline: u64, now: i32) -> bool { let started = Instant::now(); let changed = self.adopt_scope(generation, timeline, now); self.stats.maintenance_sweeps += 1; @@ -173,7 +168,7 @@ impl PresentedFireFx { pub fn execute( &mut self, - generation: frame::WorldGeneration, + generation: frame::WorldStamp, timeline: u64, request: FireFxRequest, verdicts: &net::FireVerdictState, diff --git a/crates/render_fx/src/plugin.rs b/crates/render_fx/src/plugin.rs index 0bceecb..2dc36c1 100644 --- a/crates/render_fx/src/plugin.rs +++ b/crates/render_fx/src/plugin.rs @@ -1,32 +1,29 @@ use bevy::prelude::*; +use frame::ScopeApp; pub struct RenderFxPlugin; impl Plugin for RenderFxPlugin { fn build(&self, app: &mut App) { - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::(); + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live); crate::system::register_fx_orchestration(app); } } diff --git a/crates/render_fx/src/present.rs b/crates/render_fx/src/present.rs index 5fe39eb..8178b88 100644 --- a/crates/render_fx/src/present.rs +++ b/crates/render_fx/src/present.rs @@ -673,7 +673,6 @@ fn eval_pending_collide( elem.view.coll_maxs, elem.view.use_item_clip != 0, !elem.effect_on_impact.is_absent(), - q.elem_random_seed, elem.vel_graph_local.as_slice(), elem.vel_graph_world.as_slice(), q, @@ -693,7 +692,6 @@ fn eval_pending_collide( elem.view.flags, elem.view.gravity_base, elem.view.gravity_amplitude, - q.elem_random_seed, elem.vel_graph_local.as_slice(), elem.vel_graph_world.as_slice(), q, @@ -847,7 +845,6 @@ fn tick_fx_pass( elem.view.coll_maxs, elem.view.use_item_clip != 0, !elem.effect_on_impact.is_absent(), - q.elem_random_seed, elem.vel_graph_local.as_slice(), elem.vel_graph_world.as_slice(), q, @@ -866,7 +863,6 @@ fn tick_fx_pass( elem.view.flags, elem.view.gravity_base, elem.view.gravity_amplitude, - q.elem_random_seed, elem.vel_graph_local.as_slice(), elem.vel_graph_world.as_slice(), q, diff --git a/crates/render_fx/src/system.rs b/crates/render_fx/src/system.rs index db6a530..fd395e5 100644 --- a/crates/render_fx/src/system.rs +++ b/crates/render_fx/src/system.rs @@ -1,5 +1,5 @@ use bevy::prelude::*; -use frame::{FxSoundPublished, MatchTornDown, SessionSwapApplied}; +use frame::FxSoundPublished; use fx::{FxGapCause, FxMsec, set_presentation_clock}; use net::{ ClientSet, FrameClock, GameActive, LastAdoptedSnapshot, advance_cg_frame_clock, @@ -8,41 +8,40 @@ use net::{ use render_anim::sync_camera_from_presented; use render_scene::{FlyCamera, WorldPresentFacts}; -use crate::{ - EntityMarks, FxCameraOrigin, FxJournalCursor, HostFxDlights, HostFxPostLights, HostFxSystem, - PreparedFxCatalog, PreparedFxModels, PreparedImpactFx, PreparedTracers, PresentedVehicleFx, - TracerDrawGate, TracerWorld, -}; +use crate::{FxCameraOrigin, HostFxSystem, PreparedFxCatalog, PresentedVehicleFx}; pub fn register_fx_orchestration(app: &mut App) { app.add_systems( Update, ( - kill_fx_on_match_torn_down - .in_set(ClientSet::Load) - .after(SessionSwapApplied), latch_cgame_active + .in_set(frame::InMatch) .in_set(ClientSet::Reconcile) .after(reconcile_prediction) .before(advance_cg_frame_clock), stamp_presentation_clock + .in_set(frame::InMatch) .in_set(ClientSet::Reconcile) .after(advance_cg_frame_clock), ), ) .add_systems( Update, - clear_presented_vehicle_fx.in_set(ClientSet::Receive), + clear_presented_vehicle_fx + .in_set(frame::InMatch) + .in_set(ClientSet::Receive), ) .add_systems( Update, stamp_fx_camera_origin + .in_set(frame::InMatch) .after(sync_camera_from_presented) .in_set(ClientSet::Present), ) .add_systems( Update, play_pending_fx_sounds + .in_set(frame::InMatch) .before(FxSoundPublished) .in_set(ClientSet::Effects), ); @@ -59,37 +58,6 @@ pub fn stamp_fx_camera_origin( .unwrap_or([0.0; 3]); } -fn kill_fx_on_match_torn_down( - mut torn: MessageReader, - mut host: ResMut, - mut cursor: ResMut, - mut dlights: ResMut, - mut post_lights: ResMut, - mut tracers: ResMut, - mut tracer_gate: ResMut, - mut entity_marks: ResMut, - mut commands: Commands, -) { - if torn.read().count() == 0 { - return; - } - *host = HostFxSystem::default(); - cursor.createfx_booted = false; - cursor.createfx_boot_msec = None; - *dlights = HostFxDlights::default(); - *post_lights = HostFxPostLights::default(); - *tracers = TracerWorld::default(); - *tracer_gate = TracerDrawGate::default(); - *entity_marks = EntityMarks::default(); - commands.remove_resource::(); - commands.remove_resource::(); - commands.remove_resource::(); - commands.insert_resource(crate::FxModelDrawPlan::default()); - commands.insert_resource(crate::FxModelStaging::default()); - commands.remove_resource::(); - commands.remove_resource::(); -} - fn latch_cgame_active( facts: Res, adopted: Option>, diff --git a/crates/render_fx/src/tracer.rs b/crates/render_fx/src/tracer.rs index 4670fe6..b963d3c 100644 --- a/crates/render_fx/src/tracer.rs +++ b/crates/render_fx/src/tracer.rs @@ -26,7 +26,7 @@ pub struct TracerDrawGate { } impl TracerDrawGate { - pub fn adopt_scope(&mut self, generation: frame::WorldGeneration, timeline: u64) { + pub fn adopt_scope(&mut self, generation: frame::WorldStamp, timeline: u64) { let scope = (generation.0, timeline); if self.scope != Some(scope) { *self = Self { diff --git a/crates/render_gpu/src/drawsurf/colour_submit/geometry.rs b/crates/render_gpu/src/drawsurf/colour_submit/geometry.rs index 0446c5a..59355c3 100644 --- a/crates/render_gpu/src/drawsurf/colour_submit/geometry.rs +++ b/crates/render_gpu/src/drawsurf/colour_submit/geometry.rs @@ -12,7 +12,7 @@ use render_material::MaterialGenerationId; #[derive(Resource, Default)] pub(super) struct ExactColourGeometry { pub(super) generation: MaterialGenerationId, - pub(super) world_generation: frame::WorldGeneration, + pub(super) world_generation: frame::WorldStamp, pub(super) world_products: frame::WorldProducts, pub(super) world_vertex: Option, pub(super) world_layer: Option, diff --git a/crates/render_gpu/src/drawsurf/colour_submit/mod.rs b/crates/render_gpu/src/drawsurf/colour_submit/mod.rs index 08bc83b..b237e1b 100644 --- a/crates/render_gpu/src/drawsurf/colour_submit/mod.rs +++ b/crates/render_gpu/src/drawsurf/colour_submit/mod.rs @@ -270,9 +270,10 @@ impl ExactColourTargetViews { fn exact_fragment_entry( target: TextureFormat, forward_z: bool, + depth_to_colour: bool, alpha_test: Option, ) -> String { - if forward_z && target == TextureFormat::R32Float { + if forward_z && target == TextureFormat::R32Float && !depth_to_colour { PASS_FRAGMENT_ENTRY.to_owned() } else { alpha_test_fragment_entry(alpha_test) @@ -297,6 +298,7 @@ fn exact_pipeline_plan( } else { Some(CompareFunction::Always) }; + let depth_to_colour = port.port.abi().depth_to_colour; let cached_lighting = key.cached_lighting && port.cached_source.is_some(); let source = match (cached_lighting, port.cached_source.as_ref()) { (true, Some(cached)) => ExactModuleSource::CachedLighting(cached.clone()), @@ -307,7 +309,12 @@ fn exact_pipeline_plan( "iw4_exact_colour/{:016x}/{}", key.port.vertex_program_hash, key.port.vertex_type ), - fragment_entry: exact_fragment_entry(key.target, key.forward_z, key.state0.alpha_test), + fragment_entry: exact_fragment_entry( + key.target, + key.forward_z, + depth_to_colour, + key.state0.alpha_test, + ), vertex_buffers: if cached_lighting && !port.cached_vertex_buffers.is_empty() { port.cached_vertex_buffers.clone() } else { @@ -328,7 +335,9 @@ fn exact_pipeline_plan( } else { key.state0.blend.blend_state() }, - write_mask: if key.target == TextureFormat::R32Float { + write_mask: if key.target == TextureFormat::R32Float && depth_to_colour { + ColorWrites::RED + } else if key.target == TextureFormat::R32Float { key.state0.colour_writes() & ColorWrites::RED } else { key.state0.colour_writes() @@ -543,7 +552,7 @@ fn colour_census_ms(start: Option) -> Option { #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct WorldPretessKey { - world_generation: frame::WorldGeneration, + world_generation: frame::WorldStamp, geometry_generation: MaterialGenerationId, world_run_revision: u64, colour_world: u64, @@ -649,7 +658,7 @@ fn world_pretess_key( light: &FrameProduct, emissive: &FrameProduct, run_surfs: &[u16], - world_generation: frame::WorldGeneration, + world_generation: frame::WorldStamp, geometry_generation: MaterialGenerationId, world_run_revision: u64, ) -> WorldPretessKey { @@ -802,13 +811,13 @@ pub fn colour_ports_static( } pub fn colour_world_smodel_static( - (gpu_world_generation, gpu_world_products): (frame::WorldGeneration, frame::WorldProducts), + (gpu_world_generation, gpu_world_products): (frame::WorldStamp, frame::WorldProducts), gpu_world_verts: usize, gpu_world_indices: usize, gpu_world_layer: usize, gpu_smodel_verts: usize, gpu_smodel_indices: usize, - (cpu_world_generation, cpu_world_products): (frame::WorldGeneration, frame::WorldProducts), + (cpu_world_generation, cpu_world_products): (frame::WorldStamp, frame::WorldProducts), cpu_world_verts: usize, cpu_world_indices: usize, cpu_world_layer: usize, diff --git a/crates/render_gpu/src/drawsurf/colour_submit/publication.rs b/crates/render_gpu/src/drawsurf/colour_submit/publication.rs index 0ca2e4f..3d2beaa 100644 --- a/crates/render_gpu/src/drawsurf/colour_submit/publication.rs +++ b/crates/render_gpu/src/drawsurf/colour_submit/publication.rs @@ -40,7 +40,7 @@ impl std::ops::Deref for InstalledRenderWorld { #[derive(Clone, Debug, Default)] pub struct RenderWorldData { pub generation: MaterialGenerationId, - pub world_generation: frame::WorldGeneration, + pub world_generation: frame::WorldStamp, pub world_products: frame::WorldProducts, pub smc_revision: Option, pub ports: Arc>, @@ -88,7 +88,7 @@ impl std::ops::Deref for PublishedRenderFrame { pub struct RenderFrameData { pub frame_products: ExtractedRenderFrameProducts, pub generation: MaterialGenerationId, - pub world_generation: frame::WorldGeneration, + pub world_generation: frame::WorldStamp, pub sun_shadow: Option, pub sun_effects: Option, pub warm_pipelines: bool, diff --git a/crates/render_gpu/src/drawsurf/exact_pipeline.rs b/crates/render_gpu/src/drawsurf/exact_pipeline.rs index 19cc848..2a1ccec 100644 --- a/crates/render_gpu/src/drawsurf/exact_pipeline.rs +++ b/crates/render_gpu/src/drawsurf/exact_pipeline.rs @@ -64,7 +64,7 @@ struct PortBuild { #[derive(Resource, Default)] pub(super) struct ExactPipelineRegistry { - world: frame::WorldGeneration, + world: frame::WorldStamp, slots: Vec, by_key: HashMap, modules: HashMap>, @@ -84,7 +84,7 @@ pub(super) struct ExactPipelineRegistry { } impl ExactPipelineRegistry { - pub(super) fn set_world(&mut self, world: frame::WorldGeneration) -> bool { + pub(super) fn set_world(&mut self, world: frame::WorldStamp) -> bool { if self.world == world { return false; } @@ -264,13 +264,15 @@ fn build_pipeline( bind_group_layouts: &[Some(&plan.constants_layout), Some(&plan.textures_layout)], immediate_size: 0, }); - let buffers: Vec = plan + let buffers: Vec> = plan .vertex_buffers .iter() - .map(|buffer| RawVertexBufferLayout { - array_stride: buffer.array_stride, - attributes: &buffer.attributes, - step_mode: buffer.step_mode, + .map(|buffer| { + Some(RawVertexBufferLayout { + array_stride: buffer.array_stride, + attributes: &buffer.attributes, + step_mode: buffer.step_mode, + }) }) .collect(); let compilation_options = PipelineCompilationOptions { diff --git a/crates/render_gpu/src/drawsurf/floatz.rs b/crates/render_gpu/src/drawsurf/floatz.rs index 91f584e..98aa2e7 100644 --- a/crates/render_gpu/src/drawsurf/floatz.rs +++ b/crates/render_gpu/src/drawsurf/floatz.rs @@ -18,7 +18,7 @@ use bevy::render::render_resource::{ use bevy::render::renderer::{RenderDevice, RenderQueue}; use bevy::shader::Shader; -const SHADER_PATH: &str = "embedded://render_gpu/drawsurf/floatz.wgsl"; +const SHADER_PATH: &str = "embedded://render_gpu/drawsurf/floatz.wesl"; const PARAMS_SIZE: u64 = 16; @@ -76,12 +76,14 @@ impl SpecializedRenderPipeline for ExactFloatZResolve { vertex: VertexState { shader: self.shader.clone(), shader_defs: shader_defs.clone(), + constants: Vec::new(), entry_point: Some("vs_fullscreen".into()), buffers: Vec::new(), }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, + constants: Vec::new(), entry_point: Some("fs_floatz".into()), targets: vec![Some(ColorTargetState { format: TextureFormat::R32Float, @@ -313,7 +315,7 @@ pub(super) fn blit_depth( } pub(super) fn register(app: &mut App) { - bevy::asset::embedded_asset!(app, "floatz.wgsl"); + bevy::asset::embedded_asset!(app, "floatz.wesl"); let Some(render_app) = app.get_sub_app_mut(bevy::render::RenderApp) else { return; }; diff --git a/crates/render_gpu/src/drawsurf/floatz.wgsl b/crates/render_gpu/src/drawsurf/floatz.wesl similarity index 96% rename from crates/render_gpu/src/drawsurf/floatz.wgsl rename to crates/render_gpu/src/drawsurf/floatz.wesl index 9aa9d4c..99501a5 100644 --- a/crates/render_gpu/src/drawsurf/floatz.wgsl +++ b/crates/render_gpu/src/drawsurf/floatz.wesl @@ -14,11 +14,10 @@ fn vs_fullscreen(@builtin(vertex_index) vertex_index: u32) -> VsOut { return out; } -#ifdef MULTISAMPLED +@if(MULTISAMPLED) @group(0) @binding(0) var depth_tex: texture_depth_multisampled_2d; -#else +@else @group(0) @binding(0) var depth_tex: texture_depth_2d; -#endif @group(0) @binding(1) var params: Params; @fragment diff --git a/crates/render_gpu/src/drawsurf/geometry_diagnostic.rs b/crates/render_gpu/src/drawsurf/geometry_diagnostic.rs index a882075..a631791 100644 --- a/crates/render_gpu/src/drawsurf/geometry_diagnostic.rs +++ b/crates/render_gpu/src/drawsurf/geometry_diagnostic.rs @@ -33,7 +33,7 @@ use render_frame::{ }; use render_material::MaterialGenerationId; -const SHADER_PATH: &str = "embedded://render_gpu/drawsurf/geometry_diagnostic.wgsl"; +const SHADER_PATH: &str = "embedded://render_gpu/drawsurf/geometry_diagnostic.wesl"; fn geometry_diagnostic_covers(kind: &RetainedDrawKind, key: u64) -> bool { !matches!( @@ -176,6 +176,7 @@ impl SpecializedRenderPipeline for DiagnosticPipeline { vertex: VertexState { shader: self.shader.clone(), shader_defs: Vec::new(), + constants: Vec::new(), entry_point: Some(match key.tess { DiagnosticTess::World => "vertex".into(), DiagnosticTess::Smodel | DiagnosticTess::XModel => "vertex_smodel".into(), @@ -190,6 +191,7 @@ impl SpecializedRenderPipeline for DiagnosticPipeline { fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs: Vec::new(), + constants: Vec::new(), entry_point: Some("fragment".into()), targets: vec![Some(ColorTargetState { format: key.target, @@ -779,7 +781,7 @@ pub(super) fn register(app: &mut App) { if !geometry_diagnostic_enabled() { return; } - bevy::asset::embedded_asset!(app, "geometry_diagnostic.wgsl"); + bevy::asset::embedded_asset!(app, "geometry_diagnostic.wesl"); let Some(render_app) = app.get_sub_app_mut(bevy::render::RenderApp) else { return; }; diff --git a/crates/render_gpu/src/drawsurf/geometry_diagnostic.wgsl b/crates/render_gpu/src/drawsurf/geometry_diagnostic.wesl similarity index 98% rename from crates/render_gpu/src/drawsurf/geometry_diagnostic.wgsl rename to crates/render_gpu/src/drawsurf/geometry_diagnostic.wesl index 245c50a..5a5b147 100644 --- a/crates/render_gpu/src/drawsurf/geometry_diagnostic.wgsl +++ b/crates/render_gpu/src/drawsurf/geometry_diagnostic.wesl @@ -1,4 +1,4 @@ -#import bevy_render::view::View +import bevy_render::view::View; @group(0) @binding(0) var view: View; diff --git a/crates/render_gpu/src/drawsurf/gpu_resources.rs b/crates/render_gpu/src/drawsurf/gpu_resources.rs index a5eff78..d276895 100644 --- a/crates/render_gpu/src/drawsurf/gpu_resources.rs +++ b/crates/render_gpu/src/drawsurf/gpu_resources.rs @@ -35,7 +35,7 @@ pub struct ColourWorkingSet { #[derive(Resource, Debug, Default, Clone, Copy)] pub struct WorldPipelineWarmup { - pub generation: frame::WorldGeneration, + pub generation: frame::WorldStamp, pub initialized: bool, pub total: u32, pub ready: u32, @@ -43,7 +43,7 @@ pub struct WorldPipelineWarmup { #[derive(Resource, Clone, Debug, Default)] pub struct GpuSubmitReady { - pub world_generation: frame::WorldGeneration, + pub world_generation: frame::WorldStamp, pub warm_pipelines: bool, diff --git a/crates/render_gpu/src/drawsurf/hud_blood.rs b/crates/render_gpu/src/drawsurf/hud_blood.rs index 04c7b12..745cb57 100644 --- a/crates/render_gpu/src/drawsurf/hud_blood.rs +++ b/crates/render_gpu/src/drawsurf/hud_blood.rs @@ -102,12 +102,14 @@ impl BloodPortGpu { vertex: VertexState { shader: blood.film.shader.clone(), shader_defs: Vec::new(), + constants: Vec::new(), entry_point: Some(PASS_VERTEX_ENTRY.into()), buffers: vertex_layouts, }, fragment: Some(FragmentState { shader: blood.film.shader.clone(), shader_defs: Vec::new(), + constants: Vec::new(), entry_point: Some(PASS_FRAGMENT_ENTRY.into()), targets: vec![Some(ColorTargetState { format, diff --git a/crates/render_gpu/src/drawsurf/iw_tess.rs b/crates/render_gpu/src/drawsurf/iw_tess.rs index 0e4edea..793a480 100644 --- a/crates/render_gpu/src/drawsurf/iw_tess.rs +++ b/crates/render_gpu/src/drawsurf/iw_tess.rs @@ -151,6 +151,7 @@ impl SpecializedRenderPipeline for IwTessPipeline { vertex: VertexState { shader: self.shader.clone(), shader_defs: Vec::new(), + constants: Vec::new(), entry_point: Some("vs_tess".into()), buffers: vec![VertexBufferLayout { array_stride: 32, @@ -177,6 +178,7 @@ impl SpecializedRenderPipeline for IwTessPipeline { fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs: Vec::new(), + constants: Vec::new(), entry_point: Some("fs_tess".into()), targets: vec![Some(ColorTargetState { format: key.target, diff --git a/crates/render_gpu/src/drawsurf/postfx.rs b/crates/render_gpu/src/drawsurf/postfx.rs index f66c521..c265fa5 100644 --- a/crates/render_gpu/src/drawsurf/postfx.rs +++ b/crates/render_gpu/src/drawsurf/postfx.rs @@ -501,12 +501,14 @@ fn queue_postfx_pipeline( vertex: VertexState { shader: film.shader.clone(), shader_defs: Vec::new(), + constants: Vec::new(), entry_point: Some(PASS_VERTEX_ENTRY.into()), buffers: vertex_layouts, }, fragment: Some(FragmentState { shader: film.shader.clone(), shader_defs: Vec::new(), + constants: Vec::new(), entry_point: Some(PASS_FRAGMENT_ENTRY.into()), targets: vec![Some(ColorTargetState { format: if state.srgb_write_enable() { diff --git a/crates/render_gpu/src/drawsurf/scene_depth.rs b/crates/render_gpu/src/drawsurf/scene_depth.rs index 1e6d867..d934b7f 100644 --- a/crates/render_gpu/src/drawsurf/scene_depth.rs +++ b/crates/render_gpu/src/drawsurf/scene_depth.rs @@ -4,14 +4,14 @@ use bevy::render::camera::ExtractedCamera; use bevy::render::render_resource::*; use bevy::render::renderer::RenderDevice; use bevy::render::texture::TextureCache; -use bevy::render::view::{Msaa, ViewDepthTexture}; +use bevy::render::view::{Msaa, ViewDepthStencilTexture}; pub const SCENE_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth24PlusStencil8; #[derive(Component)] pub struct SceneDepthTexture { pub texture: Texture, - attachment: ViewDepthTexture, + attachment: ViewDepthStencilTexture, sampled: TextureView, } @@ -21,20 +21,7 @@ impl SceneDepthTexture { } pub fn get_attachment(&self, store: StoreOp) -> RenderPassDepthStencilAttachment<'_> { - let mut attachment = self.attachment.get_attachment(store); - attachment.stencil_ops = Some(Operations { - load: if attachment - .depth_ops - .as_ref() - .is_some_and(|ops| matches!(ops.load, LoadOp::Clear(_))) - { - LoadOp::Clear(0) - } else { - LoadOp::Load - }, - store: StoreOp::Store, - }); - attachment + self.attachment.get_attachment(store) } } @@ -88,12 +75,13 @@ pub(crate) fn prepare_scene_depth( commands.entity(entity).insert(SceneDepthTexture { texture: raw, sampled, - attachment: ViewDepthTexture::new( + attachment: ViewDepthStencilTexture::new( texture, match settings.depth_load_op { Camera3dDepthLoadOp::Clear(value) => Some(value), Camera3dDepthLoadOp::Load => None, }, + Some(0), ), }); } diff --git a/crates/render_gpu/src/drawsurf/shellshock_screen.rs b/crates/render_gpu/src/drawsurf/shellshock_screen.rs index 355e990..250b540 100644 --- a/crates/render_gpu/src/drawsurf/shellshock_screen.rs +++ b/crates/render_gpu/src/drawsurf/shellshock_screen.rs @@ -27,6 +27,7 @@ impl TemporalScreenPipeline { }, fragment: Some(FragmentState { shader_defs: Vec::new(), + constants: Vec::new(), shader: self.shader.clone(), entry_point: Some("fragment".into()), targets: vec![Some(ColorTargetState { diff --git a/crates/render_gpu/src/drawsurf/sun_effects.rs b/crates/render_gpu/src/drawsurf/sun_effects.rs index 6e9ced2..24f78c5 100644 --- a/crates/render_gpu/src/drawsurf/sun_effects.rs +++ b/crates/render_gpu/src/drawsurf/sun_effects.rs @@ -26,7 +26,7 @@ use std::num::NonZeroU64; use super::gpu_resources::RuntimeUploadedImageRegistry; use super::postfx::PostFxSet; -const SHADER_PATH: &str = "embedded://render_gpu/drawsurf/sun_effects.wgsl"; +const SHADER_PATH: &str = "embedded://render_gpu/drawsurf/sun_effects.wesl"; const PARAMS_SIZE: u64 = 80; #[repr(C)] @@ -133,12 +133,14 @@ impl SpecializedRenderPipeline for SunEffectsGpu { vertex: VertexState { shader: self.shader.clone(), shader_defs: defs.clone(), + constants: Vec::new(), entry_point: Some(vertex.into()), buffers: Vec::new(), }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs: defs, + constants: Vec::new(), entry_point: Some(fragment.into()), targets: vec![Some(ColorTargetState { format: key.target, @@ -537,7 +539,7 @@ fn draw_sun_effects( } pub(super) fn register(app: &mut App) { - bevy::asset::embedded_asset!(app, "sun_effects.wgsl"); + bevy::asset::embedded_asset!(app, "sun_effects.wesl"); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; diff --git a/crates/render_gpu/src/drawsurf/sun_effects.wgsl b/crates/render_gpu/src/drawsurf/sun_effects.wesl similarity index 99% rename from crates/render_gpu/src/drawsurf/sun_effects.wgsl rename to crates/render_gpu/src/drawsurf/sun_effects.wesl index ce22f3c..2e882ed 100644 --- a/crates/render_gpu/src/drawsurf/sun_effects.wgsl +++ b/crates/render_gpu/src/drawsurf/sun_effects.wesl @@ -123,11 +123,10 @@ fn vs_billboard(@builtin(vertex_index) vertex_index: u32) -> BillboardOut { vec2(-1.0, 1.0), ); let o = corners[vertex_index]; -#ifdef SUN_FLARE + @if(SUN_FLARE) let size = params.viewport.w; -#else + @else let size = params.viewport.z; -#endif var clip = params.clip; clip.x = clip.x + o.x * clip.w * (size / 640.0); clip.y = clip.y + o.y * clip.w * (size / 480.0); diff --git a/crates/render_gpu/src/plugin.rs b/crates/render_gpu/src/plugin.rs index fd70ed4..bff3f0b 100644 --- a/crates/render_gpu/src/plugin.rs +++ b/crates/render_gpu/src/plugin.rs @@ -4,6 +4,9 @@ pub struct RenderGpuPlugin; impl Plugin for RenderGpuPlugin { fn build(&self, app: &mut App) { + app.add_plugins(bevy::render::extract_component::ExtractComponentPlugin::< + frame::DisplayEncodedCamera, + >::default()); match crate::SamplerTable::captured_2026_08_11() { Ok(table) => { let adapted = table.host_adapted_rows(); @@ -27,6 +30,59 @@ impl Plugin for RenderGpuPlugin { render_app .init_resource::() .init_resource::() - .init_resource::(); + .init_resource::() + .add_systems( + bevy::render::ExtractSchedule, + extract_display_encoded_formats + .after(bevy::render::camera::extract_cameras) + .before(bevy::ui_render::extract_ui_camera_view), + ) + .add_systems( + bevy::render::Render, + resolve_display_encoded_views + .in_set(bevy::render::RenderSystems::CreateViews) + .after(bevy::render::view::ResolveCompositingSpaces), + ); + } +} + +fn extract_display_encoded_formats( + cameras: bevy::render::Extract< + Query< + ( + bevy::render::sync_world::RenderEntity, + Has, + ), + With, + >, + >, + mut formats: ResMut, +) { + // UI extraction must see the same unorm format as the custom scene passes. + for (camera, hdr) in &cameras { + if !hdr && let Some(format) = formats.get_mut(&camera) { + *format = bevy::render::render_resource::TextureFormat::Rgba8Unorm; + } + } +} + +fn resolve_display_encoded_views( + mut views: Query< + ( + &bevy::render::camera::ExtractedCamera, + &mut bevy::render::view::ExtractedView, + &mut bevy::render::view::ResolvedCompositingSpace, + ), + With, + >, +) { + // The custom scene and HUD passes write sRGB values into unorm targets; + // Bevy's Camera3d resolver assumes linear output from its stock passes. + // Deferred camera extraction must finish before the view format is changed. + for (camera, mut view, mut space) in &mut views { + if !camera.hdr { + view.target_format = bevy::render::render_resource::TextureFormat::Rgba8Unorm; + } + space.0 = Some(bevy::camera::CompositingSpace::Srgb); } } diff --git a/crates/render_material/src/catalog.rs b/crates/render_material/src/catalog.rs index 82196da..333262f 100644 --- a/crates/render_material/src/catalog.rs +++ b/crates/render_material/src/catalog.rs @@ -132,6 +132,9 @@ impl PortId { if pass.hardware_shadow_compare { id.arguments_hash ^= 0x5AD0_C0DE; } + if pass.depth_to_colour { + id.arguments_hash ^= 0xDE97_C010; + } Some(id) } @@ -280,6 +283,8 @@ pub struct RuntimePass { pub color_space: crate::PassColorSpace, pub hardware_shadow_compare: bool, + + pub depth_to_colour: bool, } impl RuntimePass { @@ -290,6 +295,7 @@ impl RuntimePass { && self.arguments == other.arguments && self.color_space == other.color_space && self.hardware_shadow_compare == other.hardware_shadow_compare + && self.depth_to_colour == other.depth_to_colour } } diff --git a/crates/render_material/src/dxbc_abi.rs b/crates/render_material/src/dxbc_abi.rs index 39966cc..eafd01d 100644 --- a/crates/render_material/src/dxbc_abi.rs +++ b/crates/render_material/src/dxbc_abi.rs @@ -286,6 +286,7 @@ pub(crate) fn build_dxbc_pass_abi( vertex_constants, pixel_constants, samplers, + depth_to_colour: false, }; let lowering = PassAbi { vertex_inputs, diff --git a/crates/render_material/src/program_abi.rs b/crates/render_material/src/program_abi.rs index 918d04f..e25c059 100644 --- a/crates/render_material/src/program_abi.rs +++ b/crates/render_material/src/program_abi.rs @@ -157,6 +157,7 @@ pub fn prepare_program_abi<'a>( pass.custom_sampler_flags, pass.t5_custom_sampler_flags, pass.hardware_shadow_compare, + pass.depth_to_colour, ) .map_err(ProgramAbiRefusal::Abi)?; (abi, PreparedShaderPrograms::Sm3 { vertex, pixel }) diff --git a/crates/render_material/src/sm3_abi.rs b/crates/render_material/src/sm3_abi.rs index 5dcd9ec..d20ee15 100644 --- a/crates/render_material/src/sm3_abi.rs +++ b/crates/render_material/src/sm3_abi.rs @@ -103,6 +103,8 @@ pub struct PassProgramAbi { pub vertex_constants: Vec, pub pixel_constants: Vec, pub samplers: Vec, + + pub depth_to_colour: bool, } pub fn format_vertex_semantic(semantic: Semantic) -> String { @@ -240,6 +242,7 @@ pub(crate) fn build_pass_abi( custom_sampler_flags: u8, t5_custom_sampler_flags: u8, hardware_shadow_compare: bool, + depth_to_colour: bool, ) -> Result { let attributes = routed_attributes(decl, vertex_type)?; let vertex_inputs = bind_vertex_inputs(vertex, &attributes)?; @@ -269,6 +272,7 @@ pub(crate) fn build_pass_abi( vertex_constants, pixel_constants, samplers, + depth_to_colour, }) } diff --git a/crates/render_scene/Cargo.toml b/crates/render_scene/Cargo.toml index e185486..1f6dd0d 100644 --- a/crates/render_scene/Cargo.toml +++ b/crates/render_scene/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true description = "Scene occupancy storage and plugin. No frontend tess, anim, FX, or GPU." [dependencies] +frame.workspace = true asset_game.workspace = true asset_material.workspace = true asset_model.workspace = true diff --git a/crates/render_scene/src/camera.rs b/crates/render_scene/src/camera.rs index ebde462..09fba2b 100644 --- a/crates/render_scene/src/camera.rs +++ b/crates/render_scene/src/camera.rs @@ -10,6 +10,7 @@ pub struct WorldCameraPose { pub struct FpvLens; #[derive(Component)] +#[require(bevy::prelude::DespawnOnExit = bevy::prelude::DespawnOnExit(frame::MatchScope::Live))] pub struct FlyCamera { pub yaw: f32, pub pitch: f32, diff --git a/crates/render_scene/src/dpvs_cells.rs b/crates/render_scene/src/dpvs_cells.rs index a789329..4cc9dd2 100644 --- a/crates/render_scene/src/dpvs_cells.rs +++ b/crates/render_scene/src/dpvs_cells.rs @@ -23,4 +23,5 @@ pub struct PublishedCellVis { } #[derive(Component)] +#[require(bevy::prelude::DespawnOnExit = bevy::prelude::DespawnOnExit(frame::MatchScope::Live))] pub struct DynEntModelEntity; diff --git a/crates/render_scene/src/plugin.rs b/crates/render_scene/src/plugin.rs index 6104978..00ed145 100644 --- a/crates/render_scene/src/plugin.rs +++ b/crates/render_scene/src/plugin.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use frame::ScopeApp; use crate::{ DynAtPointLookup, HostGfxScene, ModelLightingRequests, RLockPvs, RSubwindowDvar, @@ -11,19 +12,19 @@ pub struct RenderScenePlugin; impl Plugin for RenderScenePlugin { fn build(&self, app: &mut App) { - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + app.scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .init_resource::() .init_resource::() .init_resource::() diff --git a/crates/render_scene/src/view_parms.rs b/crates/render_scene/src/view_parms.rs index 74a66a8..fb9e9d7 100644 --- a/crates/render_scene/src/view_parms.rs +++ b/crates/render_scene/src/view_parms.rs @@ -41,7 +41,7 @@ pub fn pack_live_view_parms( #[must_use] pub fn host_clip_from_view(fov: f32, aspect: f32, near: f32) -> Mat4 { - Mat4::perspective_infinite_reverse_rh(fov, aspect, near) + bevy::math::proj::perspective_infinite_reverse(fov, aspect, near) } #[derive(Resource, Clone, Debug)] diff --git a/crates/replay/src/plugin.rs b/crates/replay/src/plugin.rs index 99fdde6..09e4603 100644 --- a/crates/replay/src/plugin.rs +++ b/crates/replay/src/plugin.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; -use frame::{MatchTornDown, ReturnedToMenu, RuntimeRole, SessionSwapApplied}; +use frame::ScopeApp; +use frame::{RuntimeRole, SessionSwapApplied}; use net::{ AUTHORITY_MS, AuthorityClock, AuthorityLoadHold, AuthoritySet, ClientClock, ClientSet, ReceivedTick, ReceivedTicks, ServerTick, ServerTime, authority_should_tick, @@ -68,12 +69,13 @@ impl Plugin for ReplayPlugin { app.init_resource::() .init_resource::() .init_resource::() - .init_resource::(); + .scoped::(frame::MatchScope::Live); let role = app.world().get_resource::().copied(); if role.is_some_and(|role| role.runs_authority() || role == RuntimeRole::Replay) { app.add_systems( FixedUpdate, record_server_tick + .in_set(frame::InMatch) .in_set(AuthoritySet::Fanout) .run_if(authority_should_tick), ); @@ -82,12 +84,15 @@ impl Plugin for ReplayPlugin { Update, ( record_received_ticks + .in_set(frame::InMatch) .after(ClientSet::Receive) .before(ClientSet::Reconcile), sync_theater_occupancy + .in_set(frame::InMatch) .in_set(ClientSet::Load) .after(SessionSwapApplied), pump_playback + .in_set(frame::InMatch) .in_set(ClientSet::Load) .run_if(resource_exists::) .after(sync_theater_occupancy), @@ -98,17 +103,9 @@ impl Plugin for ReplayPlugin { fn sync_theater_occupancy( mut commands: Commands, - mut torn: MessageReader, - mut returned: MessageReader, mut pending: ResMut, - mut ring: ResMut, role: Res, ) { - if torn.read().count() > 0 || returned.read().count() > 0 { - ring.clear(); - commands.remove_resource::(); - perf::theater(0, None, None); - } if *role != RuntimeRole::Replay { return; } @@ -116,7 +113,10 @@ fn sync_theater_occupancy( return; }; let quit_on_end = pending.quit_on_end; - commands.insert_resource(ReplayPlayback::new(playback).with_quit_on_end(quit_on_end)); + let playback = ReplayPlayback::new(playback).with_quit_on_end(quit_on_end); + commands.queue(move |world: &mut World| { + frame::scope::insert(world, playback, frame::MatchScope::Live); + }); perf::theater( 1, Some(i64::from(quit_on_end)), diff --git a/crates/session/src/admission.rs b/crates/session/src/admission.rs index b375c69..39bd64a 100644 --- a/crates/session/src/admission.rs +++ b/crates/session/src/admission.rs @@ -1,6 +1,6 @@ use assets::LoadingScreen; use bevy::prelude::*; -use frame::{AppScreen, ClientSet, HasWorld, RuntimeRole}; +use frame::{AppScreen, ClientSet, RuntimeRole}; use net::{AuthorityLoadHold, ClientAdmission, SignonPhase, SignonState}; use render_frontend::prepare::scene::world::WorldScene; @@ -11,7 +11,7 @@ pub fn update_admission( mut admission: ResMut, role: Res, mut hold: Option>, - has_world: Option>, + has_world: Option>>, scene: Option>, audio: Option>, mut live: Option>, @@ -41,7 +41,7 @@ pub fn update_admission( } live.load_key = installed; } - let installed = has_world.is_some_and(|world| world.0) + let installed = has_world.is_some_and(|world| *world.get() == frame::MatchScope::Live) && live.as_ref().is_some_and(|live| { generation.0 == Some(live.load_key.local_load_request_id) && admission.core.installed() == Some(live.load_key) @@ -49,7 +49,7 @@ pub fn update_admission( let decision = crate::readiness::decide_readiness( *role, headless.is_some(), - *generation, + generation.stamp(), installed, navigation.as_ref().map(|report| report.0), scene.as_ref().map(|scene| scene.readiness), @@ -94,7 +94,7 @@ pub fn update_admission( .as_ref() .filter(|_| installed) .map(|live| live.load_key), - generation: *generation, + generation: generation.stamp(), advancement_allowed: decision.advancement, presentation_allowed: decision.presentation, admission_allowed: admitted, @@ -103,7 +103,7 @@ pub fn update_admission( let presentation_ready = decision.presentation; let audio_ready = audio .as_ref() - .is_some_and(|report| report.0.ready_for(*generation)); + .is_some_and(|report| report.0.ready_for(generation.stamp())); if signon.admitted != admitted { signon.admitted = admitted; diag::info!( @@ -119,9 +119,9 @@ pub fn drive_class_select_screen( mut loading: Option>, mut load: Option>, signon: Res, - has_world: Option>, + has_world: Option>>, ) { - let world_installed = has_world.is_some_and(|world| world.0); + let world_installed = has_world.is_some_and(|world| *world.get() == frame::MatchScope::Live); let admitted = signon.may_select_class(); if signon.phase.is_failed() { if let Some(loading) = loading.as_deref_mut() @@ -170,6 +170,7 @@ pub fn register_admission(app: &mut App) { .add_systems( Update, update_admission + .in_set(frame::InMatch) .in_set(ClientSet::Present) .before(crate::local_arm::arm_local_from_presented), ) diff --git a/crates/session/src/lib.rs b/crates/session/src/lib.rs index 0e2de95..744c359 100644 --- a/crates/session/src/lib.rs +++ b/crates/session/src/lib.rs @@ -22,7 +22,7 @@ pub use content_manifest::{ pub use life_front::{LifeFrontCensus, LifeNotifyCensus}; pub use lifecycle::{ LiveWorldIdentity, SessionSwapCompletion, SessionSwapRequest, SessionSwapResult, - SessionSwapTarget, TeardownGaps, TeardownRequest, + SessionSwapTarget, TeardownRequest, }; pub use loadout::{ AuthoritativeClassProjection, ClassRow, perk_catalog_id, project_class, resolve_class_weapon, @@ -34,3 +34,6 @@ pub use local_arm::{ pub use match_apply::{apply_prepared_match, install_script_model_id}; pub use plugin::SessionPlugin; pub use startup::{PendingConsoleLines, StartupCommands}; + +mod scope_control; +pub use scope_control::ScopeControl; diff --git a/crates/session/src/life_front.rs b/crates/session/src/life_front.rs index a96b8f3..ffd3d7c 100644 --- a/crates/session/src/life_front.rs +++ b/crates/session/src/life_front.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use bevy::prelude::*; +use frame::ScopeApp; use frame::{ AbortKillcam, BeginKillcam, KillcamEnded, LifeEndCause, LifeEnded, LifeFrontPublished, LifeStartReason, LifeStarted, PresentedPublished, SpawnedPlayer, @@ -160,14 +161,19 @@ fn emit_ended( pub fn register_life_front(app: &mut App) { app.init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) .add_message::() .add_message::() .add_observer(on_begin_killcam) .add_observer(on_abort_killcam) .add_observer(on_killcam_ended) .add_observer(on_spawned_player) - .add_systems(Update, publish_life_front.in_set(PresentedPublished)) + .add_systems( + Update, + publish_life_front + .in_set(frame::InMatch) + .in_set(PresentedPublished), + ) .configure_sets( Update, LifeFrontPublished diff --git a/crates/session/src/lifecycle.rs b/crates/session/src/lifecycle.rs index aa01427..71a7457 100644 --- a/crates/session/src/lifecycle.rs +++ b/crates/session/src/lifecycle.rs @@ -1,11 +1,10 @@ use bevy::prelude::*; use bevy::tasks::{AsyncComputeTaskPool, TaskPool}; use frame::{ - AppScreen, ClassSelectHandoff, HasWorld, LaunchIdentity, LocalLoadKey, MapLoadApproved, - MapLoadFailed, MatchInstalled, MatchKey, MatchTornDown, Retiring, ReturnedToMenu, RuntimeRole, - TeardownReason, WorldGeneration, + AppScreen, ClassSelectHandoff, InstalledMatch, LaunchIdentity, LocalLoadKey, MapLoadApproved, + MapLoadFailed, MatchKey, Retiring, RuntimeRole, TeardownReason, }; -use net::{AuthorityLoadHold, ClientSet, MatchDescriptor, PresentedSnapshot}; +use net::{AuthorityLoadHold, ClientSet, PresentedSnapshot}; #[derive(Resource, Default, Debug)] pub struct TeardownRequest(pub Option); @@ -178,54 +177,26 @@ impl SessionSwapRequest { } } -#[derive(Resource, Default, Debug)] -pub struct TeardownGaps(pub Vec<&'static str>); - -pub fn run_teardown( - mut commands: Commands, - mut request: ResMut, +fn release_match( + mut control: ResMut, mut screen: ResMut, - mut has_world: ResMut, - mut world_generation: ResMut, mut readiness: ResMut, mut armed: ResMut, mut handoff: ResMut, mut identity: Option>, mut load_hold: Option>, - live_world: Option>, - signon: Option>, bridge: Option>, - mut gaps: ResMut, mut retiring: ResMut, - mut torn: MessageWriter, ) { - let Some(reason) = request.0.take() else { + let Some(reason) = control.teardown.take() else { return; }; if let Some(bridge) = bridge.as_ref() { bridge.set_installed_load(None); } - let torn_generation = *world_generation; - let match_key = live_world - .map(|live| live.load_key.match_key) - .unwrap_or_else(|| { - bridge - .as_ref() - .map(|bridge| bridge.state().identity().match_key()) - .filter(|key| !key.is_none()) - .unwrap_or_else(|| { - MatchKey::new([0; 16], signon.map(|signon| signon.epoch.0).unwrap_or(0)) - }) - }); - - commands.remove_resource::(); - commands.remove_resource::(); - commands.insert_resource(LiveWorldIdentity::default()); *screen = AppScreen::MainMenu; - *has_world = HasWorld(false); - *world_generation = WorldGeneration(None); *readiness = crate::SessionReadinessPolicy::default(); *armed = frame::LocalSpawnArmed::default(); *handoff = ClassSelectHandoff::default(); @@ -238,38 +209,19 @@ pub fn run_teardown( identity.zone.clear(); } - gaps.0 = vec![ - "Bevy Assets handles dropped by WorldScene::default stay \ - until Bevy GC; they are not drawable leftover world", - "WorldScene Resource stays so hold still freezes level.time; render \ - empties geometry / tess / GPU plans", - "AuthorityWorld Resource stays; clip is SimWorld::shutdown_game, not remove", - ]; - - diag::info!( - Sim, - "session: match torn down ({reason:?}); teardown ran, {} declared gaps", - gaps.0.len() - ); perf::match_torn(reason.label()); diag::lifecycle_boundary( "local_session_revoked", &format!(" reason={}", reason.label()), ); retiring.mark_teardown(); - torn.write(MatchTornDown { - reason, - world_generation: torn_generation, - match_key, - match_epoch: match_key.match_epoch, - }); } fn stamp_loaded_zone( - mut installed: MessageReader, + installed: Option>, mut identity: Option>, ) { - let Some(fact) = installed.read().last() else { + let Some(fact) = installed.filter(|fact| fact.is_added()) else { return; }; if let Some(identity) = identity.as_mut() { @@ -331,7 +283,6 @@ fn occupy_after_teardown( role: &mut Option>, identity: &mut Option>, approved: &mut MessageWriter, - menu: &mut MessageWriter, leave: &mut Option>, bridge: Option<&net::MasterBridge>, ) -> Option { @@ -346,11 +297,6 @@ fn occupy_after_teardown( { action.0 = Some(net::MasterMenuAction::LeaveLobby); } - menu.write(ReturnedToMenu { - swap_id: pending.id, - had_world: pending.tore_down_world, - reason: Some(teardown_reason_for(&pending.target)), - }); Some(SessionSwapCompletion { id: pending.id, result: SessionSwapResult::Menu, @@ -393,16 +339,15 @@ fn occupy_after_teardown( fn run_session_swap( mut commands: Commands, mut transition: ResMut, - has_world: Option>, + mut scope: ResMut, + has_world: Option>>, mut teardown: ResMut, mut role: Option>, mut identity: Option>, mut approved: MessageWriter, - mut torn: MessageReader, mut failed: MessageReader, mut dvars: ResMut, - mut installed: MessageReader, - mut menu: MessageWriter, + installed: Option>, mut leave: Option>, bridge: Option>, ) { @@ -411,10 +356,7 @@ fn run_session_swap( .find(|fact| transition.accepts_install(fact.request_id)); let mut finish: Option = None; if let Some(fact) = failure { - let had_world = transition - .pending - .as_ref() - .is_some_and(|pending| pending.tore_down_world); + scope.return_to_menu(None); stamp_runtime_role(&mut role, &mut identity, RuntimeRole::Listen); if let Some(identity) = identity.as_mut() { identity.zone.clear(); @@ -430,11 +372,6 @@ fn run_session_swap( format!("Could not load map '{}'.\n\n{details}", fact.zone), ); dvars.set("ui_map_error", "1"); - menu.write(ReturnedToMenu { - swap_id: fact.request_id, - had_world, - reason: None, - }); diag::warn!( Sim, "session: map `{}` failed (swap #{}): {} — returned to menu", @@ -457,25 +394,35 @@ fn run_session_swap( let expected_teardown = teardown_reason_for(&pending.target); // A leave can arrive after a match-end teardown was requested: a return to the menu // finishes on whichever teardown comes, but it must still wait for one. - let menu_accepts_any = matches!(pending.target, SessionSwapTarget::Menu { .. }); match pending.phase { - SessionSwapPhase::Requested if has_world.is_some_and(|world| world.0) => { + SessionSwapPhase::Requested + if has_world + .as_ref() + .is_some_and(|world| *world.get() == frame::MatchScope::Live) => + { teardown.0 = Some(expected_teardown); pending.tore_down_world = true; pending.phase = SessionSwapPhase::WaitingForTeardown; } SessionSwapPhase::Requested | SessionSwapPhase::WaitingForTeardown if matches!(pending.phase, SessionSwapPhase::Requested) - || torn - .read() - .any(|fact| fact.reason == expected_teardown || menu_accepts_any) => + || !has_world + .as_ref() + .is_some_and(|world| *world.get() == frame::MatchScope::Live) => { + if matches!(pending.target, SessionSwapTarget::Menu { .. }) { + scope.return_to_menu(Some(expected_teardown)); + } else if !has_world + .as_ref() + .is_some_and(|world| *world.get() == frame::MatchScope::Loading) + { + scope.begin_load(); + } finish = occupy_after_teardown( pending, &mut role, &mut identity, &mut approved, - &mut menu, &mut leave, bridge.as_deref(), ); @@ -483,18 +430,20 @@ fn run_session_swap( SessionSwapPhase::WaitingForInstall if matches!(pending.target, SessionSwapTarget::Menu { .. }) => { + scope.return_to_menu(Some(expected_teardown)); finish = occupy_after_teardown( pending, &mut role, &mut identity, &mut approved, - &mut menu, &mut leave, bridge.as_deref(), ); } SessionSwapPhase::WaitingForInstall - if let Some(fact) = installed.read().find(|fact| fact.request_id == pending.id) => + if let Some(fact) = installed + .as_ref() + .filter(|fact| fact.request_id == pending.id) => { finish = Some(SessionSwapCompletion { id: pending.id, @@ -690,12 +639,12 @@ fn run_udp_peer_lobby_return( role: Res, presented: Option>, bridge: Option>, - has_world: Option>, + has_world: Option>>, mut transition: ResMut, mut latched: Local, ) { let master_joined = joined_in_match(bridge.as_ref().map(|b| b.state()).as_ref()).is_some(); - let has_world = has_world.is_some_and(|h| h.0); + let has_world = has_world.is_some_and(|h| *h.get() == frame::MatchScope::Live); if !udp_peer_exit_applies(*role, master_joined, has_world) { *latched = false; return; @@ -717,8 +666,8 @@ fn follow_master_match( mut offers: ResMut, bridge: Option>, identity: Res, - live: Res, - has_world: Res, + live: Option>, + has_world: Res>, busy: Option>, request: Option>, ready: Option>, @@ -742,15 +691,19 @@ fn follow_master_match( return; } let host = matches!(state, net::MasterBridgeState::Hosting { .. }); - if has_world.0 - && (live.load_key.match_key == offer.match_key - || (host && live.load_key.match_key.is_none() && identity.zone == offer.map)) + if (*has_world.get() == frame::MatchScope::Live) + && live.as_ref().is_some_and(|live| { + live.load_key.match_key == offer.match_key + || (host && live.load_key.match_key.is_none() && identity.zone == offer.map) + }) { *pending = None; return; } let loading = busy.is_some_and(|busy| busy.0) || request.is_some() || ready.is_some(); - if !*retiring && (has_world.0 || loading || swap.pending.is_some()) { + if !*retiring + && ((*has_world.get() == frame::MatchScope::Live) || loading || swap.pending.is_some()) + { let id = ready .as_ref() .map(|r| r.request_id) @@ -766,7 +719,7 @@ fn follow_master_match( *retiring = true; return; } - if has_world.0 || loading || swap.pending.is_some() { + if (*has_world.get() == frame::MatchScope::Live) || loading || swap.pending.is_some() { return; } let Some(mode) = sim::HostGameModeSelection::from_token(&offer.mode) else { @@ -799,16 +752,9 @@ pub fn register_lifecycle(app: &mut App) { app.init_resource::() .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() .init_resource::() .add_message::() .add_message::() - .add_message::() - .add_message::() - .add_message::() .configure_sets(Update, frame::SessionSwapApplied.in_set(ClientSet::Load)) .add_systems( Update, @@ -817,7 +763,7 @@ pub fn register_lifecycle(app: &mut App) { run_exit_level, run_peer_lobby_return, run_udp_peer_lobby_return, - run_teardown, + request_teardown, run_session_swap, ) .chain() @@ -829,5 +775,15 @@ pub fn register_lifecycle(app: &mut App) { .after(crate::apply_prepared_match) .in_set(ClientSet::Load), ) + .add_systems( + OnExit(frame::MatchScope::Live), + release_match.in_set(frame::ScopeSet::Release), + ) .add_systems(Update, retire_handed_over.in_set(ClientSet::Diag)); } + +fn request_teardown(mut request: ResMut, mut scope: ResMut) { + if let Some(reason) = request.0.take() { + scope.tear_down(reason); + } +} diff --git a/crates/session/src/local_arm.rs b/crates/session/src/local_arm.rs index f59aa51..4365b5a 100644 --- a/crates/session/src/local_arm.rs +++ b/crates/session/src/local_arm.rs @@ -19,11 +19,11 @@ pub fn arm_local_from_presented( sim_cam: Option>, mut look: ResMut, ) { - if !role.runs_client() || armed.armed_for(*generation) { + if !role.runs_client() || armed.armed_for(generation.stamp()) { return; } - if !policy.admission_allowed || policy.generation != *generation { + if !policy.admission_allowed || policy.generation != generation.stamp() { return; } let Some(ps) = presented.alive_player(local.0) else { @@ -49,7 +49,7 @@ pub fn arm_local_from_presented( ps.viewangles[1], ps.weapon ); - armed.0 = *generation; + armed.0 = generation.stamp(); } fn apply_local_input_policy( @@ -60,8 +60,8 @@ fn apply_local_input_policy( mut gate: ResMut, ) { gate.local_cmds_enabled = policy.local_input_allowed( - *generation, - armed.armed_for(*generation), + generation.stamp(), + armed.armed_for(generation.stamp()), signon.phase.is_failed(), ); } @@ -74,8 +74,8 @@ fn guard_local_input_before_prediction( mut gate: ResMut, ) { gate.local_cmds_enabled = policy.local_input_allowed( - *generation, - armed.armed_for(*generation), + generation.stamp(), + armed.armed_for(generation.stamp()), signon.phase.is_failed(), ); } @@ -153,21 +153,28 @@ pub fn register_local_arm_systems(app: &mut App) { .add_systems( Update, ( - arm_local_from_presented.after(ClassEquipResolved), + arm_local_from_presented + .in_set(frame::InMatch) + .after(ClassEquipResolved), join_local_on_class_select.after(crate::admission::update_admission), - apply_local_input_policy.after(arm_local_from_presented), + apply_local_input_policy + .in_set(frame::InMatch) + .after(arm_local_from_presented), ) .in_set(ClientSet::Present), ) .add_systems( Update, guard_local_input_before_prediction + .in_set(frame::InMatch) .in_set(ClientSet::Predict) .after(net::client::runtime::enforce_client_work_limits) .before(net::client::runtime::predict_local_move), ) .add_systems( Update, - sync_prediction_metrics_to_probe.in_set(ClientSet::Diag), + sync_prediction_metrics_to_probe + .in_set(frame::InMatch) + .in_set(ClientSet::Diag), ); } diff --git a/crates/session/src/match_apply.rs b/crates/session/src/match_apply.rs index 1cabde7..4946284 100644 --- a/crates/session/src/match_apply.rs +++ b/crates/session/src/match_apply.rs @@ -7,8 +7,8 @@ use assets::{ use bevy::ecs::system::SystemParam; use bevy::prelude::*; use frame::{ - CacWeaponOffer, ClassSelectHandoff, HasWorld, HostClassLoadouts, LaunchIdentity, LaunchReport, - MatchInstalled, WorldGeneration, WorldProducts, + CacWeaponOffer, ClassSelectHandoff, HostClassLoadouts, InstalledMatch, LaunchIdentity, + LaunchReport, WorldProducts, WorldStamp, }; use net::{AuthorityInputGate, AuthorityLoadHold, AuthorityWorld, ClientSet}; @@ -37,6 +37,7 @@ fn log_world_report(report: &[String]) { #[derive(SystemParam)] pub struct PreparedMatchSource<'w, 's> { ready: ResMut<'w, PreparedMatchReady>, + audio_status: Option>, swap: Res<'w, crate::SessionSwapRequest>, bridge: Option>, intent: Option>, @@ -64,7 +65,6 @@ impl MatchInstallConsole<'_> { #[derive(SystemParam)] pub struct MatchInstallAuthority<'w> { role: Res<'w, frame::RuntimeRole>, - prediction: Option>, input_gate: Res<'w, AuthorityInputGate>, cheats: Option>, profile: Option>, @@ -77,11 +77,7 @@ pub struct MatchInstallAuthority<'w> { pub struct MatchInstallPresentation<'w> { probe: Option>, camera: Res<'w, SimCamera>, -} - -#[derive(SystemParam)] -pub struct MatchInstallOccupancy<'w> { - has_world: ResMut<'w, HasWorld>, + player_data: Res<'w, sim::LocalPlayerData>, } pub fn apply_prepared_match( @@ -90,13 +86,13 @@ pub fn apply_prepared_match( mut console: MatchInstallConsole, authority: MatchInstallAuthority, presentation: MatchInstallPresentation, - occupancy: MatchInstallOccupancy, mode_selection: Option>, rules: Option>, mut failed: MessageWriter, ) { let PreparedMatchSource { mut ready, + audio_status, swap, bridge, intent, @@ -110,7 +106,6 @@ pub fn apply_prepared_match( } = source; let MatchInstallAuthority { role, - prediction, input_gate, cheats, profile, @@ -122,8 +117,8 @@ pub fn apply_prepared_match( let MatchInstallPresentation { mut probe, camera: sim_cam, + player_data, } = presentation; - let MatchInstallOccupancy { mut has_world } = occupancy; let stale_key = !ready.load_key.match_key.is_none() && bridge.as_ref().is_none_or(|bridge| { let state = bridge.state(); @@ -165,6 +160,20 @@ pub fn apply_prepared_match( ); return; } + match audio_status.as_deref() { + Some(audio::LoadingAudioStatus::Ready) => {} + Some(audio::LoadingAudioStatus::Failed(error)) => { + failed.write(frame::MapLoadFailed { + request_id: ready.request_id, + load_key: ready.load_key, + zone: ready.zone.clone(), + error: error.clone(), + }); + commands.remove_resource::(); + return; + } + _ => return, + } // The stage opens a frame before the work so the row is already up when // the main thread stops answering — the yield is the point, not an // accident of where the handle was created. @@ -218,7 +227,6 @@ pub fn apply_prepared_match( } probe.world_report = world_report; } - *has_world = HasWorld(false); if let Some(stage) = install_stage { stage.fail(); } @@ -237,6 +245,7 @@ pub fn apply_prepared_match( }; let prepared_install = (|| -> Result { let mut install = bevy::ecs::world::CommandQueue::default(); + install.push(audio::stage_prepared_match_audio); let MatchInstallPlan { ui_images, scripts, @@ -276,9 +285,59 @@ pub fn apply_prepared_match( } = plan; let spawn_count = prepared_map.spawns.len(); + let mut local_player_data = player_data.clone(); + let definitions = script_level + .schemas + .get("mp/playerdata.def") + .ok_or_else(|| { + InstallRefusal::new("Local progression: player data schema is missing") + })?; + let ranks = weapons + .registry() + .rank_progression() + .map_err(|error| InstallRefusal::new(format!("Local progression: {error}")))?; + local_player_data + .install( + definitions, + ranks, + account + .as_ref() + .and_then(|account| account.snapshot.as_ref()), + ) + .map_err(|error| InstallRefusal::new(format!("Local progression: {error:?}")))?; + local_player_data.install_unlock_data( + account_defaults.clone(), + script_level + .tables + .get("mp/allchallengestable.csv") + .cloned(), + ); + for name in ["mp/ranktable.csv", "mp/rankicontable.csv"] { + if let Some(table) = script_level.tables.get(name) { + local_player_data.install_rank_table(name, table.clone()); + } + } + diag::info!( + Sim, + "local progression prepared before admission: rank={}", + local_player_data + .progression + .as_ref() + .expect("installed progression") + .rank + ); + install.push(move |world: &mut World| { + world + .resource_mut::() + .0 + .push(move |world: &mut World| { + world.insert_resource(local_player_data); + }); + }); + let facts = std::mem::take(&mut prepared_map.facts); let airstrike_height = facts.airstrike_height; - stage_resource(&mut install, ui_images); + stage_resource(&mut install, crate::scope_control::MatchUiImages(ui_images)); stage_resource( &mut install, assets::SessionCompass { @@ -304,7 +363,7 @@ pub fn apply_prepared_match( let mut scene = loaded_scene; scene.readiness = frame::WorldReadiness::new( - WorldGeneration::from_install(request_id), + WorldStamp::from_install(request_id), frame::ReadinessState::Pending, ); let mut sim_cam = *sim_cam; @@ -319,6 +378,9 @@ pub fn apply_prepared_match( ) .map_err(|error| InstallRefusal::new(format!("Invalid simulation weapons: {error:?}")))?; let mut content = sim::SimContentBuilder::for_match(Arc::clone(sim_weapons.content())); + content + .set_map(Arc::clone(&prepared_map.content)) + .map_err(|error| InstallRefusal::new(format!("Invalid map content: {error}")))?; content.set_script_sound_aliases(script_sound_aliases); let mut sim = sim::SimWorld::new(); if role.runs_authority() @@ -505,34 +567,6 @@ pub fn apply_prepared_match( .map_err(|e| script_refusal(&zone, gametype, "entry", &e))?; } let load_hold = AuthorityLoadHold(true); - if let Some(glass) = scene.fx_glass.as_ref() { - let panes = (0..glass.piece_places.len()) - .filter_map(|i| { - let (origin, axis_s, axis_t) = glass.pane_basis(i)?; - Some(( - i as u32, - sim::GlassPaneBasis { - origin, - axis_s, - axis_t, - }, - )) - }) - .collect(); - sim.world_objects_mut().install_glass_panes(panes); - sim.set_glass_names( - glass - .names - .iter() - .map(|(name, pieces)| { - ( - name.clone(), - pieces.iter().copied().map(u32::from).collect(), - ) - }) - .collect(), - ); - } sim.world_objects_mut() .set_map_round_epoch(load_key.match_key.match_epoch); spawn_script_model_movers(&mut sim, &model_spawns); @@ -604,39 +638,58 @@ pub fn apply_prepared_match( "match preparation: {:.1}ms on the main thread (scene handoff, sim boot, catalogs)", install_started.elapsed().as_secs_f32() * 1000.0 ); - if role.runs_authority() || *role == frame::RuntimeRole::Replay { - if let Some(prediction) = prediction.as_ref() { - let mut state = net::ClientPrediction::new(prediction.0.local()); - state.arm_from_content(&sim); - stage_resource(&mut install, net::ClientPredictionState(state)); - } - stage_resource(&mut install, AuthorityWorld(sim)); - } else if let Some(prediction) = prediction.as_ref() { - install.push(|world: &mut World| { - world.remove_resource::(); - }); - let mut state = net::ClientPrediction::new(prediction.0.local()); - // Prediction adopts replicated state at admission; the prepared world's - // GSC runtime belongs to authority and cannot be restored by snapshots. + if role.runs_client() { + let client = local.as_ref().map_or(sim::ClientId(0), |local| local.0); + let mut state = net::ClientPrediction::new(client); state.arm_from_content(&sim); stage_resource(&mut install, net::ClientPredictionState(state)); - } else { - return Err(InstallRefusal::new( - "No simulation owner for this execution mode", - )); + } + if role.runs_authority() || *role == frame::RuntimeRole::Replay { + stage_resource(&mut install, AuthorityWorld(sim)); } sim_cam.freeze_fly = true; stage_resource(&mut install, WorldProducts::from_walk(scene.products_id)); stage_resource(&mut install, scene); - stage_resource(&mut install, sim_cam); - stage_resource(&mut install, input_gate); - stage_resource(&mut install, load_hold); - stage_resource(&mut install, class_handoff); + install.push(move |world: &mut World| { + world + .resource_mut::() + .0 + .push(move |world: &mut World| { + world.insert_resource(sim_cam); + }); + }); + install.push(move |world: &mut World| { + world + .resource_mut::() + .0 + .push(move |world: &mut World| { + world.insert_resource(input_gate); + }); + }); + install.push(move |world: &mut World| { + world + .resource_mut::() + .0 + .push(move |world: &mut World| { + world.insert_resource(load_hold); + }); + }); + install.push(move |world: &mut World| { + world + .resource_mut::() + .0 + .push(move |world: &mut World| { + world.insert_resource(class_handoff); + }); + }); stage_resource(&mut install, crate::LiveWorldIdentity { load_key }); - stage_resource(&mut install, HasWorld(true)); - stage_resource(&mut install, WorldGeneration::from_install(request_id)); + stage_resource( + &mut install, + frame::WorldGeneration::from_install(request_id), + ); let installed_zone = zone.clone(); install.push(move |world: &mut World| { + world.resource_mut::().0.push(move |world: &mut World| { if let Some(mut probe) = world.get_resource_mut::() { probe.sim_gap = sim_gap; probe.world_report = world_report; @@ -644,18 +697,19 @@ pub fn apply_prepared_match( diag::script_boundary("installed", &script_facts); diag::info!(Sim, "match: {} ({}) — world installed ({} dm spawn points); class select waits for admission", kind.display_name(), kind.token(), spawn_count); perf::match_installed(&installed_zone, 1); - world.write_message(MatchInstalled { + frame::scope::insert(world, InstalledMatch { request_id, load_key, zone: installed_zone, spawn_count, + }, frame::MatchScope::Live); }); }); Ok(install) })(); match prepared_install { - Ok(mut install) => { + Ok(install) => { commands.queue(move |world: &mut World| { let accepts = world .resource::() @@ -669,7 +723,7 @@ pub fn apply_prepared_match( && bridge.incarnation() == load_key.incarnation }); if accepts && same_match { - install.apply(world); + world.resource_mut::().install(install); } }); if let Some(stage) = install_stage { @@ -1038,7 +1092,7 @@ fn preflight_match_install( Default::default() } }; - let script_level = sim::script::LevelData { + let mut script_level = sim::script::LevelData { absent_effects, player_data_defaults: account_defaults.clone(), schemas: sources.0.schemas().clone(), @@ -1058,6 +1112,29 @@ fn preflight_match_install( }) .collect(), }; + let shared_ranks = weapons.registry().shared_rank_table().map_err(|error| { + InstallRefusal::new(format!("Invalid shared rank progression: {error}")) + })?; + script_level.tables.insert( + "mp/ranktable.csv".into(), + sim::script::StringTable { + columns: shared_ranks.columns, + rows: shared_ranks.rows, + cells: shared_ranks.cells.clone(), + }, + ); + let shared_icons = weapons + .registry() + .shared_rank_icons() + .map_err(|error| InstallRefusal::new(format!("Invalid shared rank icons: {error}")))?; + script_level.tables.insert( + "mp/rankicontable.csv".into(), + sim::script::StringTable { + columns: shared_icons.columns, + rows: shared_icons.rows, + cells: shared_icons.cells.clone(), + }, + ); let startup = sim::script::Iw4Startup::new(&sources, gametype, zone); let roots: Vec<&str> = startup.roots.iter().map(String::as_str).collect(); let scripts = sim::script::Program::load(&sources, &roots, &sim::script::Catalog::iw4()) @@ -1874,6 +1951,7 @@ pub fn register_match_apply_systems(app: &mut App) { .add_systems( Update, apply_prepared_match + .after(frame::SessionSwapApplied) .run_if(resource_exists::) .in_set(ClientSet::Load), ); @@ -1881,6 +1959,6 @@ pub fn register_match_apply_systems(app: &mut App) { fn stage_resource(queue: &mut bevy::ecs::world::CommandQueue, resource: T) { queue.push(move |world: &mut World| { - world.insert_resource(resource); + frame::scope::stage(world, resource); }); } diff --git a/crates/session/src/plugin.rs b/crates/session/src/plugin.rs index db0c92a..a06345c 100644 --- a/crates/session/src/plugin.rs +++ b/crates/session/src/plugin.rs @@ -4,6 +4,7 @@ pub struct SessionPlugin; impl Plugin for SessionPlugin { fn build(&self, app: &mut App) { + crate::scope_control::register(app); crate::lifecycle::register_lifecycle(app); crate::match_apply::register_match_apply_systems(app); crate::admission::register_admission(app); diff --git a/crates/session/src/readiness.rs b/crates/session/src/readiness.rs index 00aed8d..baaa342 100644 --- a/crates/session/src/readiness.rs +++ b/crates/session/src/readiness.rs @@ -1,5 +1,5 @@ use bevy::prelude::Resource; -use frame::{LocalLoadKey, RuntimeRole, WorldGeneration, WorldReadiness}; +use frame::{LocalLoadKey, RuntimeRole, WorldReadiness, WorldStamp}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ReadinessDecision { @@ -34,7 +34,7 @@ impl ReadinessDecision { pub fn decide_readiness( role: RuntimeRole, headless: bool, - generation: WorldGeneration, + generation: WorldStamp, installed: bool, navigation: Option, rendering: Option, @@ -74,7 +74,7 @@ pub fn decide_readiness( #[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct SessionReadinessPolicy { pub load_key: Option, - pub generation: WorldGeneration, + pub generation: WorldStamp, pub advancement_allowed: bool, pub presentation_allowed: bool, pub admission_allowed: bool, @@ -82,12 +82,7 @@ pub struct SessionReadinessPolicy { } impl SessionReadinessPolicy { - pub fn local_input_allowed( - self, - generation: WorldGeneration, - armed: bool, - failed: bool, - ) -> bool { + pub fn local_input_allowed(self, generation: WorldStamp, armed: bool, failed: bool) -> bool { self.input_allowed && self.generation == generation && generation.0.is_some() diff --git a/crates/session/src/scope_control.rs b/crates/session/src/scope_control.rs new file mode 100644 index 0000000..f90eac2 --- /dev/null +++ b/crates/session/src/scope_control.rs @@ -0,0 +1,182 @@ +use bevy::ecs::world::CommandQueue; +use bevy::prelude::*; +use frame::ScopeApp; +use frame::{MatchScope, RoundPhase, ScopeSet, TeardownReason}; + +#[derive(Resource, Default)] +pub struct ScopeControl { + pending: Option<(u8, MatchScope)>, + install: Option, + pub(crate) teardown: Option, + round: Option, + last_reason: Option, +} + +impl ScopeControl { + fn request(&mut self, priority: u8, target: MatchScope) { + if let Some((current, previous)) = self.pending { + if current > priority { + return; + } + assert!( + current != priority || previous == target, + "conflicting scope requests" + ); + } + self.pending = Some((priority, target)); + } + + pub fn begin_load(&mut self) { + self.request(1, MatchScope::Loading); + } + + pub(crate) fn install(&mut self, install: CommandQueue) { + assert!(self.install.is_none(), "two prepared installs in one frame"); + self.install = Some(install); + self.request(2, MatchScope::Live); + } + + pub fn tear_down(&mut self, reason: TeardownReason) { + self.teardown = Some(reason); + self.last_reason = Some(reason); + self.request( + 3, + if reason == TeardownReason::Replaced { + MatchScope::Loading + } else { + MatchScope::Absent + }, + ); + } + + pub fn return_to_menu(&mut self, reason: Option) { + self.last_reason = reason; + self.request(3, MatchScope::Absent); + } + pub fn exit_reason(&self) -> Option { + self.last_reason + } + + pub fn begin_round(&mut self) { + self.round = Some(RoundPhase::Playing); + } + pub fn end_round(&mut self) { + self.round = Some(RoundPhase::Between); + } +} + +fn resolve_scope(world: &mut World) { + let (pending, round) = { + let mut control = world.resource_mut::(); + (control.pending.take(), control.round.take()) + }; + if let Some((_, target)) = pending { + if target != MatchScope::Live { + if let Some(install) = world.resource_mut::().install.take() { + world.resource_mut::().hand_over(install); + } + frame::scope::discard_staging(world); + let announcement = std::mem::take(&mut world.resource_mut::().0); + world + .resource_mut::() + .hand_over(announcement); + } + if target == MatchScope::Live { + if let Some(mut install) = world.resource_mut::().install.take() { + install.apply(world); + } + } + world.resource_mut::>().set(target); + } + if let Some(round) = round + && world.contains_resource::>() + { + world.resource_mut::>().set(round); + } +} + +#[derive(Resource, Default)] +pub(crate) struct InstallAnnouncement(pub CommandQueue); + +fn announce_install(world: &mut World) { + let mut queue = std::mem::take(&mut world.resource_mut::().0); + queue.apply(world); +} + +pub(crate) fn register(app: &mut App) { + if !app.is_plugin_added::() { + app.add_plugins(frame::ScopePlugin); + } + app.staged::(MatchScope::Live) + .staged::(MatchScope::Live) + .staged::(MatchScope::Live) + .staged::(MatchScope::Live) + .staged::(MatchScope::Live) + .staged::(MatchScope::Live) + .scoped::(MatchScope::Live) + .init_resource::() + .init_resource::() + .add_systems( + Update, + follow_round + .in_set(frame::InMatch) + .in_set(frame::ClientSet::Present) + .after(net::publish_presented), + ) + .add_systems(Last, resolve_scope.in_set(frame::scope::ScopeCommit)) + .add_systems( + OnEnter(MatchScope::Live), + publish_ui_images.in_set(ScopeSet::Derive), + ) + .add_systems( + OnExit(MatchScope::Live), + release_ui_images.in_set(ScopeSet::Release), + ) + .add_systems( + OnEnter(MatchScope::Live), + announce_install.in_set(ScopeSet::Announce), + ); +} + +#[derive(Resource, Default)] +struct RoundClock(Option<(u32, bool)>); + +fn follow_round( + adopted: Res, + mut clock: ResMut, + mut control: ResMut, +) { + let Some(snapshot) = adopted.next() else { + return; + }; + let playing = snapshot.meta.phase == sim::MatchPhase::Playing; + let next = (snapshot.meta.round_serial, playing); + if clock.0 == Some(next) { + return; + } + if playing { + control.begin_round(); + } else { + control.end_round(); + } + clock.0 = Some(next); +} + +#[derive(Resource)] +pub(crate) struct MatchUiImages(pub asset_material::UiImagePublication); + +fn publish_ui_images(world: &mut World) { + let publication = world.resource::().0.clone(); + if let Some(previous) = world.remove_resource::() { + world.resource_mut::().hand_over(previous); + } + world.insert_resource(publication); +} + +fn release_ui_images(world: &mut World) { + if let Some(publication) = world.remove_resource::() { + world + .resource_mut::() + .hand_over(publication); + } +} diff --git a/crates/session/src/weapon_content.rs b/crates/session/src/weapon_content.rs index 9b04c2f..7042776 100644 --- a/crates/session/src/weapon_content.rs +++ b/crates/session/src/weapon_content.rs @@ -61,6 +61,7 @@ pub(crate) fn compile( }) .unwrap_or(sim::WeaponScriptSounds::default()); sim::SimWeaponRow { + unlock_requirement: weapons.weapon_unlock_requirement(id).map_err(str::to_owned), wire_id: id, scales: weapon.movement_scales(), execution, @@ -91,7 +92,11 @@ pub(crate) fn compile( .unwrap_or(sim::EquipmentRuntimeFacts::default()), } }); - let content = sim::SimWeaponContent::compile(rows, aliases, pen_table, pen_table_loaded)?; + let ranks = weapons + .rank_progression() + .map_err(|_| sim::SimWeaponContentRefusal::InvalidProgression)?; + let content = + sim::SimWeaponContent::compile(rows, aliases, pen_table, pen_table_loaded, ranks)?; if !refused.is_empty() { diag::info!( Sim, diff --git a/crates/sim/src/carrier.rs b/crates/sim/src/carrier.rs index accf9e0..f837dbd 100644 --- a/crates/sim/src/carrier.rs +++ b/crates/sim/src/carrier.rs @@ -28,7 +28,10 @@ impl Default for SimWorld { install_state_entity(&mut ecs, state_entity); ecs.insert_resource(crate::LocalPlayerProfile::default()); ecs.insert_resource(crate::PersistentDataStore::default()); - ecs.insert_resource(crate::script::Runtime::default()); + ecs.init_resource::(); + ecs.insert_resource(crate::script::RoundScript::new( + ecs.resource::(), + )); ecs.insert_resource(crate::script::Mechanics::default()); ecs.insert_resource(crate::script::NativeRegistry::default()); Self { @@ -89,6 +92,42 @@ impl DerefMut for SimWorld { } impl SimWorld { + pub fn validate_entity_lifetimes(&self) -> Result<(), String> { + crate::frame::validate_entity_payloads(&self.ecs) + } + + pub fn entity_lifetime_summary(&self) -> String { + let snapshot = self.entity_kernel().to_snapshot(); + let occupied = snapshot + .slots + .iter() + .filter(|slot| slot.occupied.is_some()) + .count(); + let held = snapshot + .slots + .iter() + .filter(|slot| { + slot.occupied + .is_some_and(|slot| slot.transient_event_time_ms.is_some()) + }) + .count(); + let runtime = self.ecs.resource::(); + format!( + "occupied_slots = {occupied}\nevent_holds = {held}\nprojectile_payloads = {}\nitem_payloads = {}\nmover_payloads = {}\nscript_objects = {}\nscript_defined_entities = {}\nentity_consistency = {:?}\nmap_glass = {:?}\n", + collect_projectiles(&self.ecs).len(), + collect_dropped_items(&self.ecs).len(), + collect_script_movers(&self.ecs).len(), + runtime.objects.len(), + runtime + .entities + .keys() + .filter(|id| runtime.script_is_defined(id)) + .count(), + self.validate_entity_lifetimes(), + self.content().map().glass + ) + } + pub fn new() -> Self { Self::default() } @@ -176,18 +215,9 @@ impl SimWorld { crate::script::set_dvar(&mut self.ecs, name, value); } - pub fn set_glass_names(&mut self, names: Vec<(String, Vec)>) { - if !names.is_empty() { - let pieces: usize = names.iter().map(|(_, pieces)| pieces.len()).sum(); - diag::info!(Sim, "glass: {} named sets, {pieces} pieces", names.len()); - } - self.ecs - .insert_resource(crate::script::GlassNames(names.into_iter().collect())); - } - pub fn gsc_realm(&self) -> Option { self.ecs - .resource::() + .resource::() .program .as_ref() .map(|program| program.rules()) @@ -195,7 +225,7 @@ impl SimWorld { pub fn gsc_program_fingerprint(&self) -> Option<[u8; 32]> { self.ecs - .resource::() + .resource::() .program_fingerprint() } @@ -205,7 +235,7 @@ impl SimWorld { requested_ms: i32, attained_ms: i32, ) { - let mut runtime = self.ecs.resource_mut::(); + let mut runtime = self.ecs.resource_mut::(); if let Some(slot) = runtime.players.get_mut(&client.0) && slot.seat.archive_ms == requested_ms { @@ -218,7 +248,7 @@ impl SimWorld { } pub fn take_script_fault(&mut self) -> Option { - let mut runtime = self.ecs.resource_mut::(); + let mut runtime = self.ecs.resource_mut::(); if runtime.fault_reported { return None; } @@ -306,7 +336,7 @@ impl SimWorld { } pub fn retire_client(&mut self, id: ClientId) { - let mut runtime = self.ecs.resource_mut::(); + let mut runtime = self.ecs.resource_mut::(); if runtime.players.contains_key(&id.0) { runtime.disconnects.insert(id.0); } else { @@ -361,7 +391,7 @@ impl SimWorld { pub fn slow_motion(&self) -> Option { self.ecs - .resource::() + .resource::() .engine .slow_motion } @@ -386,6 +416,15 @@ impl SimWorld { self.frame().adopt_prediction_snapshot(snapshot, local) } + pub fn request_round_restart(&mut self) -> Result<(), String> { + if !self.is_running() || !self.cheats_enabled() { + return Err("round restart needs an active match with cheats".into()); + } + self.ecs + .resource_mut::() + .request_round_restart(true) + } + pub fn shutdown_game(&mut self) { self.frame().shutdown_game(); crate::script::reset(&mut self.ecs); diff --git a/crates/sim/src/combat.rs b/crates/sim/src/combat.rs index 07e6fe6..091c235 100644 --- a/crates/sim/src/combat.rs +++ b/crates/sim/src/combat.rs @@ -461,6 +461,7 @@ pub(crate) fn advance_weapon_command( } }, perks0: ps.perks[0], + perks1: ps.perks[1], perk_weap_reload_multiplier: weapon_iw4::PERK_WEAP_RELOAD_MULTIPLIER_DEFAULT, offhand: { let mut inventory = [OffhandInvRow::default(); OFFHAND_INV_SLOTS]; diff --git a/crates/sim/src/content.rs b/crates/sim/src/content.rs index 35043af..eebc2c5 100644 --- a/crates/sim/src/content.rs +++ b/crates/sim/src/content.rs @@ -2,7 +2,7 @@ use crate::spawn::{AuthoredSpawnPoint, MatchBootstrap}; use crate::world::SimBrush; use weapon_iw4::WeaponCombatFacts; -pub const CONTENT_DIGEST_SCHEME: u64 = 19; +pub const CONTENT_DIGEST_SCHEME: u64 = 21; #[derive(Clone, Copy)] struct Digest(u64); @@ -361,12 +361,15 @@ pub fn content_digest( combat: &[WeaponCombatFacts], penetration: &[weapon_iw4::BulletPenFacts], runnable: &[bool], + unlocks: &[Result], + ranks: &gamemode_iw4::progression::RankProgression, transition_groups: &[u32], camouflage_slots: &[Vec], equipment: &[crate::EquipmentRuntimeFacts], bootstrap: &MatchBootstrap, clip_brushes: &[SimBrush], script_models: &[crate::EntityCollisionCapabilities], + map_content: &asset_core::MapContentDefinition, ) -> u64 { let mut h = Digest::new(); h.u64(CONTENT_DIGEST_SCHEME); @@ -375,9 +378,12 @@ pub fn content_digest( hash_weapon_admission(&mut h, runnable, transition_groups); hash_camouflage_slots(&mut h, camouflage_slots); hash_equipment(&mut h, equipment); + hash_unlocks(&mut h, unlocks); + hash_ranks(&mut h, ranks); hash_class_catalog(&mut h); hash_spawns(&mut h, &bootstrap.spawns); hash_collision(&mut h, clip_brushes); + hash_map_content(&mut h, map_content); hash_script_models(&mut h, script_models); h.finish() } @@ -397,12 +403,15 @@ pub fn content_components( combat: &[WeaponCombatFacts], penetration: &[weapon_iw4::BulletPenFacts], runnable: &[bool], + unlocks: &[Result], + ranks: &gamemode_iw4::progression::RankProgression, transition_groups: &[u32], camouflage_slots: &[Vec], equipment: &[crate::EquipmentRuntimeFacts], bootstrap: &MatchBootstrap, clip_brushes: &[SimBrush], script_models: &[crate::EntityCollisionCapabilities], + map_content: &asset_core::MapContentDefinition, ) -> ContentComponents { let component = |tag: u8| { let mut h = Digest::new(); @@ -414,6 +423,7 @@ pub fn content_components( let mut map = component(b'M'); hash_spawns(&mut map, &bootstrap.spawns); hash_collision(&mut map, clip_brushes); + hash_map_content(&mut map, map_content); let mut models = component(b'D'); hash_script_models(&mut models, script_models); @@ -424,6 +434,8 @@ pub fn content_components( hash_weapon_admission(&mut weapons, runnable, transition_groups); hash_camouflage_slots(&mut weapons, camouflage_slots); hash_equipment(&mut weapons, equipment); + hash_unlocks(&mut weapons, unlocks); + hash_ranks(&mut weapons, ranks); let mut classes = component(b'C'); hash_class_catalog(&mut classes); @@ -435,3 +447,72 @@ pub fn content_components( classes: classes.finish(), } } + +fn hash_unlocks( + h: &mut Digest, + unlocks: &[Result], +) { + h.u64(unlocks.len() as u64); + for unlock in unlocks { + match unlock { + Ok(requirement) => { + h.byte(1); + h.u32(requirement.rank); + h.u64(requirement.challenges.len() as u64); + for challenge in &requirement.challenges { + h.bytes(challenge.name.as_bytes()); + h.i32(challenge.tier); + } + } + Err(reason) => { + h.byte(0); + h.bytes(reason.as_bytes()); + } + } + } +} + +fn hash_ranks(h: &mut Digest, ranks: &gamemode_iw4::progression::RankProgression) { + h.u64(ranks.thresholds().len() as u64); + for &(rank, xp) in ranks.thresholds() { + h.u32(rank); + h.i32(xp); + } +} + +fn hash_map_content(h: &mut Digest, content: &asset_core::MapContentDefinition) { + match &content.glass { + asset_core::GlassContent::Absent => h.byte(0), + asset_core::GlassContent::Unsupported => h.byte(1), + asset_core::GlassContent::Invalid(error) => { + h.byte(2); + h.bytes(error.as_bytes()); + } + asset_core::GlassContent::Prepared(glass) => { + h.byte(3); + h.u64(glass.panes().len() as u64); + for pane in glass.panes() { + for value in pane + .origin + .into_iter() + .chain(pane.axis_s) + .chain(pane.axis_t) + { + h.f32(value); + } + } + h.u64(glass.names().len() as u64); + for (name, pieces) in glass.names() { + h.bytes(name.as_bytes()); + h.u64(pieces.len() as u64); + for piece in pieces { + h.u32(*piece); + } + } + h.u64(glass.collision_pieces().len() as u64); + for piece in glass.collision_pieces() { + h.u32(*piece); + } + } + } +} diff --git a/crates/sim/src/damage.rs b/crates/sim/src/damage.rs index 7dd820a..2cd092f 100644 --- a/crates/sim/src/damage.rs +++ b/crates/sim/src/damage.rs @@ -121,7 +121,7 @@ pub(crate) fn apply_explosion_blast(world: &mut FrameWorld, tick: Tick, blast: & let means = crate::script_player::means(world, blast.source, blast.weapon, 0, true); let ignore_model = world .ecs() - .get_resource::() + .get_resource::() .and_then(|runtime| match blast.source { DamageSource::Projectile(id) => runtime .missiles diff --git a/crates/sim/src/equipment.rs b/crates/sim/src/equipment.rs index fd6e533..bd802fb 100644 --- a/crates/sim/src/equipment.rs +++ b/crates/sim/src/equipment.rs @@ -1254,7 +1254,11 @@ pub(crate) fn think_projectile(world: &mut FrameWorld, tick: Tick, entnum: i32) *row = projectile; } } else { - let _ = world.remove_projectile_by_number(entnum); + let terminal = detonated + .iter() + .find(|info| info.splash) + .map(|info| (tick, info.projectile)); + let _ = world.finish_projectile(entnum, terminal); } for info in &detonated { impacts.push(ProjectileImpact { @@ -1272,11 +1276,6 @@ pub(crate) fn think_projectile(world: &mut FrameWorld, tick: Tick, entnum: i32) }); } world.record_projectile_impacts(tick, &impacts); - for info in &detonated { - if info.splash { - world.note_dying_missile(tick, info.projectile); - } - } let resolves_damage = world.publishes_snapshot(); if resolves_damage { for (projectile, target) in direct_hits { diff --git a/crates/sim/src/frame.rs b/crates/sim/src/frame.rs index 4736b7a..055b48a 100644 --- a/crates/sim/src/frame.rs +++ b/crates/sim/src/frame.rs @@ -76,19 +76,23 @@ impl DerefMut for FrameWorld<'_> { impl FrameWorld<'_> { pub(crate) fn missile_collision_models(&self, id: crate::ProjectileId) -> Vec { self.ecs - .get_resource::() + .get_resource::() .map(|runtime| runtime.missile_collision_models(id)) .unwrap_or_default() } pub(crate) fn emit_script_entity_trace(&self) { if perf::enabled() - && let Some(runtime) = self.ecs.get_resource::() + && let Some(runtime) = self.ecs.get_resource::() { crate::script::host::trace::entities(runtime, self.entity_collision_capabilities()); } } + pub(crate) fn ecs_ref(&self) -> &World { + self.ecs + } + pub(crate) fn ecs(&mut self) -> &mut World { self.ecs } @@ -103,22 +107,84 @@ impl FrameWorld<'_> { Some(&mut self.ecs.get_mut::(entity)?.into_inner().0) } - /// Not for detonations: `note_dying_missile` keeps their slot until the event expires. - pub(crate) fn despawn_projectile(&mut self, entnum: i32) -> Option { - let projectile = self.remove_projectile_by_number(entnum)?; - self.free_dynamic_entity_number(projectile.entnum); + pub(crate) fn finish_projectile( + &mut self, + number: i32, + terminal: Option<(Tick, ProjectileState)>, + ) -> Option { + let projectile = self.projectile_by_number(number)?; + let entity = self + .entity_kernel() + .current_ref(number) + .expect("live projectile slot"); + self.finish_entity(entity, terminal); Some(projectile) } - pub(crate) fn remove_projectile_by_number(&mut self, entnum: i32) -> Option { - let entity = entity_by_number(self.ecs, entnum)?; - let projectile = self.ecs.get::(entity)?.0; - unbind_number(self.ecs, entnum); + pub(crate) fn despawn_projectile(&mut self, number: i32) -> Option { + self.finish_projectile(number, None) + } + + pub(crate) fn despawn_dynamic_entity(&mut self, number: i32) -> bool { + let Ok(reference) = self.entity_kernel().current_ref(number) else { + return false; + }; + if self.projectile_by_number(number).is_none() + && self.dropped_item_by_number(number).is_none() + && self.script_mover_by_number(number).is_none() + { + return false; + } + self.finish_entity(reference, None); + true + } + + fn finish_entity( + &mut self, + reference: crate::gentity::EntityRef, + terminal: Option<(Tick, ProjectileState)>, + ) { + let slot = self + .entity_kernel() + .resolve(reference) + .expect("live entity generation"); + let number = reference.number(); + let entity = entity_by_number(self.ecs, number).expect("live entity payload"); + let mover = self.ecs.get::(entity).map(|row| row.0.id); + assert!( + match slot.kind { + EntityRunKind::Missile => self.ecs.get::(entity).is_some(), + EntityRunKind::Item => self.ecs.get::(entity).is_some(), + EntityRunKind::ScriptMover => mover.is_some(), + _ => false, + }, + "entity payload does not match its slot" + ); + if slot.kind == EntityRunKind::Item + && let Some(mut runtime) = self.ecs.get_resource_mut::() + { + runtime.retire_item_payload(number); + } + unbind_number(self.ecs, number); assert!( self.ecs.despawn(entity), - "projectile component vanished before removal" + "entity payload vanished before retirement" ); - Some(projectile) + if let Some(id) = mover { + self.remove_collision_owner(id); + } + match terminal { + Some((tick, projectile)) => { + assert_eq!(slot.kind, EntityRunKind::Missile); + assert_eq!(projectile.entnum, number); + self.note_dying_missile(tick, projectile); + } + None => self.free_dynamic_entity_number(number), + } + } + + pub(crate) fn validate_entity_payloads(&self) -> Result<(), String> { + validate_entity_payloads(self.ecs) } pub(crate) fn push_projectile(&mut self, projectile: ProjectileState) { @@ -139,18 +205,14 @@ impl FrameWorld<'_> { } pub(crate) fn remove_script_mover_by_number(&mut self, number: i32) -> bool { - let Some(entity) = entity_by_number(self.ecs, number) else { - return false; - }; - if self.ecs.get::(entity).is_none() { + if self.script_mover_by_number(number).is_none() { return false; } - unbind_number(self.ecs, number); - assert!( - self.ecs.despawn(entity), - "script mover vanished before removal" - ); - self.free_dynamic_entity_number(number); + let entity = self + .entity_kernel() + .current_ref(number) + .expect("live mover slot"); + self.finish_entity(entity, None); true } @@ -276,20 +338,13 @@ impl FrameWorld<'_> { spawn_dropped_item(self.ecs, item); } - pub(crate) fn remove_dropped_item_by_number(&mut self, number: i32) -> Option { - let entity = entity_by_number(self.ecs, number)?; - let item = self.ecs.get::(entity)?.0; - unbind_number(self.ecs, number); - assert!( - self.ecs.despawn(entity), - "dropped item component vanished before removal" - ); - Some(item) - } - pub(crate) fn despawn_dropped_item(&mut self, number: i32) -> Option { - let item = self.remove_dropped_item_by_number(number)?; - self.free_dynamic_entity_number(item.state.number); + let item = self.dropped_item_by_number(number)?; + let entity = self + .entity_kernel() + .current_ref(number) + .expect("live item slot"); + self.finish_entity(entity, None); Some(item) } @@ -1050,3 +1105,103 @@ fn begin_script_movers_rotate_velocity_supplied( } n } + +fn numbered_payload_numbers(world: &World) -> Vec { + world + .get::(state_entity(world)) + .expect("payload index") + .by_number + .iter() + .enumerate() + .filter_map(|(number, entity)| entity.map(|_| number as i32)) + .collect() +} + +pub(crate) fn validate_entity_payloads(world: &World) -> Result<(), String> { + let live_kernel = kernel(world); + let kernel = live_kernel.to_snapshot(); + kernel + .validate() + .map_err(|error| format!("entity kernel: {error:?}"))?; + let events = world + .get::(state_entity(world)) + .expect("simulation state") + .retained_missile_events(); + for (index, slot) in kernel.slots.iter().enumerate() { + let Some(occupied) = slot.occupied else { + continue; + }; + let number = crate::gentity::GENTITY_RESERVED_COUNT + index as i32; + if occupied.transient_event_time_ms.is_some() + && (occupied.kind != EntityRunKind::Missile + || events.iter().filter(|event| event.number == number).count() != 1) + { + return Err(format!( + "entity {number}: event hold lacks one terminal missile event" + )); + } + let entity = entity_by_number(world, number); + let payload = entity.is_some_and(|entity| match occupied.kind { + EntityRunKind::Missile => world.get::(entity).is_some(), + EntityRunKind::Item => world.get::(entity).is_some(), + EntityRunKind::ScriptMover => world.get::(entity).is_some(), + _ => true, + }); + if matches!( + occupied.kind, + EntityRunKind::Missile | EntityRunKind::Item | EntityRunKind::ScriptMover + ) && payload == occupied.transient_event_time_ms.is_some() + { + return Err(format!( + "entity {number}: expected payload or event hold, kind={:?}", + occupied.kind + )); + } + } + for event in events { + if event + .number + .checked_sub(crate::gentity::GENTITY_RESERVED_COUNT) + .and_then(|index| usize::try_from(index).ok()) + .and_then(|index| kernel.slots.get(index)) + .and_then(|slot| slot.occupied) + .is_none_or(|slot| { + slot.kind != EntityRunKind::Missile || slot.transient_event_time_ms.is_none() + }) + { + return Err(format!( + "terminal event {} has no missile hold", + event.number + )); + } + } + for number in numbered_payload_numbers(world) { + let kind = kernel + .occupied_kind(number) + .ok_or_else(|| format!("payload {number} has no occupied slot"))?; + let entity = entity_by_number(world, number).expect("numbered payload index"); + let expected = if world.get::(entity).is_some() { + EntityRunKind::Missile + } else if world.get::(entity).is_some() { + EntityRunKind::Item + } else { + EntityRunKind::ScriptMover + }; + let row_number = if let Some(row) = world.get::(entity) { + row.0.entnum + } else if let Some(row) = world.get::(entity) { + row.0.state.number + } else if let Some(row) = world.get::(entity) { + row.0.state.number + } else { + return Err(format!("index {number} has no typed payload")); + }; + if row_number != number { + return Err(format!("index {number} points to payload {row_number}")); + } + if kind != expected { + return Err(format!("payload {number}: {kind:?} != {expected:?}")); + } + } + Ok(()) +} diff --git a/crates/sim/src/item.rs b/crates/sim/src/item.rs index 56043f3..9cd1c19 100644 --- a/crates/sim/src/item.rs +++ b/crates/sim/src/item.rs @@ -743,8 +743,7 @@ fn grab_scavenger(world: &mut FrameWorld, walker: ClientId, number: i32) { } let picker_pm_type = ps.pm_type; let weapon = u32::try_from(item.state.index).unwrap_or(0); - world.remove_dropped_item_by_number(number); - world.free_dynamic_entity_number(item.state.number); + world.despawn_dropped_item(number); world.item_pickups_mut().push(ItemPickupRecord { picker: walker.0 as i32, weapon, @@ -881,8 +880,7 @@ fn grab_projectile(world: &mut FrameWorld, walker: ClientId, number: i32, tick: let meta = world.client_meta_mut(walker); meta.set_ammo(weapon, clip, stock); meta.mirror_held_ammo(ps.weapon); - world.remove_projectile_by_number(number); - world.free_dynamic_entity_number(number); + world.despawn_projectile(number); world.item_pickups_mut().push(ItemPickupRecord { picker: walker.0 as i32, weapon, diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index 93c120b..d2c4c39 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -26,6 +26,8 @@ mod missile; mod missile_guidance; pub use missile_guidance::{MissileGuide, MissileTarget}; mod penetration; +mod progression; +pub use progression::LocalPlayerData; mod persistent_data; mod persistent_defaults; pub use persistent_data::{ diff --git a/crates/sim/src/match_state/loadout.rs b/crates/sim/src/match_state/loadout.rs index ded865a..568c6db 100644 --- a/crates/sim/src/match_state/loadout.rs +++ b/crates/sim/src/match_state/loadout.rs @@ -88,7 +88,7 @@ pub struct LoadoutSpec { pub perks: [u32; 3], } -pub const PERSONAL_CLASS_SLOTS: usize = 5; +pub const PERSONAL_CLASS_SLOTS: usize = 10; pub const CLASS_CATALOG_DEATHSTREAKS: [&str; 4] = [ "specialty_copycat", diff --git a/crates/sim/src/match_state/snapshot_meta.rs b/crates/sim/src/match_state/snapshot_meta.rs index de677dc..edf7403 100644 --- a/crates/sim/src/match_state/snapshot_meta.rs +++ b/crates/sim/src/match_state/snapshot_meta.rs @@ -89,6 +89,7 @@ pub struct ClientSnapshotMeta { pub struct SnapshotMeta { pub objectives: crate::ObjectiveMatch, pub phase: MatchPhase, + pub round_serial: u32, pub match_elapsed_ms: u32, pub prematch: gamemode_iw4::PrematchStep, diff --git a/crates/sim/src/persistent_data.rs b/crates/sim/src/persistent_data.rs index 1142dc9..c5c2421 100644 --- a/crates/sim/src/persistent_data.rs +++ b/crates/sim/src/persistent_data.rs @@ -124,6 +124,24 @@ impl PersistentDataStore { Ok(()) } + pub fn replace_unbound( + &mut self, + snapshot: AccountSnapshot, + ) -> Result<(), PersistentDataError> { + if self + .clients + .values() + .any(|account| *account == snapshot.account) + { + return Err(PersistentDataError::AccountInUse); + } + let mut updated = self.clone(); + updated.accounts.remove(&snapshot.account); + updated.import(snapshot)?; + *self = updated; + Ok(()) + } + pub fn initialize( &mut self, account: AccountId, @@ -307,6 +325,12 @@ impl PersistentDataStore { pub fn account(&self, client: ClientId) -> Option { self.clients.get(&client).copied() } + pub(crate) fn temporary_client(&self, client: ClientId) -> bool { + self.account(client) + .and_then(|account| self.accounts.get(&account)) + .is_some_and(|record| record.temporary) + } + pub fn for_new_match(&self) -> Self { let mut store = self.clone(); store.clear_bindings(); @@ -481,6 +505,62 @@ impl PersistentDataStore { Ok(()) } + pub(crate) fn submit_ranks( + &mut self, + mode: &str, + ranks: &BTreeMap, + ) -> Result { + let participants: Vec<_> = ranks + .iter() + .filter_map(|(&client, &(group, score))| { + let account = self.account(ClientId(client))?; + let record = self.accounts.get(&account)?; + (!record.temporary).then_some(( + account, + group, + score, + record.skills, + record.revision, + )) + }) + .collect(); + let mut updates = Vec::new(); + for &(account, group, score, skills, revision) in &participants { + let opponents: Vec<_> = participants + .iter() + .filter(|&&(other, other_group, ..)| other != account && other_group != group) + .map(|&(_, _, other_score, other_skills, _)| { + let outcome = match score.cmp(&other_score) { + std::cmp::Ordering::Greater => 1.0, + std::cmp::Ordering::Equal => 0.5, + std::cmp::Ordering::Less => 0.0, + }; + (other_skills, outcome) + }) + .collect(); + if opponents.is_empty() { + continue; + } + let updated = skills + .updated_ranked(mode, &opponents) + .map_err(PersistentDataError::Skill)?; + let revision = revision + .checked_add(1) + .ok_or(PersistentDataError::RevisionOverflow)?; + updates.push((account, updated, revision)); + } + let count = updates.len(); + for (account, skills, revision) in updates { + let record = self + .accounts + .get_mut(&account) + .expect("ranked account exists"); + record.skills = skills; + record.revision = revision; + } + Ok(count) + } + pub fn snapshot(&self, account: AccountId) -> Option { let record = self.accounts.get(&account)?; Some(AccountSnapshot { diff --git a/crates/sim/src/progression.rs b/crates/sim/src/progression.rs new file mode 100644 index 0000000..19cefec --- /dev/null +++ b/crates/sim/src/progression.rs @@ -0,0 +1,399 @@ +use crate::{AccountSnapshot, PersistentDataError}; +use bevy_ecs::prelude::Resource; +use gamemode_iw4::progression::{PlayerProgression, RankProgression, UnlockRequirement}; +use std::sync::Arc; +use structured_data_iw4::{DataType, Definition, Key, Value}; + +pub(crate) fn read_progression( + definition: &Definition, + bytes: &[u8], + ranks: &RankProgression, +) -> Result { + let read = |keys: &[Key<'_>]| -> Result { + match definition.read(bytes, definition.lookup(keys)?)? { + Value::Int(n) => Ok(n), + _ => Err(PersistentDataError::Field( + structured_data_iw4::Error::WrongValue, + )), + } + }; + let experience = read(&[Key::Name("experience")])?; + let prestige = read(&[Key::Name("prestige")])?; + let rank = ranks + .rank(experience) + .ok_or(PersistentDataError::InvalidSchema)?; + let mut challenges = std::collections::BTreeMap::new(); + let DataType::EnumArray(index) = definition.path_type(&[Key::Name("challengeState")])? else { + return Err(PersistentDataError::InvalidSchema); + }; + let enumeration = &definition.enums[definition.enum_arrays[index].enumeration]; + for entry in &enumeration.entries { + let state = read(&[Key::Name("challengeState"), Key::Name(&entry.name)])?; + if state != 0 { + challenges.insert(entry.name.clone(), state); + } + } + Ok(PlayerProgression { + experience, + prestige, + rank, + challenges, + }) +} + +#[derive(Resource, Clone, Debug, Default)] +pub struct LocalPlayerData { + definition: Option>, + ranks: RankProgression, + snapshot: Option, + initial_bytes: Vec, + defaults: Option, + challenges: Option, + rank_tables: std::collections::BTreeMap, + pub progression: Option, +} + +impl LocalPlayerData { + pub fn custom_class_available(&self, index: usize) -> bool { + let Some(progression) = &self.progression else { + return false; + }; + index + < gamemode_iw4::progression::custom_class_capacity( + progression.rank, + progression.prestige, + ) + } + + pub fn unlock_data_ready(&self) -> bool { + self.definition.is_some() && self.defaults.is_some() && self.challenges.is_some() + } + + pub fn install( + &mut self, + definitions: &structured_data_iw4::DefinitionSet, + ranks: RankProgression, + snapshot: Option<&AccountSnapshot>, + ) -> Result<(), PersistentDataError> { + self.progression = None; + self.ranks = ranks; + self.definition = snapshot + .and_then(|snapshot| { + definitions.definitions.iter().find(|definition| { + definition.version == snapshot.version + && definition.checksum == snapshot.checksum + }) + }) + .cloned() + .map(Arc::new) + .or_else(|| definitions.definitions.first().cloned().map(Arc::new)); + self.initial_bytes = self + .definition + .as_ref() + .map_or_else(Vec::new, |definition| vec![0; definition.size]); + self.snapshot = None; + self.refresh(snapshot) + } + + pub fn refresh( + &mut self, + snapshot: Option<&AccountSnapshot>, + ) -> Result<(), PersistentDataError> { + if self.snapshot.as_ref() == snapshot && self.progression.is_some() { + return Ok(()); + } + let progression = match snapshot { + Some(snapshot) => { + let definition = self + .definition + .as_ref() + .ok_or(PersistentDataError::MissingSchema)?; + if definition.version != snapshot.version + || definition.checksum != snapshot.checksum + { + return Err(PersistentDataError::SchemaMismatch); + } + Some(read_progression(definition, &snapshot.bytes, &self.ranks)?) + } + None => Some(PlayerProgression::default()), + }; + self.progression = progression; + self.snapshot = snapshot.cloned(); + Ok(()) + } + + pub fn install_unlock_data( + &mut self, + defaults: Option, + challenges: Option, + ) { + self.defaults = defaults; + self.challenges = challenges; + } + + pub fn unlock_snapshot( + &self, + account: crate::AccountId, + snapshot: Option<&AccountSnapshot>, + unlock: bool, + ) -> Result { + let definition = self + .definition + .as_ref() + .ok_or(PersistentDataError::MissingSchema)?; + let defaults = self + .defaults + .as_ref() + .ok_or(PersistentDataError::MissingDefaults)?; + let initial = crate::persistent_defaults::initialize(definition, defaults)?; + let mut updated = match snapshot { + Some(snapshot) + if snapshot.account == account + && snapshot.version == definition.version + && snapshot.checksum == definition.checksum => + { + snapshot.clone() + } + Some(_) => return Err(PersistentDataError::SchemaMismatch), + None => AccountSnapshot { + account, + revision: 0, + version: definition.version, + checksum: definition.checksum, + bytes: initial.clone(), + skills: Default::default(), + }, + }; + let xp = if unlock { + self.ranks + .thresholds() + .last() + .ok_or(PersistentDataError::InvalidSchema)? + .1 + } else { + 0 + }; + let mut dirty = vec![0; updated.bytes.len().div_ceil(8)]; + let mut changed = definition.write( + &mut updated.bytes, + &mut dirty, + definition.lookup(&[Key::Name("experience")])?, + Value::Int(xp), + )?; + if !unlock { + changed |= definition.write( + &mut updated.bytes, + &mut dirty, + definition.lookup(&[Key::Name("prestige")])?, + Value::Int(0), + )?; + } + let table = if unlock { + Some( + self.challenges + .as_ref() + .ok_or(PersistentDataError::MissingDefaults)?, + ) + } else { + None + }; + for field in ["challengeState", "challengeProgress"] { + let DataType::EnumArray(index) = definition.path_type(&[Key::Name(field)])? else { + return Err(PersistentDataError::InvalidSchema); + }; + let enumeration = &definition.enums[definition.enum_arrays[index].enumeration]; + for entry in &enumeration.entries { + let keys = [Key::Name(field), Key::Name(&entry.name)]; + let lookup = definition.lookup(&keys)?; + let value = if let Some(table) = table { + let Some(row) = + crate::script::host::tables::table_search(table, 0, &entry.name) + else { + continue; + }; + let mut target = 0; + let mut state = 1; + for tier in 1..=10 { + let next = table + .cell(row, 6 + (tier - 1) * 2) + .and_then(|value| value.parse::().ok()) + .unwrap_or(0); + if next <= 0 { + break; + } + target = next; + state = tier as i32 + 1; + } + if target == 0 { + return Err(PersistentDataError::InvalidDefaults); + } + Value::Int(if field == "challengeState" { + state + } else { + target + }) + } else { + definition.read(&initial, lookup)? + }; + changed |= definition.write(&mut updated.bytes, &mut dirty, lookup, value)?; + } + } + if changed { + updated.revision = updated + .revision + .checked_add(1) + .ok_or(PersistentDataError::RevisionOverflow)?; + } + read_progression(definition, &updated.bytes, &self.ranks)?; + Ok(updated) + } + + pub fn install_rank_table(&mut self, name: &str, table: crate::script::StringTable) { + self.rank_tables.insert(name.to_ascii_lowercase(), table); + } + + pub fn table_cell(&self, name: &str, row: i32, column: i32) -> Option<&str> { + let table = self.rank_tables.get(&name.to_ascii_lowercase())?; + Some( + usize::try_from(row) + .ok() + .zip(usize::try_from(column).ok()) + .and_then(|(row, column)| table.cell(row, column)) + .unwrap_or(""), + ) + } + + pub fn table_lookup( + &self, + name: &str, + column: i32, + key: &str, + result_column: i32, + ) -> Option<&str> { + let table = self.rank_tables.get(&name.to_ascii_lowercase())?; + let row = usize::try_from(column) + .ok() + .and_then(|column| crate::script::host::tables::table_search(table, column, key)); + Some( + row.zip(usize::try_from(result_column).ok()) + .and_then(|(row, column)| table.cell(row, column)) + .unwrap_or(""), + ) + } + + pub fn read(&self, keys: &[Key<'_>]) -> Result, PersistentDataError> { + let bytes = self + .snapshot + .as_ref() + .map_or(self.initial_bytes.as_slice(), |snapshot| { + snapshot.bytes.as_slice() + }); + let definition = self + .definition + .as_ref() + .ok_or(PersistentDataError::MissingSchema)?; + Ok(definition.read(bytes, definition.lookup(keys)?)?) + } +} + +pub(crate) fn unlocked( + world: &bevy_ecs::prelude::World, + client: crate::ClientId, + requirement: &UnlockRequirement, +) -> Result { + let store = world.resource::(); + let xp = match store.read(client, &[Key::Name("experience")])? { + Value::Int(xp) => xp, + _ => return Err(PersistentDataError::InvalidBuffer), + }; + let state = world + .get::(crate::frame::state_entity(world)) + .ok_or(PersistentDataError::MissingSchema)?; + let rank = state + .rank_for_xp(xp) + .ok_or(PersistentDataError::InvalidSchema)?; + Ok(requirement.unlocked(rank, |name| { + match store.read(client, &[Key::Name("challengeState"), Key::Name(name)]) { + Ok(Value::Int(state)) => Some(state), + _ => None, + } + })) +} + +pub(crate) fn item_requirement( + world: &bevy_ecs::prelude::World, + name: &str, +) -> Result { + let tables = &world.resource::().tables; + let table = crate::script::host::tables::table(tables, "mp/unlockTable.csv") + .ok_or("unlock.missing_table")?; + let Some(row) = crate::script::host::tables::table_search(table, 0, name) else { + return Ok(Default::default()); + }; + UnlockRequirement::capture( + table.cell(row, 2).unwrap_or(""), + table.cell(row, 3).unwrap_or(""), + ) + .map_err(str::to_owned) +} + +pub(crate) fn validate_class( + world: &crate::frame::FrameWorld, + client: crate::ClientId, + def: &crate::ClassDef, +) -> Result<(), crate::ClassRejectReason> { + let reject = crate::ClassRejectReason::LockedContent; + let store = world.ecs_ref().resource::(); + let prestige = match store.read(client, &[Key::Name("prestige")]) { + Ok(Value::Int(n)) => n, + _ => return Err(reject), + }; + let feature = item_requirement(world.ecs_ref(), "cac").map_err(|_| reject)?; + if !unlocked(world.ecs_ref(), client, &feature).map_err(|_| reject)? { + return Err(reject); + } + let index = def.id.0 as usize; + if index >= gamemode_iw4::progression::custom_class_slot_count(prestige) { + return Err(reject); + } + for weapon in def + .weapon_slot_ids() + .into_iter() + .filter(|&weapon| weapon != 0) + { + let requirement = world + .weapon_unlock_requirement(weapon) + .ok_or(reject)? + .as_ref() + .map_err(|_| reject)?; + if !unlocked(world.ecs_ref(), client, requirement).map_err(|_| reject)? { + return Err(reject); + } + } + for name in def + .perks + .iter() + .filter_map(|&id| crate::match_state::class_catalog_perk_name(id)) + .chain( + (!def.deathstreak.is_empty() && def.deathstreak != "specialty_null") + .then_some(def.deathstreak.as_str()), + ) + { + let requirement = item_requirement(world.ecs_ref(), name).map_err(|_| reject)?; + if !unlocked(world.ecs_ref(), client, &requirement).map_err(|_| reject)? { + return Err(reject); + } + } + if def.perks[0] == 12 + && world + .weapon_setup(def.secondary) + .is_some_and(|setup| setup.attachments.len() > 1) + { + let requirement = + item_requirement(world.ecs_ref(), "specialty_secondarybling").map_err(|_| reject)?; + if !unlocked(world.ecs_ref(), client, &requirement).map_err(|_| reject)? { + return Err(reject); + } + } + Ok(()) +} diff --git a/crates/sim/src/script/host/arrays.rs b/crates/sim/src/script/host/arrays.rs index 7747926..fd30888 100644 --- a/crates/sim/src/script/host/arrays.rs +++ b/crates/sim/src/script/host/arrays.rs @@ -1,5 +1,5 @@ use super::args::kind; -use crate::script::{ArrayKey, Runtime, Value}; +use crate::script::{ArrayKey, RoundScript, Value}; use bevy_ecs::prelude::World; pub(crate) fn array_values(world: &World, value: &Value) -> Result, String> { @@ -7,7 +7,7 @@ pub(crate) fn array_values(world: &World, value: &Value) -> Result, S return Err(format!("{} is not an array", kind(value))); }; Ok(world - .resource::() + .resource::() .arrays .get(id) .ok_or("invalid array reference")? @@ -17,7 +17,7 @@ pub(crate) fn array_values(world: &World, value: &Value) -> Result, S } pub(crate) fn new_array(world: &mut World, values: Vec) -> Result { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let id = runtime.next_object; runtime.next_object = id.checked_add(1).ok_or("object identifier exhausted")?; runtime.arrays.insert( @@ -39,7 +39,7 @@ pub(crate) fn iteration_key( let Value::Array(id) = value else { return Err(format!("{} is not an array", kind(value))); }; - let runtime = world.resource::(); + let runtime = world.resource::(); let array = runtime.arrays.get(id).ok_or("invalid array reference")?; let entry = if let Some(previous) = previous { let key = match previous { diff --git a/crates/sim/src/script/host/audio.rs b/crates/sim/src/script/host/audio.rs index 09bee63..e6a8f58 100644 --- a/crates/sim/src/script/host/audio.rs +++ b/crates/sim/src/script/host/audio.rs @@ -1,5 +1,5 @@ use super::args::{float, optional, string}; -use crate::script::{Namespace, NativeRegistry, Runtime, Value}; +use crate::script::{Namespace, NativeRegistry, RoundScript, Value}; use bevy_ecs::prelude::World; fn change(world: &mut World, args: &[Value], stop: bool, ac130: bool) -> Result { @@ -33,7 +33,7 @@ fn change(world: &mut World, args: &[Value], stop: bool, ac130: bool) -> Result< start_ms: now, end_ms: end, }; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if ac130 { runtime.engine.ac130_ambient = Some(plan); } else { @@ -92,7 +92,7 @@ fn sound_fade(world: &mut World, args: &[Value]) -> Result { fn stop_entity_sounds(world: &mut World, receiver: &Value) -> Result { let id = super::natives::engine::entity_id(world, receiver)?; - let number = world.resource::().entities[&id].number; + let number = world.resource::().entities[&id].number; let origin = match super::players::entity_field(world, id, "origin") { Value::Vector(origin) => origin, _ => [0.0; 3], @@ -142,7 +142,7 @@ fn channel_volumes( } else { let name = string(args, 1)?.to_ascii_lowercase(); let precached = world - .resource::() + .resource::() .precached .keys() .any(|(kind, item)| *kind == "shellshock" && item.eq_ignore_ascii_case(&name)); diff --git a/crates/sim/src/script/host/client_effects.rs b/crates/sim/src/script/host/client_effects.rs index fa15abf..b49f3af 100644 --- a/crates/sim/src/script/host/client_effects.rs +++ b/crates/sim/src/script/host/client_effects.rs @@ -2,7 +2,7 @@ use super::args::{float, optional, string}; use super::natives::player::player; use crate::frame::FrameWorld; use crate::script::Namespace::{Function, Method}; -use crate::script::{NativeRegistry, Runtime, Value}; +use crate::script::{NativeRegistry, RoundScript, Value}; use crate::world::ClientId; use bevy_ecs::prelude::World; @@ -29,7 +29,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { for client in frame.client_ids_sorted() { frame.client_meta_mut(client).view_effects.$field = None; } - world.resource_mut::().engine.$field = Some(vision); + world.resource_mut::().engine.$field = Some(vision); Ok(Value::Undefined) }); registry.register(Method, $player, |world, receiver, args| { diff --git a/crates/sim/src/script/host/controls.rs b/crates/sim/src/script/host/controls.rs index 3c2e070..a27f292 100644 --- a/crates/sim/src/script/host/controls.rs +++ b/crates/sim/src/script/host/controls.rs @@ -2,7 +2,7 @@ use super::args::{float, int, optional, string, vector}; use super::natives::player::player; use crate::frame::FrameWorld; use crate::script::Namespace::{Function, Method}; -use crate::script::{Arc, NativeRegistry, Runtime, Value}; +use crate::script::{Arc, NativeRegistry, RoundScript, Value}; use crate::world::ClientId; use bevy_ecs::prelude::World; use playerstate_iw4::{PlayerState, buttons, weap_flags}; @@ -293,7 +293,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { let upper_left = [float(args, 1)?, float(args, 2)?]; let lower_right = [float(args, 3)?, float(args, 4)?]; let north_yaw = world - .resource::() + .resource::() .engine .worldspawn .get("northyaw") @@ -303,7 +303,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { hud_iw4::compass_map_bounds_from_minimap_corners(upper_left, lower_right, north_yaw) .ok_or("setminimap: the corners enclose no area")?; let (upper_left, north, size) = (bounds.upper_left, bounds.north, bounds.world_size); - world.resource_mut::().engine.minimap = Some(MiniMap { + world.resource_mut::().engine.minimap = Some(MiniMap { upper_left, north, size, @@ -383,13 +383,13 @@ pub(crate) fn select_location( .reason .advances_authority_world(); let receiver = world - .resource::() + .resource::() .players .get(&client) .map(|slot| Value::Object(slot.object)); if let (true, Some(receiver)) = (authority, receiver) { if pressed & buttons::LOCATION_SELECT != 0 { - let map = world.resource::().engine.minimap; + let map = world.resource::().engine.minimap; let loc = |byte: u8| (f32::from(byte as i8) + 128.0) / 255.0; let location = map.map_or([0.0; 3], |map| { let x = loc(cmd.selected_location[0]) * map.size[0]; @@ -437,19 +437,20 @@ fn raise_commands(world: &mut World, client: u32, fired: impl Fn(&str) -> bool) { return; } - let Some((receiver, notifies)) = world - .resource::() - .players - .get(&client) - .map(|slot| { - let notifies: Vec> = slot - .commands - .iter() - .filter(|(command, _)| fired(command)) - .map(|(_, notify)| notify.clone()) - .collect(); - (Value::Object(slot.object), notifies) - }) + let Some((receiver, notifies)) = + world + .resource::() + .players + .get(&client) + .map(|slot| { + let notifies: Vec> = slot + .commands + .iter() + .filter(|(command, _)| fired(command)) + .map(|(_, notify)| notify.clone()) + .collect(); + (Value::Object(slot.object), notifies) + }) else { return; }; diff --git a/crates/sim/src/script/host/entities.rs b/crates/sim/src/script/host/entities.rs index 8ae3f8c..69bf562 100644 --- a/crates/sim/src/script/host/entities.rs +++ b/crates/sim/src/script/host/entities.rs @@ -1,4 +1,4 @@ -use crate::script::{Arc, ArrayKey, BTreeMap, Runtime, Value}; +use crate::script::{Arc, ArrayKey, BTreeMap, RoundScript, Value}; #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct StringTable { @@ -119,7 +119,6 @@ pub(crate) struct EngineState { pub team_scores: BTreeMap, pub team_radar: BTreeMap, pub team_radar_blocked: std::collections::BTreeSet, - pub match_data: super::match_data::MatchData, pub game_end_time: i32, pub slow_motion: Option, pub ambient: Option, @@ -233,7 +232,7 @@ fn vector(text: &str) -> [f32; 3] { std::array::from_fn(|_| parts.next().unwrap_or(0.0)) } -impl Runtime { +impl RoundScript { pub(crate) fn object_field(&mut self, id: u64, name: &str) -> Value { let field = self.symbol(name); self.objects @@ -400,6 +399,12 @@ impl Runtime { )); } self.shown.remove(&id); + self.missiles.retain(|_, object| *object != id); + self.vehicles.remove(&id); + self.planes.remove(&id); + self.fired_once.remove(&id); + self.require_look_at.remove(&id); + self.lingering.retain(|(_, object)| *object != id); for slot in &mut self.engine.attractors { if slot.is_some_and(|a| a.anchor == super::guidance::Anchor::Entity(id)) { *slot = None; diff --git a/crates/sim/src/script/host/entity_damage.rs b/crates/sim/src/script/host/entity_damage.rs index 3159bbd..649ee6b 100644 --- a/crates/sim/src/script/host/entity_damage.rs +++ b/crates/sim/src/script/host/entity_damage.rs @@ -1,7 +1,7 @@ use crate::ScriptModelId; use crate::frame::FrameWorld; use crate::script::runtime::raise; -use crate::script::{Runtime, Value}; +use crate::script::{RoundScript, Value}; use crate::world::ClientId; use bevy_ecs::prelude::World; @@ -85,7 +85,7 @@ pub(crate) enum HitTarget { } pub(crate) fn apply_script_blasts(world: &mut World, tick: crate::Tick) { - let (blasts, hits) = match world.get_resource_mut::() { + let (blasts, hits) = match world.get_resource_mut::() { Some(mut runtime) => ( std::mem::take(&mut runtime.blasts), std::mem::take(&mut runtime.hits), @@ -109,7 +109,7 @@ pub(crate) fn apply_script_blasts(world: &mut World, tick: crate::Tick) { ); continue; } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let at = match runtime .presented_by(target) .map(|o| runtime.object_field(o, "origin")) @@ -170,7 +170,7 @@ pub(crate) fn radius_targets( origin: [f32; 3], radius: f32, ) -> Vec<(ScriptModelId, [f32; 3], f32)> { - let Some(mut runtime) = world.get_resource_mut::() else { + let Some(mut runtime) = world.get_resource_mut::() else { return Vec::new(); }; let candidates: Vec<(u64, ScriptModelId)> = runtime @@ -220,9 +220,11 @@ pub(crate) fn radius_targets( pub(crate) fn damage_entity(world: &mut World, hit: &EntityHit) -> bool { let Some(object) = world - .get_resource::() + .get_resource::() .and_then(|runtime| runtime.presented_by(hit.target)) - .filter(|object| world.resource::().entities[object].accepts_damage(hit.flags)) + .filter(|object| { + world.resource::().entities[object].accepts_damage(hit.flags) + }) else { return false; }; @@ -243,11 +245,13 @@ pub(crate) fn damage_entity(world: &mut World, hit: &EntityHit) -> bool { let attacker = hit.attacker.map_or(Value::Undefined, |a| { super::players::player_object(world, a.0) }); - if world.resource::().entities[&object].kind == super::entities::EntityKind::Vehicle { + if world.resource::().entities[&object].kind + == super::entities::EntityKind::Vehicle + { super::vehicles::damage(world, object, hit, attacker, &weapon, &tag); return true; } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let model = match runtime.object_field(object, "model") { Value::String(model) => model, _ => "".into(), @@ -292,7 +296,7 @@ pub(crate) fn destructible_callback( ) { let Some(object) = owner .script_model() - .and_then(|id| world.get_resource::()?.presented_by(id)) + .and_then(|id| world.get_resource::()?.presented_by(id)) else { return; }; @@ -318,7 +322,7 @@ pub(crate) fn destructible_effect( direction: [f32; 3], ) { let now = super::players::now_ms(world); - let Some(mut runtime) = world.get_resource_mut::() else { + let Some(mut runtime) = world.get_resource_mut::() else { return; }; let Ok(id) = runtime.create_entity(super::entities::EntityKind::Spawned, "script_model") else { @@ -347,7 +351,7 @@ pub(crate) fn set_destructible_model( model: Option<&str>, sound: Option<&str>, ) { - let Some(mut runtime) = world.get_resource_mut::() else { + let Some(mut runtime) = world.get_resource_mut::() else { return; }; let Some(object) = owner.script_model().and_then(|id| runtime.presented_by(id)) else { @@ -375,7 +379,7 @@ pub(crate) fn destructible_debris( 0.0, ]; let Ok(object) = world - .resource_mut::() + .resource_mut::() .create_entity(super::entities::EntityKind::Spawned, "script_model") else { return; @@ -383,7 +387,15 @@ pub(crate) fn destructible_debris( let presence = match super::presence::spawn_presence(world, origin) { Ok(presence) => presence, Err(_) => { - world.resource_mut::().delete_entity(object); + let number = world + .resource::() + .entities + .get(&object) + .map(|entity| entity.number); + if let Some(number) = number { + crate::frame::FrameWorld::from_world(world).despawn_dynamic_entity(number); + } + world.resource_mut::().delete_entity(object); return; } }; @@ -393,7 +405,7 @@ pub(crate) fn destructible_debris( row.dobj = Some(dobj); row.solid = false; } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.set_object_field(object, "origin", Value::Vector(origin)); runtime.set_object_field(object, "angles", Value::Vector(angles)); runtime.set_object_field(object, "model", Value::string(&model)); @@ -403,7 +415,7 @@ pub(crate) fn destructible_debris( drop(runtime); let now = super::players::now_ms(world); world - .resource_mut::() + .resource_mut::() .lingering .push((now + 30000, object)); world diff --git a/crates/sim/src/script/host/guidance.rs b/crates/sim/src/script/host/guidance.rs index e87dbcb..27aa9b3 100644 --- a/crates/sim/src/script/host/guidance.rs +++ b/crates/sim/src/script/host/guidance.rs @@ -2,7 +2,7 @@ use super::args::{arg, float, int, optional, vector}; use super::entities::EntityKind; use crate::frame::FrameWorld; use crate::script::Namespace::{Function, Method}; -use crate::script::{NativeRegistry, Runtime, Value}; +use crate::script::{NativeRegistry, RoundScript, Value}; use bevy_ecs::prelude::World; use glam::Vec3; @@ -30,7 +30,7 @@ pub(crate) fn target( if let Value::Vector(point) = value { return Ok(crate::MissileTarget::Point(*point)); } - let runtime = world.resource::(); + let runtime = world.resource::(); let (_, entity) = runtime .entity(value) .filter(|(id, _)| runtime.live(id)) @@ -62,7 +62,7 @@ fn guide( receiver: &Value, edit: impl FnOnce(&mut crate::MissileGuide), ) -> Result { - let runtime = world.resource::(); + let runtime = world.resource::(); let (_, entity) = runtime .entity(receiver) .filter(|(id, _)| runtime.live(id)) @@ -95,7 +95,7 @@ fn create_attractor( if max_dist <= 0.0 { return Err("parameter 3: maxDist must be greater than zero".into()); } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let slots = &mut runtime.engine.attractors; let index = slots.iter().position(Option::is_none).ok_or_else(|| { format!("Ran out of attractor/repulsors. Max allowed: {ATTRACTOR_SLOTS}") @@ -131,7 +131,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { .ok() .filter(|&index| index < ATTRACTOR_SLOTS) .ok_or("parameter 1: Invalid attractor or repulsor")?; - world.resource_mut::().engine.attractors[index] = None; + world.resource_mut::().engine.attractors[index] = None; Ok(Value::Undefined) }); registry.register(Method, "missile_settargetent", |world, receiver, args| { @@ -177,7 +177,7 @@ pub(crate) use crate::missile_guidance::turn_toward; fn attract(world: &mut World, now: i32) { let slots: Vec = world - .resource::() + .resource::() .engine .attractors .iter() diff --git a/crates/sim/src/script/host/hud.rs b/crates/sim/src/script/host/hud.rs index 880ecc8..8a5ca30 100644 --- a/crates/sim/src/script/host/hud.rs +++ b/crates/sim/src/script/host/hud.rs @@ -1,7 +1,7 @@ use super::args::{arg, float, int, optional, string}; use super::entities::{EntityKind, HudAudience}; use crate::frame::FrameWorld; -use crate::script::{Namespace, NativeRegistry, Runtime, Value}; +use crate::script::{Namespace, NativeRegistry, RoundScript, Value}; use bevy_ecs::prelude::World; use hud_iw4::{ HE_TYPE_CLOCK_DOWN, HE_TYPE_CLOCK_UP, HE_TYPE_MATERIAL, HE_TYPE_PLAYERNAME, @@ -110,17 +110,17 @@ pub(crate) fn new_hud_elem(world: &mut World, audience: HudAudience) -> Result (*client as i32, 0), }; let id = world - .resource_mut::() + .resource_mut::() .create_entity(EntityKind::HudElem, "hudelem")?; let Some(slot) = crate::hudelem::alloc_hud_elem( FrameWorld::from_world(world).hud_elem_slots_mut(), client_num, team, ) else { - world.resource_mut::().delete_entity(id); + world.resource_mut::().delete_entity(id); return Err("no free hud elems".into()); }; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); for (name, value) in DEFAULTS { runtime.set_object_field(id, name, value()); } @@ -130,15 +130,15 @@ pub(crate) fn new_hud_elem(world: &mut World, audience: HudAudience) -> Result().hud_slots.remove(&id); + let slot = world.resource_mut::().hud_slots.remove(&id); if let Some(slot) = slot { crate::hudelem::free_hud_elem(FrameWorld::from_world(world).hud_elem_slots_mut(), slot); } - world.resource_mut::().delete_entity(id); + world.resource_mut::().delete_entity(id); } fn slot_of(world: &World, receiver: &Value) -> Result<(u64, usize), String> { - let runtime = world.resource::(); + let runtime = world.resource::(); match runtime.entity(receiver) { Some((id, e)) if e.kind == EntityKind::HudElem => runtime .hud_slots @@ -249,7 +249,7 @@ pub(crate) fn store_field( name: &str, value: &Value, ) -> Result<(), String> { - let Some(&slot) = world.resource::().hud_slots.get(&id) else { + let Some(&slot) = world.resource::().hud_slots.get(&id) else { return Ok(()); }; let label = if name == "label" { @@ -351,7 +351,7 @@ fn clock(world: &mut World, receiver: &Value, args: &[Value], kind: i32) -> Resu fn plain_text(world: &mut World, value: &Value) -> String { let player = match value { - Value::Object(id) => world.resource::().player_client(*id), + Value::Object(id) => world.resource::().player_client(*id), _ => None, }; let name = player.and_then(|client| super::players::load_field(world, client, "name")); @@ -427,7 +427,7 @@ pub(crate) fn chat( ); let recipients = if team_only { let clients: Vec = world - .resource::() + .resource::() .players .keys() .copied() @@ -522,7 +522,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { s.elem = crate::hudelem::default_hud_elem(); s.archived = 1; }); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); for (name, value) in DEFAULTS { runtime.set_object_field(id, name, value()); } @@ -616,7 +616,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }); registry.register(Method, "settargetent", |world, receiver, args| { let (_, slot) = slot_of(world, receiver)?; - let number = match world.resource::().entity(arg(args, 0)?) { + let number = match world.resource::().entity(arg(args, 0)?) { Some((_, e)) if e.kind != EntityKind::HudElem && e.number >= 0 => e.number, _ => return Err("setTargetEnt needs an entity".into()), }; diff --git a/crates/sim/src/script/host/iw4_gametype.rs b/crates/sim/src/script/host/iw4_gametype.rs index a9742b8..6297617 100644 --- a/crates/sim/src/script/host/iw4_gametype.rs +++ b/crates/sim/src/script/host/iw4_gametype.rs @@ -4,7 +4,7 @@ use crate::frame::FrameWorld; use crate::identities::MatchPhase; use crate::script::runtime::{level_endon_armed, raise, return_from}; -use crate::script::{Runtime, Value}; +use crate::script::{RoundScript, Value}; use bevy_ecs::prelude::World; pub(crate) fn apply_level_notify(frame: &mut FrameWorld, name: &str) { @@ -17,7 +17,7 @@ const WAIT_FOR_PLAYERS: &str = "maps/mp/gametypes/_gamelogic::waitforplayers"; const START_TIMER_BEGINNING: &str = "match_start_timer_beginning"; pub(crate) fn force_match_start(world: &mut World, tick: crate::Tick) -> bool { - let runtime = world.resource::(); + let runtime = world.resource::(); if runtime.fault.is_some() || runtime.program.is_none() { return false; } @@ -28,7 +28,7 @@ pub(crate) fn force_match_start(world: &mut World, tick: crate::Tick) -> bool { return false; } world - .resource_mut::() + .resource_mut::() .set_object_field(0, "prematchperiodend", Value::Int(0)); raise(world, Value::level(), START_TIMER_BEGINNING, Vec::new()); true diff --git a/crates/sim/src/script/host/mechanics.rs b/crates/sim/src/script/host/mechanics.rs index b2fd8cf..0be8fb9 100644 --- a/crates/sim/src/script/host/mechanics.rs +++ b/crates/sim/src/script/host/mechanics.rs @@ -1,6 +1,6 @@ use super::entities::Link; use crate::script::runtime::raise; -use crate::script::{BTreeMap, Resource, Runtime, Value}; +use crate::script::{BTreeMap, Resource, RoundScript, Value}; use bevy_ecs::prelude::World; /// Script-driven entity mechanics: timed moves, launched physics bodies and @@ -121,7 +121,7 @@ impl Mechanics { pub(crate) fn explode( &mut self, - runtime: &mut Runtime, + runtime: &mut RoundScript, center: [f32; 3], outer: f32, inner: f32, @@ -167,7 +167,7 @@ pub(crate) fn advance_mechanics(world: &mut World) { return; } let now = i64::from(request.tick.0) * i64::from(crate::MATCH_TICK_MS); - let runtime = world.resource::(); + let runtime = world.resource::(); if runtime.fault.is_some() || runtime.program.is_none() { return; } @@ -188,7 +188,7 @@ pub(crate) fn deliver_finished(world: &mut World) { fn advance_motions(world: &mut World, now: i64) { world.resource_scope::(|world, mut mechanics| { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let Mechanics { motions, finished, .. } = &mut *mechanics; @@ -215,14 +215,18 @@ fn advance_bodies(world: &mut World) { bodies, finished, .. } = &mut *mechanics; bodies.retain(|object, body| { - if !world.resource::().entities.contains_key(object) { + if !world + .resource::() + .entities + .contains_key(object) + { return false; } if !body.active { return true; } let (origin, angles) = { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if !runtime.entities.contains_key(object) { return false; } @@ -261,7 +265,7 @@ fn advance_bodies(world: &mut World) { } body.ticks += 1; let rested = fraction < 1.0 || body.ticks >= SETTLE_TICKS; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.set_object_field(*object, "origin", Value::Vector(at)); if rested { runtime.set_object_field(*object, "angles", Value::Vector([0.0, angles[1], 0.0])); @@ -284,7 +288,7 @@ fn crush_victims( at: [f32; 3], body: &Body, ) -> Vec<(crate::ClientId, [f32; 3])> { - let presence = world.resource::().entities[&object].presence; + let presence = world.resource::().entities[&object].presence; let frame = crate::frame::FrameWorld::from_world(world); let (mins, maxs) = presence .and_then(|id| { @@ -343,17 +347,20 @@ pub(crate) struct Slide { } pub(crate) fn load_slide_field(world: &World, object: u64, name: &str) -> Option { - (name == "slidevelocity" && world.resource::().entities.contains_key(&object)).then( - || { - world - .resource::() - .slides - .get(&object) - .map_or(Value::Vector([0.0; 3]), |slide| { - Value::Vector(slide.velocity) - }) - }, - ) + (name == "slidevelocity" + && world + .resource::() + .entities + .contains_key(&object)) + .then(|| { + world + .resource::() + .slides + .get(&object) + .map_or(Value::Vector([0.0; 3]), |slide| { + Value::Vector(slide.velocity) + }) + }) } pub(crate) fn store_slide_field( @@ -362,7 +369,12 @@ pub(crate) fn store_slide_field( name: &str, value: &Value, ) -> Result { - if name != "slidevelocity" || !world.resource::().entities.contains_key(&object) { + if name != "slidevelocity" + || !world + .resource::() + .entities + .contains_key(&object) + { return Ok(false); } let Value::Vector(velocity) = value else { @@ -384,7 +396,7 @@ fn advance_slides(world: &mut World) { world.resource_scope::(|world, mut mechanics| { mechanics.slides.retain(|object, slide| { let origin = { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if !runtime.entities.contains_key(object) { return false; } @@ -398,9 +410,11 @@ fn advance_slides(world: &mut World) { } super::presence::settle_collision(world); let at = crate::step::script_slide(world, *object, origin, slide); - world - .resource_mut::() - .set_object_field(*object, "origin", Value::Vector(at)); + world.resource_mut::().set_object_field( + *object, + "origin", + Value::Vector(at), + ); true }); }); @@ -560,7 +574,7 @@ fn clip_slide(velocity: [f32; 3], normal: [f32; 3]) -> [f32; 3] { } fn apply_entity_links(world: &mut World) { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let linked: Vec<(u64, Link)> = runtime .entities .iter() @@ -571,7 +585,8 @@ fn apply_entity_links(world: &mut World) { runtime.entities.get_mut(&id).unwrap().linked_to = None; continue; } - let field = |runtime: &mut Runtime, name| match runtime.object_field(link.parent, name) { + let field = |runtime: &mut RoundScript, name| match runtime.object_field(link.parent, name) + { Value::Vector(v) => v, _ => [0.0; 3], }; diff --git a/crates/sim/src/script/host/natives/engine.rs b/crates/sim/src/script/host/natives/engine.rs index dd9c8a0..0cb10b1 100644 --- a/crates/sim/src/script/host/natives/engine.rs +++ b/crates/sim/src/script/host/natives/engine.rs @@ -7,13 +7,13 @@ use super::math::distance_sq; use crate::bullet_collision::{ MASK_PLAYER_SOLID, MASK_SHOT, PLAYER_MAXS, PLAYER_MINS, TraceOutcome, }; -use crate::script::{Arc, ArrayKey, BTreeMap, Namespace, NativeRegistry, Resource, Runtime, Value}; +use crate::script::{Arc, ArrayKey, Namespace, NativeRegistry, RoundScript, Value}; use bevy_ecs::prelude::World; const ZERO: [f32; 3] = [0.0; 3]; -fn runtime(world: &mut World) -> bevy_ecs::world::Mut<'_, Runtime> { - world.resource_mut::() +fn runtime(world: &mut World) -> bevy_ecs::world::Mut<'_, RoundScript> { + world.resource_mut::() } fn now_ms(world: &World) -> i64 { @@ -49,7 +49,7 @@ fn describe(value: &Value) -> &'static str { } pub(crate) fn entity_id(world: &World, value: &Value) -> Result { - match world.resource::().entity(value) { + match world.resource::().entity(value) { Some((id, e)) if e.kind != EntityKind::HudElem => Ok(id), Some(_) => Err("hud element is not an entity".into()), None => Err(format!("{} is not an entity", describe(value))), @@ -75,7 +75,7 @@ fn vector_field(world: &mut World, id: u64, name: &str) -> [f32; 3] { fn origin_of(world: &mut World, value: &Value) -> Result<[f32; 3], String> { match value { - Value::Object(id) if world.resource::().live(id) => { + Value::Object(id) if world.resource::().live(id) => { Ok(vector_field(world, *id, "origin")) } Value::Vector(v) => Ok(*v), @@ -135,7 +135,7 @@ impl TraceIgnore { } pub(crate) fn trace_ignore(world: &World, value: Option<&Value>) -> TraceIgnore { - let runtime = world.resource::(); + let runtime = world.resource::(); let Some(value) = value else { return TraceIgnore::default(); }; @@ -189,7 +189,7 @@ pub(crate) fn collider_entity( collider: crate::bullet_collision::ColliderId, ) -> Value { use crate::bullet_collision::ColliderId; - let runtime = world.resource::(); + let runtime = world.resource::(); let found = match collider { ColliderId::World { .. } => None, ColliderId::Player { client, .. } => runtime.players.get(&client.0).map(|s| s.object), @@ -302,7 +302,7 @@ fn matching_entities(world: &mut World, args: &[Value]) -> Result, Stri } fn classname_prefix(world: &World, ids: Vec, prefix: &str) -> Vec { - let runtime = world.resource::(); + let runtime = world.resource::(); ids.into_iter() .filter(|id| runtime.entities[id].classname.starts_with(prefix)) .collect() @@ -325,7 +325,7 @@ fn array_values(world: &World, value: &Value) -> Result, String> { return Err(format!("{} is not an array", kind(value))); }; Ok(world - .resource::() + .resource::() .arrays .get(id) .ok_or("invalid array reference")? @@ -404,7 +404,7 @@ fn move_axis( } let duration = seconds_ms(time)?; let id = entity_id(world, receiver)?; - if world.resource::().player_client(id).is_some() { + if world.resource::().player_client(id).is_some() { return Err("axis moves require a non-player entity".into()); } let from = vector_field(world, id, "origin"); @@ -462,7 +462,7 @@ fn add_angle( fn name(world: &World, id: i32) -> Result { world - .resource::() + .resource::() .precached .iter() .find(|((kind, _), index)| *kind == "fx" && **index == id) @@ -682,8 +682,8 @@ fn bind_match_data( .get(&name) .cloned() .ok_or_else(|| format!("match data schema '{name}' is not installed"))?; - runtime(world) - .engine + world + .resource_mut::() .match_data .bind(scope, Arc::clone(&schema))?; let definition = &schema.definitions[0]; @@ -776,7 +776,7 @@ fn register_entities(registry: &mut NativeRegistry) { }); registry.register(Function, "getentarray", |world, _, args| { let ids = if args.is_empty() { - let runtime = world.resource::(); + let runtime = world.resource::(); runtime .entities .iter() @@ -810,7 +810,7 @@ fn register_entities(registry: &mut NativeRegistry) { registry.register(Function, "isplayer", |world, _, args| { let value = arg(args, 0)?; let player = matches!(value, Value::Object(id) - if world.resource::().player_client(*id).is_some()); + if world.resource::().player_client(*id).is_some()); Ok(Value::Int(player.into())) }); registry.register(Function, "isalive", |world, _, args| { @@ -927,7 +927,7 @@ fn register_entities(registry: &mut NativeRegistry) { if runtime(world).player_client(id).is_some() { return Err("cannot delete a client entity".into()); } - if runtime(world).pending_deletes.contains(&id) { + if !runtime(world).can_receive_call(&id) { return Ok(Value::Undefined); } let item = match runtime(world).entities.get(&id).map(|e| &e.kind) { @@ -939,7 +939,7 @@ fn register_entities(registry: &mut NativeRegistry) { } // Script code can still read fields after delete() in the same frame // (for example UAV bookkeeping). Retire at the scheduler's frame boundary. - runtime(world).pending_deletes.push(id); + runtime(world).request_delete(id); crate::script::runtime::raise(world, Value::Object(id), "death", Vec::new()); Ok(Value::Undefined) }); @@ -1027,7 +1027,9 @@ fn register_placement(registry: &mut NativeRegistry) { }); registry.register(Method, "getentitynumber", |world, receiver, _| { let id = entity_id(world, receiver)?; - Ok(Value::Int(world.resource::().entities[&id].number)) + Ok(Value::Int( + world.resource::().entities[&id].number, + )) }); } @@ -1122,14 +1124,14 @@ fn register_appearance(registry: &mut NativeRegistry) { registry.register(Method, "getlightintensity", |world, receiver, _| { let id = entity_id(world, receiver)?; Ok(Value::Float( - world.resource::().entities[&id].light, + world.resource::().entities[&id].light, )) }); } fn slide_object(world: &mut World, receiver: &Value) -> Result { let object = entity_id(world, receiver)?; - let entity = &world.resource::().entities[&object]; + let entity = &world.resource::().entities[&object]; if !matches!( &*entity.classname, "script_model" | "script_brushmodel" | "script_origin" | "light" @@ -1265,7 +1267,7 @@ fn register_motion(registry: &mut NativeRegistry) { }); registry.register(Method, "islinked", |world, receiver, _| { let id = entity_id(world, receiver)?; - let runtime = world.resource::(); + let runtime = world.resource::(); let parent = if let Some(client) = runtime.player_client_of(receiver) { runtime .players @@ -1473,13 +1475,15 @@ fn register_attachments(registry: &mut NativeRegistry) { registry.register(Method, "getattachsize", |world, receiver, _| { let id = entity_id(world, receiver)?; Ok(Value::Int( - world.resource::().entities[&id].attachments.len() as i32, + world.resource::().entities[&id] + .attachments + .len() as i32, )) }); registry.register(Method, "getattachmodelname", |world, receiver, args| { let index = int(args, 0)?; let id = entity_id(world, receiver)?; - let entity = &world.resource::().entities[&id]; + let entity = &world.resource::().entities[&id]; let (model, _) = entity .attachments .get(index.max(0) as usize) @@ -1489,7 +1493,7 @@ fn register_attachments(registry: &mut NativeRegistry) { registry.register(Method, "getattachtagname", |world, receiver, args| { let index = int(args, 0)?; let id = entity_id(world, receiver)?; - let entity = &world.resource::().entities[&id]; + let entity = &world.resource::().entities[&id]; let (_, tag) = entity .attachments .get(index.max(0) as usize) @@ -1826,13 +1830,8 @@ fn register_match(registry: &mut NativeRegistry) { }); registry.register(Function, "map_restart", |world, _, args| { let persist = !args.is_empty() && int(args, 0)? != 0; - let mut state = runtime(world); - if std::mem::replace(&mut state.finished, true) { - return Err("map_restart already called".into()); - } - state.pending_restart = Some(persist); - drop(state); - signal(world, MAP_RESTART) + runtime(world).request_round_restart(persist)?; + Ok(Value::Undefined) }); registry.register(Function, "setmatchdatadef", |world, _, args| { bind_match_data(world, super::super::match_data::Scope::Match, args) @@ -1841,26 +1840,26 @@ fn register_match(registry: &mut NativeRegistry) { bind_match_data(world, super::super::match_data::Scope::Client, args) }); registry.register(Function, "setmatchdata", |world, _, args| { - runtime(world) - .engine + world + .resource_mut::() .match_data .set(super::super::match_data::Scope::Match, args) }); registry.register(Function, "getmatchdata", |world, _, args| { - runtime(world) - .engine + world + .resource_mut::() .match_data .get(super::super::match_data::Scope::Match, args) }); registry.register(Function, "setclientmatchdata", |world, _, args| { - runtime(world) - .engine + world + .resource_mut::() .match_data .set(super::super::match_data::Scope::Client, args) }); registry.register(Function, "getclientmatchdata", |world, _, args| { - runtime(world) - .engine + world + .resource_mut::() .match_data .get(super::super::match_data::Scope::Client, args) }); @@ -2012,27 +2011,33 @@ fn register_level(registry: &mut NativeRegistry) { }); )*}; } - unavailable!("no script ranking service is connected": "sendranks", "setplayerteamrank"); unavailable!("no script match-data upload service is connected": "sendmatchdata", "sendclientmatchdata"); unavailable!("IW4 lobby termination is not bound to the IW4L lobby lifecycle": "endlobby"); unavailable!("IW4 party termination is not bound to the IW4L party lifecycle": "endparty"); } -#[derive(Resource, Default)] -pub struct GlassNames(pub BTreeMap>); - -fn glass_named(world: &World, name: &str) -> Vec { - let Some(pieces) = world - .get_resource::() - .and_then(|names| names.0.get(name)) - else { - return Vec::new(); - }; - pieces.clone() +fn glass_named(world: &mut World, name: &str) -> Result, String> { + let frame = crate::frame::FrameWorld::from_world(world); + match &frame.content().map().glass { + asset_core::GlassContent::Prepared(glass) => Ok(glass.named(name).to_vec()), + asset_core::GlassContent::Absent => Ok(Vec::new()), + asset_core::GlassContent::Unsupported => Err("map family glass is unsupported".into()), + asset_core::GlassContent::Invalid(error) => Err(error.clone()), + } } -fn glass_id(args: &[Value], index: usize) -> Result { - u32::try_from(int(args, index)?).map_err(|_| "glass id must not be negative".into()) +fn glass_id(world: &mut World, args: &[Value], index: usize) -> Result { + let id = + u32::try_from(int(args, index)?).map_err(|_| "glass id must not be negative".to_owned())?; + let frame = crate::frame::FrameWorld::from_world(world); + match &frame.content().map().glass { + asset_core::GlassContent::Prepared(glass) if (id as usize) < glass.panes().len() => Ok(id), + asset_core::GlassContent::Prepared(_) | asset_core::GlassContent::Absent => { + Err(format!("glass piece {id} is absent from map content")) + } + asset_core::GlassContent::Unsupported => Err("map family glass is unsupported".into()), + asset_core::GlassContent::Invalid(error) => Err(error.clone()), + } } fn register_glass(registry: &mut NativeRegistry) { @@ -2040,7 +2045,7 @@ fn register_glass(registry: &mut NativeRegistry) { registry.register(Function, "getglassarray", |world, _, args| { let name = string(args, 0)?; - let ids = glass_named(world, &name) + let ids = glass_named(world, &name)? .into_iter() .map(|id| Value::Int(id as i32)) .collect(); @@ -2048,12 +2053,12 @@ fn register_glass(registry: &mut NativeRegistry) { }); registry.register(Function, "getglass", |world, _, args| { let name = string(args, 0)?; - Ok(glass_named(world, &name) + Ok(glass_named(world, &name)? .first() .map_or(Value::Undefined, |&id| Value::Int(id as i32))) }); registry.register(Function, "destroyglass", |world, _, args| { - let id = glass_id(args, 0)?; + let id = glass_id(world, args, 0)?; let now = i32::try_from(super::super::players::now_ms(world)).unwrap_or(i32::MAX); crate::frame::FrameWorld::from_world(world) .world_objects_mut() @@ -2061,14 +2066,14 @@ fn register_glass(registry: &mut NativeRegistry) { Ok(Value::Undefined) }); registry.register(Function, "isglassdestroyed", |world, _, args| { - let id = glass_id(args, 0)?; + let id = glass_id(world, args, 0)?; let solid = crate::frame::FrameWorld::from_world(world) .world_objects() .glass_is_solid(id); Ok(Value::Int(i32::from(!solid))) }); registry.register(Function, "getglassorigin", |world, _, args| { - let id = glass_id(args, 0)?; + let id = glass_id(world, args, 0)?; Ok(crate::frame::FrameWorld::from_world(world) .world_objects() .glass_pane(id) diff --git a/crates/sim/src/script/host/natives/iw4.rs b/crates/sim/src/script/host/natives/iw4.rs index 895746f..25d944c 100644 --- a/crates/sim/src/script/host/natives/iw4.rs +++ b/crates/sim/src/script/host/natives/iw4.rs @@ -1,5 +1,5 @@ use super::super::args::string; -use crate::script::{Namespace, NativeRegistry, Runtime, Value}; +use crate::script::{Namespace, NativeRegistry, RoundScript, Value}; use bevy_ecs::prelude::World; pub(crate) fn register(registry: &mut NativeRegistry) { @@ -32,7 +32,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { } let name = dvar_name(args)?; let value = dvar_value(args)?; - let mut runtime = runtime(world); + let mut runtime = world.resource_mut::(); let slot = runtime.dvars.entry(name).or_default(); if slot.is_empty() { *slot = value; @@ -110,19 +110,34 @@ pub(crate) fn register(registry: &mut NativeRegistry) { } else { String::new() }; - let mut runtime = runtime(world); - runtime.server_info.insert(name.clone()); - runtime.dvars.entry(name).or_insert(value); + world + .resource_mut::() + .server_info + .insert(name.clone()); + let mut runtime = world.resource_mut::(); + if let std::collections::btree_map::Entry::Vacant(entry) = runtime.dvars.entry(name.clone()) + { + entry.insert(value.clone()); + world + .resource_mut::() + .server_info_defaults + .insert(name, value); + } Ok(Value::Undefined) }); } pub(crate) fn precache(world: &mut World, kind: &'static str, name: String) -> Result { - let mut runtime = runtime(world); - if let Some(&index) = runtime.precached.get(&(kind, name.clone())) { + if let Some(&index) = world + .resource::() + .precached + .get(&(kind, name.clone())) + { return Ok(index); } - if !runtime.loading { + let loading = world.resource::().loading; + let mut runtime = world.resource_mut::(); + if !loading { return Err(format!( "{kind} must be precached before any wait statements in the gametype or level script" )); @@ -136,7 +151,7 @@ pub(crate) fn precache(world: &mut World, kind: &'static str, name: String) -> R } pub(crate) fn register_local_presentation_dvars(world: &mut World, local: Option) { - let mut runtime = runtime(world); + let mut runtime = world.resource_mut::(); runtime.local_presentation_dvars = true; if runtime.local_presentation_client != local { runtime.pending_local_dvars.clear(); @@ -160,7 +175,7 @@ pub(crate) fn register_local_presentation_dvars(world: &mut World, local: Option pub(crate) fn set_dvar(world: &mut World, name: &str, value: &str) { let name = name.to_ascii_lowercase(); - let mut runtime = runtime(world); + let mut runtime = world.resource_mut::(); let value = if runtime.local_presentation_dvars && let Some(setting) = crate::TargetBoxDvar::named(&name) { @@ -182,10 +197,17 @@ pub(crate) fn deliver_local_presentation_dvars(world: &mut World) { { return; } - let Some(client) = world.resource::().local_presentation_client else { + let Some(client) = world + .resource::() + .local_presentation_client + else { return; }; - let commands = std::mem::take(&mut runtime(world).pending_local_dvars); + let commands = std::mem::take( + &mut world + .resource_mut::() + .pending_local_dvars, + ); if crate::frame::FrameWorld::from_world(world) .client_meta(client) .is_none() @@ -197,7 +219,7 @@ pub(crate) fn deliver_local_presentation_dvars(world: &mut World) { set_dvar(world, setting.name(), &value); } let values: Vec<_> = world - .resource::() + .resource::() .dvars .iter() .filter(|(name, _)| crate::TargetBoxDvar::named(name).is_some()) @@ -216,8 +238,8 @@ pub(crate) fn deliver_local_presentation_dvars(world: &mut World) { } } -fn runtime(world: &mut World) -> bevy_ecs::world::Mut<'_, Runtime> { - world.resource_mut::() +fn runtime(world: &mut World) -> bevy_ecs::world::Mut<'_, RoundScript> { + world.resource_mut::() } fn dvar(world: &mut World, args: &[Value]) -> Result { @@ -227,7 +249,12 @@ fn dvar(world: &mut World, args: &[Value]) -> Result { _ => return Err("wrong number of parameters".into()), }; let name = string(args, 0)?.to_ascii_lowercase(); - Ok(runtime(world).dvars.get(&name).cloned().unwrap_or(fallback)) + Ok(world + .resource_mut::() + .dvars + .get(&name) + .cloned() + .unwrap_or(fallback)) } fn dvar_name(args: &[Value]) -> Result { diff --git a/crates/sim/src/script/host/natives/math.rs b/crates/sim/src/script/host/natives/math.rs index 9c14232..6ce9af2 100644 --- a/crates/sim/src/script/host/natives/math.rs +++ b/crates/sim/src/script/host/natives/math.rs @@ -2,7 +2,7 @@ use super::super::args::{arg, float, int, kind, optional, string, vector}; use super::super::arrays::{array_values, iteration_key, new_array}; use super::super::tables::{table, table_lookup, table_lookup_by_row, table_search}; use super::iw4::atoi; -use crate::script::{ArrayKey, Namespace, NativeRegistry, Runtime, Value}; +use crate::script::{ArrayKey, Namespace, NativeRegistry, RoundScript, Value}; use bevy_ecs::prelude::World; fn dot(a: [f32; 3], b: [f32; 3]) -> f32 { @@ -41,7 +41,7 @@ fn nearest_on_segment(a: [f32; 3], b: [f32; 3], point: [f32; 3]) -> [f32; 3] { } pub(crate) fn random(world: &mut World) -> u32 { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let mut x = runtime.rng; x ^= x << 13; x ^= x >> 17; @@ -279,7 +279,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { return Err("getarraykeys expects an array".into()); }; let keys: Vec<_> = world - .resource::() + .resource::() .arrays .get(id) .ok_or("invalid array reference")? @@ -305,7 +305,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { Ok(Value::LocalizedString(table_lookup_by_row(world, args)?)) }); registry.register(Function, "tablelookuprownum", |world, _, args| { - let tables = world.resource::().tables.clone(); + let tables = world.resource::().tables.clone(); let name = string(args, 0)?; let column = int(args, 1)?; let value = string(args, 2)?; diff --git a/crates/sim/src/script/host/natives/player.rs b/crates/sim/src/script/host/natives/player.rs index 61cb41d..a08d514 100644 --- a/crates/sim/src/script/host/natives/player.rs +++ b/crates/sim/src/script/host/natives/player.rs @@ -4,7 +4,7 @@ use super::super::players::{LinkView, PlayerLink}; use super::super::tables::perk_slot_code; use crate::frame::FrameWorld; use crate::script::Namespace::Method; -use crate::script::{Arc, NativeRegistry, Runtime, Value, runtime}; +use crate::script::{Arc, NativeRegistry, RoundScript, Value, runtime}; use crate::script_player; use crate::world::ClientId; use bevy_ecs::prelude::World; @@ -12,7 +12,7 @@ use bevy_ecs::prelude::World; pub(crate) fn player(world: &World, receiver: &Value) -> Result { match receiver { Value::Object(id) => world - .resource::() + .resource::() .player_client(*id) .ok_or_else(|| "receiver is not a player".into()), _ => Err("receiver is not a player".into()), @@ -24,7 +24,7 @@ fn slot<'w>( client: u32, ) -> Result<&'w mut super::super::players::PlayerSlot, String> { world - .resource_mut::() + .resource_mut::() .into_inner() .players .get_mut(&client) @@ -73,7 +73,7 @@ fn publish_client_dvar(world: &mut World, client: u32, name: &str, value: String return; }; let value = value.to_string(); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if runtime.local_presentation_client == Some(ClientId(client)) { runtime.pending_local_dvars.push((setting, value)); return; @@ -187,7 +187,7 @@ fn data_error(error: crate::PersistentDataError) -> String { pub(crate) fn check_data_write(world: &World) -> Result<(), String> { if world - .resource::() + .resource::() .program .as_ref() .is_some_and(|program| program.has_impure_scripts()) @@ -195,7 +195,7 @@ pub(crate) fn check_data_write(world: &World) -> Result<(), String> { return Err("player data cannot be changed after loading external scripts".into()); } if world - .resource::() + .resource::() .dvars .get("developer_script") .and_then(|value| value.parse::().ok()) @@ -481,15 +481,25 @@ pub(crate) fn register(registry: &mut NativeRegistry) { "forceusehintoff", "predictstreampos", ); - macro_rules! answers { - ($value:expr => $($name:literal),* $(,)?) => {$( - registry.register(Method, $name, |world, receiver, _| { - player(world, receiver)?; - Ok($value) - }); - )*}; - } - answers!(Value::Int(1) => "isitemunlocked"); + registry.register(Method, "isitemunlocked", |world, receiver, args| { + let client = data_client(world, player(world, receiver)?)?; + let name = string(args, 0)?; + let tables = &world.resource::().tables; + let table = super::super::tables::table(tables, "mp/unlockTable.csv") + .ok_or("unlock.missing_table")?; + let Some(row) = super::super::tables::table_search(table, 0, &name) else { + return Ok(Value::Int(1)); + }; + let requirement = gamemode_iw4::progression::UnlockRequirement::capture( + table.cell(row, 2).unwrap_or(""), + table.cell(row, 3).unwrap_or(""), + )?; + Ok(Value::Int( + crate::progression::unlocked(world, client, &requirement) + .map_err(data_error)? + .into(), + )) + }); registry.register(Method, "playerhide", |world, receiver, _| { let id = client_of(world, receiver)?; let mut frame = FrameWorld::from_world(world); @@ -501,7 +511,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { registry.register(Method, "playerforcedeathanim", |world, receiver, args| { let id = client_of(world, receiver)?; let inflictor = match args.first() { - Some(Value::Object(obj)) if world.resource::().live(obj) => { + Some(Value::Object(obj)) if world.resource::().live(obj) => { match super::super::players::entity_field(world, *obj, "origin") { Value::Vector(v) => Some(v), _ => None, @@ -551,7 +561,14 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }); Ok(Value::Int(using.into())) }); - answers!(Value::Vector([0.0; 3]) => "getthirdpersoncrosshairoffset"); + registry.register( + Method, + "getthirdpersoncrosshairoffset", + |world, receiver, _| { + player(world, receiver)?; + Ok(Value::Vector([0.0; 3])) + }, + ); } const PLAYER_ANIM_TREE: &str = "multiplayer"; @@ -562,7 +579,10 @@ fn tick(world: &World) -> crate::Tick { fn maybe_player(world: &World, args: &[Value], index: usize) -> Option { match args.get(index) { - Some(Value::Object(id)) => world.resource::().player_client(*id).map(ClientId), + Some(Value::Object(id)) => world + .resource::() + .player_client(*id) + .map(ClientId), _ => None, } } @@ -575,7 +595,7 @@ fn entity_kind( return None; }; world - .resource::() + .resource::() .entities .get(id) .map(|e| (*id, e.kind.clone())) @@ -590,7 +610,11 @@ pub(crate) fn corpse_anim(world: &World, receiver: &Value) -> Result Result { match entity_kind(world, receiver) { - Some((_, super::super::entities::EntityKind::Item(number))) => Ok(number), + Some((id, super::super::entities::EntityKind::Item(number))) + if world.resource::().can_receive_call(&id) => + { + Ok(number) + } _ => Err("receiver is not a weapon item".into()), } } @@ -619,7 +643,7 @@ pub(crate) fn new_item_entity( let origin = FrameWorld::from_world(world) .dropped_item_by_number(number) .map_or([0.0; 3], |i| i.origin); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let id = runtime.create_entity(super::super::entities::EntityKind::Item(number), classname)?; runtime.set_object_field(id, "origin", Value::Vector(origin)); Ok(Value::Object(id)) @@ -651,7 +675,7 @@ fn register_death(registry: &mut NativeRegistry) { _ => None, }; let commit = world - .resource::() + .resource::() .current_hit .as_ref() .filter(|hit| hit.victim == id) @@ -741,7 +765,7 @@ fn register_death(registry: &mut NativeRegistry) { let Some(slot) = script_player::clone_corpse(&mut frame, tick, id) else { return Ok(Value::Undefined); }; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let body = runtime.create_entity( super::super::entities::EntityKind::Corpse { slot, anim }, "player_corpse", @@ -872,7 +896,7 @@ pub(crate) fn link_to( ) -> Result { let client = player(world, receiver)?; let parent = super::engine::entity_id(world, arg(args, 0)?)?; - if world.resource::().player_client(parent) == Some(client) { + if world.resource::().player_client(parent) == Some(client) { return Err("cannot link an entity to itself".into()); } let tag = match args.get(1) { @@ -1253,7 +1277,7 @@ fn register_body(registry: &mut NativeRegistry) { return Err(format!("shellshock duration {seconds} is negative")); } let index = *world - .resource::() + .resource::() .precached .get(&("shellshock", name.clone())) .ok_or_else(|| format!("shellshock '{name}' was not precached"))?; @@ -1398,7 +1422,7 @@ fn register_inventory(registry: &mut NativeRegistry) { let named = weapon_arg(world, args, 0)?; let weapon = player_weapon(world, id, args, 0)?; let t5 = world - .resource::() + .resource::() .program .as_ref() .is_some_and(|program| program.rules() == crate::script::Realm::T5); @@ -1662,7 +1686,7 @@ fn register_inventory(registry: &mut NativeRegistry) { registry.register(Method, $name, |world, receiver, args| { let id = client_of(world, receiver)?; let mut class = offhand_class_of(world, &string(args, 0)?); - if let Some(bridge) = world.resource::().weapon_bridge.get(&id.0).cloned() { + if let Some(bridge) = world.resource::().weapon_bridge.get(&id.0).cloned() { let frame = FrameWorld::from_world(world); if let Some(native_class) = bridge.iter().find_map(|(stand_in, native)| { let source = frame.equipment_facts_for(*stand_in)?; diff --git a/crates/sim/src/script/host/natives/scene_effects.rs b/crates/sim/src/script/host/natives/scene_effects.rs index 47f5a89..45998e9 100644 --- a/crates/sim/src/script/host/natives/scene_effects.rs +++ b/crates/sim/src/script/host/natives/scene_effects.rs @@ -1,7 +1,7 @@ use bevy_ecs::prelude::World; use crate::script::host::args::{float, optional, vector}; -use crate::script::{Runtime, Value}; +use crate::script::{RoundScript, Value}; use crate::{ScriptEarthquake, ScriptFog, ScriptFogParams, ScriptSunFog}; fn duration_ms(seconds: f32) -> Result { @@ -33,7 +33,7 @@ pub(super) fn set_slow_motion( if !slow_motion.valid() { return Err("slow-motion scales must be finite and positive".into()); } - world.resource_mut::().engine.slow_motion = Some(slow_motion); + world.resource_mut::().engine.slow_motion = Some(slow_motion); Ok(Value::Undefined) } @@ -67,7 +67,7 @@ pub(super) fn set_exp_fog(world: &mut World, _: &Value, args: &[Value]) -> Resul } let duration_ms = duration_ms(float(args, if args.len() == 6 { 5 } else { 6 })?)?; let now = crate::level_time_ms(world.resource::().tick); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let previous = runtime.engine.fog.map(|fog| fog.sample(now)); runtime.engine.fog = Some(ScriptFog { from: previous.unwrap_or(target), @@ -94,7 +94,7 @@ pub(super) fn earthquake(world: &mut World, _: &Value, args: &[Value]) -> Result if !quake.valid() { return Err("invalid earthquake parameters".into()); } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let engine = &mut runtime.engine; engine.earthquakes.retain(|quake| quake.active(now)); if engine.earthquakes.len() >= crate::MAX_SCRIPT_EARTHQUAKES { diff --git a/crates/sim/src/script/host/natives/skill.rs b/crates/sim/src/script/host/natives/skill.rs index f04cf79..8243ec5 100644 --- a/crates/sim/src/script/host/natives/skill.rs +++ b/crates/sim/src/script/host/natives/skill.rs @@ -1,7 +1,56 @@ -use super::super::args::{arg, float, string}; +use super::super::args::{arg, float, int, string}; use crate::script::{Namespace, NativeRegistry, Value}; pub(crate) fn register(registry: &mut NativeRegistry) { + registry.register( + Namespace::Function, + "setplayerteamrank", + |world, _, args| { + if args.len() != 3 { + return Err("setplayerteamrank needs a player, ranking group and score".into()); + } + super::player::check_data_write(world)?; + let client = super::player::player(world, arg(args, 0)?)?; + let group = int(args, 1)?; + let score = int(args, 2)?; + if group < 0 { + return Err("ranking group must be nonnegative".into()); + } + let mut runtime = world.resource_mut::(); + if !runtime.ranks_sent { + runtime.team_ranks.insert(client, (group, score)); + } + Ok(Value::Undefined) + }, + ); + registry.register(Namespace::Function, "sendranks", |world, _, args| { + if !args.is_empty() { + return Err("sendranks takes no arguments".into()); + } + super::player::check_data_write(world)?; + let runtime = world.resource::(); + if runtime.ranks_sent { + return Ok(Value::Undefined); + } + let ranks = runtime.team_ranks.clone(); + let mode = crate::frame::FrameWorld::from_world(world) + .game_mode_kind() + .token(); + let count = world + .resource_mut::() + .submit_ranks(mode, &ranks) + .map_err(|error| format!("skill rating: {error:?}"))?; + let mut runtime = world.resource_mut::(); + runtime.team_ranks.clear(); + runtime.ranks_sent = true; + diag::info!( + Sim, + "script rankings submitted: mode={} accounts={}", + mode, + count + ); + Ok(Value::Undefined) + }); registry.register(Namespace::Function, "updateskill", |world, _, args| { if args.len() != 4 { return Err("updateskill needs two players, a mode and a score".into()); diff --git a/crates/sim/src/script/host/natives/t5.rs b/crates/sim/src/script/host/natives/t5.rs index ac84f5b..50c676f 100644 --- a/crates/sim/src/script/host/natives/t5.rs +++ b/crates/sim/src/script/host/natives/t5.rs @@ -4,7 +4,7 @@ use super::super::entities::EntityKind; use super::iw4::atof; use crate::frame::FrameWorld; use crate::script::Namespace::{Function, Method}; -use crate::script::{Arc, BTreeMap, NativeRegistry, Runtime, StringTable, Value}; +use crate::script::{Arc, BTreeMap, NativeRegistry, RoundScript, StringTable, Value}; use crate::script_player; use crate::world::ClientId; use bevy_ecs::prelude::World; @@ -70,7 +70,7 @@ const STATS_CLASSES: usize = 11; const STATS_SLOT: usize = 13; fn t5(world: &mut World) -> &mut T5State { - &mut world.resource_mut::().into_inner().t5 + &mut world.resource_mut::().into_inner().t5 } fn now_ms(world: &World) -> i64 { @@ -186,14 +186,14 @@ fn timeout(world: &World, args: &[Value], index: usize) -> Result, S fn settle_influencers(world: &mut World) { let now = now_ms(world); let owners: Vec<(i32, u64)> = world - .resource::() + .resource::() .t5 .influencers .iter() .filter_map(|(id, i)| i.owner.map(|o| (*id, o))) .collect(); for (id, owner) in owners { - let live = world.resource::().live(&owner); + let live = world.resource::().live(&owner); let origin = live.then(|| origin_of(world, &Value::Object(owner))); let state = t5(world); match origin { @@ -230,7 +230,7 @@ fn sorted_spawn_points( let origin = origin_of(world, &point); let yaw = vector_field(world, &point, "angles")[1].to_radians(); let forward = [yaw.cos(), yaw.sin(), 0.0]; - let state = &world.resource::().t5; + let state = &world.resource::().t5; let mut score: f32 = state .influencers .values() @@ -261,7 +261,7 @@ fn sorted_spawn_points( fn players_on(world: &mut World, team: &str, except: Option) -> Vec { let clients: Vec<(u32, u64)> = world - .resource::() + .resource::() .players .iter() .filter(|(c, slot)| &*slot.sessionstate == "playing" && Some(**c) != except) @@ -304,7 +304,7 @@ fn sight(world: &mut World, start: [f32; 3], end: [f32; 3]) -> bool { fn do_damage(world: &mut World, receiver: &Value, args: &[Value]) -> Result { let amount = float(args, 0)? as i32; let origin = vector(args, 1)?; - let runtime = world.resource::(); + let runtime = world.resource::(); let target = match runtime.player_client_of(receiver) { Some(client) => crate::script::HitTarget::Player(ClientId(client)), None => match runtime.presence_of(receiver) { @@ -341,7 +341,7 @@ fn do_damage(world: &mut World, receiver: &Value, args: &[Value]) -> Result() + .resource_mut::() .hits .push(crate::script::ScriptHit { piece: None, @@ -373,7 +373,7 @@ fn stats_row( world: &World, reference: &str, ) -> Option<(Arc>, usize)> { - let tables = world.resource::().tables.clone(); + let tables = world.resource::().tables.clone(); let table = tables.get(&super::super::entities::table_key(STATS_TABLE))?; let row = (0..table.rows).find(|&row| { table @@ -548,13 +548,13 @@ fn visibility( ) -> Result { let id = super::engine::entity_id(world, receiver)?; let clients: Vec = world - .resource::() + .resource::() .players .keys() .copied() .collect(); let everyone = client_bits(world, clients); - let runtime = world.resource_mut::().into_inner(); + let runtime = world.resource_mut::().into_inner(); change(runtime.entities.get_mut(&id).unwrap(), everyone); Ok(Value::Undefined) } @@ -608,7 +608,7 @@ fn register_script(registry: &mut NativeRegistry) { }); registry.register(Function, "isvehicle", |world, _, args| { let vehicle = world - .resource::() + .resource::() .entity(arg(args, 0)?) .is_some_and(|(_, e)| e.kind == EntityKind::Vehicle); Ok(Value::Int(vehicle.into())) @@ -620,7 +620,7 @@ fn register_script(registry: &mut NativeRegistry) { registry.register(Function, "tablelookupcolumnforrow", |world, _, args| { let name = string(args, 0)?; let (row, column) = (int(args, 1)?, int(args, 2)?); - let tables = world.resource::().tables.clone(); + let tables = world.resource::().tables.clone(); let cell = tables .get(&super::super::entities::table_key(&name)) .filter(|_| row >= 0 && column >= 0) @@ -631,7 +631,7 @@ fn register_script(registry: &mut NativeRegistry) { registry.register(Function, "getdefaultclassslot", |world, _, args| { let class = string(args, 0)?.to_ascii_lowercase(); let slot = string(args, 1)?; - let tables = world.resource::().tables.clone(); + let tables = world.resource::().tables.clone(); let Some(table) = tables.get(&super::super::entities::table_key(STATS_TABLE)) else { return Err(format!("{STATS_TABLE} is not loaded")); }; @@ -663,7 +663,7 @@ fn register_script(registry: &mut NativeRegistry) { }); registry.register(Function, "getreffromitemindex", |world, _, args| { let index = int(args, 0)?.to_string(); - let tables = world.resource::().tables.clone(); + let tables = world.resource::().tables.clone(); let reference = tables .get(&super::super::entities::table_key(STATS_TABLE)) .and_then(|t| { @@ -676,7 +676,7 @@ fn register_script(registry: &mut NativeRegistry) { }); registry.register(Function, "getitemgroupfromitemindex", |world, _, args| { let index = int(args, 0)?.to_string(); - let tables = world.resource::().tables.clone(); + let tables = world.resource::().tables.clone(); let group = tables .get(&super::super::entities::table_key(STATS_TABLE)) .and_then(|t| { @@ -689,7 +689,7 @@ fn register_script(registry: &mut NativeRegistry) { }); registry.register(Function, "getattachmentindex", |world, _, args| { let name = string(args, 0)?; - let tables = world.resource::().tables.clone(); + let tables = world.resource::().tables.clone(); let index = tables .get(&super::super::entities::table_key("mp/attachmenttable.csv")) .and_then(|t| { @@ -708,7 +708,7 @@ fn register_script(registry: &mut NativeRegistry) { if slot <= 0 { return Ok(Value::string("none")); } - let tables = world.resource::().tables.clone(); + let tables = world.resource::().tables.clone(); let attachment = tables .get(&super::super::entities::table_key(STATS_TABLE)) .and_then(|t| { @@ -882,7 +882,7 @@ fn register_script(registry: &mut NativeRegistry) { }); registry.register(Function, "getdroppedweapons", |world, _, _| { let items: Vec = world - .resource::() + .resource::() .entities .iter() .filter(|(_, e)| matches!(e.kind, EntityKind::Item(_))) @@ -994,7 +994,7 @@ fn register_spawning(registry: &mut NativeRegistry) { let team = string(args, 2)?; let except = args .get(3) - .and_then(|p| world.resource::().player_client_of(p)); + .and_then(|p| world.resource::().player_client_of(p)); let target = [origin[0], origin[1], origin[2] + 60.0]; for client in players_on(world, &team, except) { if let Some(eye) = eye(world, client) @@ -1130,7 +1130,7 @@ fn register_player(registry: &mut NativeRegistry) { registry.register(Method, "getperks", |world, receiver, _| { let client = player_id(world, receiver)?; let perks: Vec = world - .resource::() + .resource::() .players .get(&client) .map(|slot| { @@ -1310,7 +1310,7 @@ fn register_entity(registry: &mut NativeRegistry) { }) }); registry.register(Method, "islinkedto", |world, receiver, args| { - let runtime = world.resource::(); + let runtime = world.resource::(); let parent = object_of(arg(args, 0)?); let linked = runtime .entity(receiver) @@ -1323,7 +1323,7 @@ fn register_entity(registry: &mut NativeRegistry) { let number = FrameWorld::from_world(world) .player(id) .map_or(-1, |ps| ps.ground_entity_num); - let runtime = world.resource::(); + let runtime = world.resource::(); Ok(runtime .entities .iter() @@ -1335,7 +1335,7 @@ fn register_entity(registry: &mut NativeRegistry) { registry.register(Method, $name, |world, receiver, args| { let id = super::engine::entity_id(world, receiver)?; let value = arg(args, 0)?.clone(); - world.resource_mut::().set_object_field(id, $field, value); + world.resource_mut::().set_object_field(id, $field, value); Ok(Value::Undefined) }); )*}; @@ -1349,7 +1349,7 @@ fn register_entity(registry: &mut NativeRegistry) { registry.register(Method, "clearentityowner", |world, receiver, _| { let id = super::engine::entity_id(world, receiver)?; world - .resource_mut::() + .resource_mut::() .set_object_field(id, "owner", Value::Undefined); Ok(Value::Undefined) }); @@ -1411,7 +1411,7 @@ fn register_platform(registry: &mut NativeRegistry) { macro_rules! presented { ($($name:literal),* $(,)?) => {$( registry.register(Function, $name, |world, _, args| { - world.resource_mut::().presented.insert($name, args.to_vec()); + world.resource_mut::().presented.insert($name, args.to_vec()); Ok(Value::Undefined) }); )*}; @@ -1445,7 +1445,7 @@ fn register_platform(registry: &mut NativeRegistry) { fn presented_team_flag(world: &World, key: &str, team: &str) -> Value { match world - .resource::() + .resource::() .presented .get(key) .map(Vec::as_slice) diff --git a/crates/sim/src/script/host/objectives.rs b/crates/sim/src/script/host/objectives.rs index 27925e6..da0d0fc 100644 --- a/crates/sim/src/script/host/objectives.rs +++ b/crates/sim/src/script/host/objectives.rs @@ -1,7 +1,7 @@ use super::args::{arg, int, string, vector}; use crate::frame::FrameWorld; use crate::script::runtime::type_name; -use crate::script::{Namespace, NativeRegistry, Runtime, Value}; +use crate::script::{Namespace, NativeRegistry, RoundScript, Value}; use crate::{CompassObjective, ObjectiveMatch, ObjectiveState, ScriptEffect}; use bevy_ecs::prelude::World; use gamemode_iw4::Team; @@ -40,7 +40,7 @@ fn state(args: &[Value], at: usize) -> Result { fn objective(world: &mut World, index: u8) -> bevy_ecs::world::Mut<'_, ScriptObjective> { world - .resource_mut::() + .resource_mut::() .map_unchanged(|runtime| runtime.engine.objectives.entry(index).or_default()) } @@ -77,7 +77,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { registry.register(Function, "objective_delete", |world, _, args| { let index = index(args)?; world - .resource_mut::() + .resource_mut::() .engine .objectives .remove(&index); @@ -110,7 +110,10 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }); registry.register(Function, "objective_team", |world, _, args| { let index = index(args)?; - if let Some(client) = world.resource::().player_client_of(arg(args, 1)?) { + if let Some(client) = world + .resource::() + .player_client_of(arg(args, 1)?) + { let mut objective = objective(world, index); objective.team = Team::Free; objective.viewer = Some(client); @@ -133,7 +136,7 @@ pub(crate) fn publish(world: &mut World) { let vehicles = super::vehicles::compass_rows(world); let vehicle_targets = super::vehicles::hud_targets(world); let rows: Vec<(u8, ScriptObjective)> = world - .resource::() + .resource::() .engine .objectives .iter() @@ -142,7 +145,7 @@ pub(crate) fn publish(world: &mut World) { let mut compass = Vec::with_capacity(rows.len()); for (index, row) in rows { let origin = match &row.entity { - Some(Value::Object(id)) if world.resource::().live(id) => { + Some(Value::Object(id)) if world.resource::().live(id) => { match super::players::entity_field(world, *id, "origin") { Value::Vector(origin) => origin, _ => row.origin, @@ -159,23 +162,24 @@ pub(crate) fn publish(world: &mut World) { viewer: row.viewer, }); } - let runtime = world.resource::(); + let runtime = world.resource::(); + let match_script = world.resource::(); let score = |team: &str| runtime.engine.team_scores.get(team).copied().unwrap_or(0); let scores = [0, score("axis"), score("allies")]; let engine = ENGINE_SERVER_INFO .iter() .filter(|name| !runtime.server_info.contains(**name)) - .filter_map(|name| Some((name.to_string(), runtime.dvars.get(*name)?.clone()))); + .filter_map(|name| Some((name.to_string(), match_script.dvars.get(*name)?.clone()))); let server_info = runtime .server_info .iter() .map(|name| { - let value = runtime.dvars.get(name).cloned().unwrap_or_default(); + let value = match_script.dvars.get(name).cloned().unwrap_or_default(); (name.clone(), value) }) .chain(engine) .chain( - runtime + match_script .dvars .iter() .filter(|(name, _)| { @@ -194,7 +198,7 @@ pub(crate) fn publish(world: &mut World) { .program .is_some() .then(|| runtime.engine.ac130_ambient.clone().unwrap_or_default()); - let rumble_aliases = runtime + let rumble_aliases = match_script .precached .iter() .filter(|((kind, _), _)| *kind == "rumble") @@ -225,7 +229,7 @@ pub(crate) fn publish(world: &mut World) { Some((*id, fx.clone(), viewers)) }) .collect(); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime .engine .effects diff --git a/crates/sim/src/script/host/physics.rs b/crates/sim/src/script/host/physics.rs index 4eea4b9..dcbb13b 100644 --- a/crates/sim/src/script/host/physics.rs +++ b/crates/sim/src/script/host/physics.rs @@ -1,7 +1,7 @@ use super::args::{arg, float, string, vector}; use crate::frame::FrameWorld; use crate::script::Namespace::{Function, Method}; -use crate::script::{NativeRegistry, Runtime, Value}; +use crate::script::{NativeRegistry, RoundScript, Value}; use crate::world::ClientId; use bevy_ecs::prelude::World; @@ -23,7 +23,7 @@ fn usable<'a>( receiver: &Value, ) -> Result<&'a mut super::entities::Usable, String> { let object = object_of(world, receiver)?; - let runtime = world.resource_mut::().into_inner(); + let runtime = world.resource_mut::().into_inner(); let entity = runtime.entities.get_mut(&object).unwrap(); let enabled = matches!(&*entity.classname, "trigger_use" | "trigger_use_touch"); Ok(entity @@ -62,7 +62,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { } world.resource_scope::(|world, mut mechanics| { mechanics.explode( - &mut world.resource_mut::(), + &mut world.resource_mut::(), center, outer, inner, @@ -91,7 +91,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { world .resource_mut::() .launch(object, force); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.entities.get_mut(&object).unwrap().linked_to = None; Ok(Value::Undefined) }); @@ -138,7 +138,7 @@ pub(crate) fn select_usables(world: &mut World) { super::entities::Usable, super::triggers::TriggerPolicy, )> = { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let candidates: Vec<(u64, super::entities::Usable, super::triggers::TriggerPolicy)> = runtime .entities @@ -162,7 +162,7 @@ pub(crate) fn select_usables(world: &mut World) { .collect() }; let clients: Vec = world - .resource::() + .resource::() .players .keys() .copied() @@ -206,5 +206,5 @@ pub(crate) fn select_usables(world: &mut World) { ps.cursor_hint_string = usable.hint; } } - world.resource_mut::().use_selected = selected; + world.resource_mut::().use_selected = selected; } diff --git a/crates/sim/src/script/host/players.rs b/crates/sim/src/script/host/players.rs index 2a71f82..a6fa3a2 100644 --- a/crates/sim/src/script/host/players.rs +++ b/crates/sim/src/script/host/players.rs @@ -1,6 +1,6 @@ use crate::frame::FrameWorld; use crate::script::runtime::{raise, run_now}; -use crate::script::{Arc, BTreeMap, Fault, Location, Runtime, Value, VecDeque}; +use crate::script::{Arc, BTreeMap, Fault, Location, RoundScript, Value, VecDeque}; use crate::world::ClientId; use bevy_ecs::prelude::World; @@ -17,7 +17,7 @@ pub(crate) fn now_ms(world: &World) -> i64 { pub(crate) fn player_object(world: &World, client: u32) -> Value { world - .resource::() + .resource::() .players .get(&client) .map_or(Value::Undefined, |slot| Value::Object(slot.object)) @@ -31,7 +31,7 @@ pub(crate) fn player_damage(world: &mut World, tick: crate::Tick, hit: &crate::s let Value::Object(object) = victim else { return; }; - let runtime = world.resource::(); + let runtime = world.resource::(); if !runtime.entities[&object].accepts_damage(hit.flags) || runtime.engine.players_ignore_radius_damage && hit.flags & crate::script_player::IDFLAGS_RADIUS != 0 @@ -61,10 +61,10 @@ pub(crate) fn player_damage(world: &mut World, tick: crate::Tick, hit: &crate::s Value::string(hitloc), Value::Int(0), ]; - world.resource_mut::().current_hit = Some(hit.clone()); + world.resource_mut::().current_hit = Some(hit.clone()); let now = i64::from(tick.0) * i64::from(crate::MATCH_TICK_MS); let result = run_now(world, DAMAGE, victim, args, now); - world.resource_mut::().current_hit = None; + world.resource_mut::().current_hit = None; if result.is_ok() { // Deaths from this hit are settled before the caller continues. settle_deaths(world); @@ -79,7 +79,7 @@ pub(crate) fn crush_player(world: &mut World, victim: ClientId, pusher: u64, poi let Value::Object(object) = target else { return; }; - let runtime = world.resource::(); + let runtime = world.resource::(); if !runtime.entities[&object].accepts_damage(0) || !runtime.entities.contains_key(&pusher) { return; } @@ -103,14 +103,14 @@ pub(crate) fn crush_player(world: &mut World, victim: ClientId, pusher: u64, poi fn world_entity(world: &World) -> Value { world - .resource::() + .resource::() .engine .world .map_or(Value::Undefined, Value::Object) } pub(crate) fn damage_entity(world: &World, value: Option<&Value>) -> Value { - let runtime = world.resource::(); + let runtime = world.resource::(); match value { Some(Value::Object(object)) if runtime.entities.contains_key(object) @@ -126,7 +126,7 @@ fn damage_inflictor(world: &mut World, hit: &crate::script_player::Hit) -> Optio match hit.inflictor? { crate::script_player::HitInflictor::Projectile(id) => projectile_entity(world, id, hit), crate::script_player::HitInflictor::ScriptModel(presence) => world - .resource::() + .resource::() .presented_by(presence) .map(Value::Object), } @@ -137,7 +137,7 @@ fn projectile_entity( id: crate::ProjectileId, hit: &crate::script_player::Hit, ) -> Option { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let object = runtime .entities .iter() @@ -177,7 +177,7 @@ pub(crate) fn flashbang( pub(crate) fn owe(world: &mut World, client: u32, callback: &'static str, args: Vec) { world - .resource_mut::() + .resource_mut::() .deaths .push_back((client, callback, args)); } @@ -217,7 +217,7 @@ pub(crate) fn force_death(world: &mut World, tick: crate::Tick, client: u32) { } fn profile_data(world: &World, profile: crate::PlayerProfile) -> Option, Value)>> { - let tables = &world.resource::().tables; + let tables = &world.resource::().tables; let titles = super::tables::table(tables, "mp/cardTitleTable.csv")?; let emblems = super::tables::table(tables, "mp/cardIconTable.csv")?; let streaks = super::tables::table(tables, "mp/killstreakTable.csv")?; @@ -301,7 +301,7 @@ pub(crate) fn give_killstreak(world: &mut World, client: u32, name: &str) -> boo pub(crate) fn settle_deaths(world: &mut World) { let now = now_ms(world); loop { - let Some((client, callback, args)) = world.resource_mut::().deaths.pop_front() + let Some((client, callback, args)) = world.resource_mut::().deaths.pop_front() else { return; }; @@ -348,20 +348,20 @@ pub(crate) fn answer_menu(world: &mut World, client: u32, menu: &str, response: } pub(crate) fn answer_join(world: &mut World, client: u32) { - if world.resource_mut::().joined.insert(client) { + if world.resource_mut::().joined.insert(client) { answer_menu(world, client, TEAM_MENU, "autoassign"); } } pub(crate) fn note_team_answer(world: &mut World, client: u32, menu: &str) { if menu == TEAM_MENU { - world.resource_mut::().joined.insert(client); + world.resource_mut::().joined.insert(client); } } fn push_answer(world: &mut World, client: u32, answer: MenuAnswer) { world - .resource_mut::() + .resource_mut::() .menu_answers .entry(client) .or_default() @@ -372,7 +372,7 @@ const T5_DEFAULT_CLASSES: [&str; 5] = ["smg_mp", "cqb_mp", "assault_mp", "lmg_mp pub(crate) fn is_t5(world: &World) -> bool { world - .resource::() + .resource::() .program .as_ref() .is_some_and(|p| p.rules() == crate::script::Realm::T5) @@ -380,11 +380,11 @@ pub(crate) fn is_t5(world: &World) -> bool { pub(crate) fn choose_default_class(world: &mut World, client: u32, index: u8) { world - .resource_mut::() + .resource_mut::() .selected_classes .remove(&client); let realm = world - .resource::() + .resource::() .program .as_ref() .map(|p| p.rules()); @@ -410,7 +410,7 @@ const T5_STAND_INS: [&str; 4] = ["ak47_mp", "m1911_mp", "frag_grenade_mp", "flas pub(crate) fn stand_in_for(world: &mut World, slot: usize, weapon: u32) -> Option { let realm = world - .resource::() + .resource::() .program .as_ref() .map_or(crate::script::Realm::Iw4, |p| p.rules()); @@ -451,7 +451,7 @@ fn bridge_class_weapon(world: &mut World, client: u32, slot: usize, weapon: u32) let Some(stand_in) = stand_in_for(world, slot, weapon) else { return; }; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let bridge = runtime.weapon_bridge.entry(client).or_default(); bridge.retain(|(from, _)| *from != stand_in); bridge.push((stand_in, weapon)); @@ -459,7 +459,7 @@ fn bridge_class_weapon(world: &mut World, client: u32, slot: usize, weapon: u32) fn offhand_stand_in(world: &mut World, weapon: u32) -> Option { let realm = world - .resource::() + .resource::() .program .as_ref() .map_or(crate::script::Realm::Iw4, |p| p.rules()); @@ -488,7 +488,7 @@ fn bridge_offhand(world: &mut World, client: u32, weapon: u32) { let Some(stand_in) = offhand_stand_in(world, weapon) else { return; }; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let bridge = runtime.weapon_bridge.entry(client).or_default(); bridge.retain(|(from, _)| *from != stand_in); bridge.push((stand_in, weapon)); @@ -541,7 +541,7 @@ pub(crate) fn insertion_light( .iter() .find(|(flare, _)| *flare == effect)?; world - .resource::() + .resource::() .insertion_spots .iter() .find(|(spot, _)| { @@ -566,7 +566,7 @@ pub(crate) fn note_insertion_throw( } let model: Arc = format!("{}{native}", crate::WEAPON_MODEL_PREFIX).into(); world - .resource_mut::() + .resource_mut::() .thrown_insertions .insert(client, (model, grenade)); } @@ -576,7 +576,7 @@ pub(crate) fn dress_insertion_glow(world: &mut World, object: u64, model: &str) return; } let origin = match world - .resource_mut::() + .resource_mut::() .object_field(object, "origin") { Value::Vector(origin) => origin, @@ -586,7 +586,7 @@ pub(crate) fn dress_insertion_glow(world: &mut World, object: u64, model: &str) a.iter().zip(b).map(|(a, b)| (a - b) * (a - b)).sum::() <= reach * reach }; let placed = world - .resource::() + .resource::() .insertion_spots .iter() .find(|(spot, _)| near(*spot, origin, 1.0)) @@ -595,7 +595,7 @@ pub(crate) fn dress_insertion_glow(world: &mut World, object: u64, model: &str) Some(model) => model, None => { let throwers: Vec = world - .resource::() + .resource::() .thrown_insertions .keys() .copied() @@ -606,16 +606,14 @@ pub(crate) fn dress_insertion_glow(world: &mut World, object: u64, model: &str) .player(ClientId(client)) .is_some_and(|ps| near(ps.origin, origin, INSERTION_PLANT_REACH)) }); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let Some((model, grenade)) = thrower.and_then(|client| runtime.thrown_insertions.remove(&client)) else { return; }; - if runtime.entities.contains_key(&grenade) - && !runtime.pending_deletes.contains(&grenade) - { - runtime.pending_deletes.push(grenade); + if runtime.entities.contains_key(&grenade) && runtime.can_receive_call(&grenade) { + runtime.request_delete(grenade); } if runtime.insertion_spots.len() >= 32 { runtime.insertion_spots.remove(0); @@ -624,7 +622,7 @@ pub(crate) fn dress_insertion_glow(world: &mut World, object: u64, model: &str) model } }; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let Some(entity) = runtime.entities.get_mut(&object) else { return; }; @@ -643,7 +641,7 @@ fn bridge_insertion_carrier(world: &mut World, client: u32, weapon: u32) { if weapon == 0 || FrameWorld::from_world(world).weapon_script_name(stand_in) != INSERTION { return; } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let bridge = runtime.weapon_bridge.entry(client).or_default(); bridge.retain(|(from, _)| *from != carrier); bridge.push((carrier, weapon)); @@ -667,7 +665,7 @@ fn mark_t6_offhands(world: &mut World, client: u32, class: &crate::ClassDef) { let Value::Object(player) = player_object(world, client) else { return; }; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.set_object_field(player, "t6lethal", lethal); runtime.set_object_field(player, "t6tactical", tactical); } @@ -696,7 +694,7 @@ pub(crate) fn give_carried_insertion( pub(crate) fn bridged_weapon(world: &World, client: u32, weapon: u32) -> u32 { world - .resource::() + .resource::() .weapon_bridge .get(&client) .and_then(|bridge| bridge.iter().find(|(from, _)| *from == weapon)) @@ -705,7 +703,7 @@ pub(crate) fn bridged_weapon(world: &World, client: u32, weapon: u32) -> u32 { pub(crate) fn script_weapon(world: &mut World, client: u32, weapon: u32) -> u32 { if let Some(stand_in) = world - .resource::() + .resource::() .weapon_bridge .get(&client) .and_then(|bridge| bridge.iter().find(|(_, native)| *native == weapon)) @@ -717,7 +715,10 @@ pub(crate) fn script_weapon(world: &mut World, client: u32, weapon: u32) -> u32 .missile_launch_facts(weapon) .is_some_and(|facts| facts.require_lock_to_fire && matches!(facts.missile_guidance, 1 | 3)); if needs_lock_bridge - && world.resource::().players.contains_key(&client) + && world + .resource::() + .players + .contains_key(&client) && let Some(stand_in) = stand_in_for(world, 1, weapon) { bridge_class_weapon(world, client, 1, weapon); @@ -732,15 +733,16 @@ pub(crate) fn personal_class( class: crate::ClassId, ) -> Option { world - .resource::() + .resource::() .personal_classes .get(&(client, class.0)) .cloned() } pub(crate) fn selected_camouflage(world: &World, client: u32, weapon: u32) -> Option { - let runtime = world.resource::(); - let class = runtime + let runtime = world.resource::(); + let class = world + .resource::() .personal_classes .get(&(client, *runtime.selected_classes.get(&client)?))?; [class.primary, class.secondary] @@ -751,20 +753,20 @@ pub(crate) fn selected_camouflage(world: &World, client: u32, weapon: u32) -> Op pub(crate) fn choose_class(world: &mut World, client: u32, class: &crate::ClassDef) { world - .resource_mut::() + .resource_mut::() .selected_classes .insert(client, class.id.0); world - .resource_mut::() + .resource_mut::() .personal_classes .insert((client, class.id.0), class.clone()); let realm = world - .resource::() + .resource::() .program .as_ref() .map(|p| p.rules()); world - .resource_mut::() + .resource_mut::() .weapon_bridge .remove(&client); bridge_class_weapon(world, client, 0, class.primary); @@ -899,7 +901,7 @@ fn menu_kind(menu: &str) -> Option { fn deliver_answers(world: &mut World, client: u32) { // Answers queued ahead of the open menu's kind were for a menu the scripts skipped. - let runtime = world.resource::(); + let runtime = world.resource::(); let Some(slot) = runtime.players.get(&client) else { return; }; @@ -919,12 +921,12 @@ fn deliver_answers(world: &mut World, client: u32) { }) .unwrap_or(0); if skipped > 0 { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if let Some(queue) = runtime.menu_answers.get_mut(&client) { queue.drain(..skipped); } } - let runtime = world.resource::(); + let runtime = world.resource::(); let Some(slot) = runtime.players.get(&client) else { return; }; @@ -940,7 +942,7 @@ fn deliver_answers(world: &mut World, client: u32) { return; } let object = slot.object; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let answer = runtime .menu_answers .get_mut(&client) @@ -949,7 +951,7 @@ fn deliver_answers(world: &mut World, client: u32) { let slot = runtime.players.get_mut(&client).expect("checked above"); slot.menu = None; if let Err(message) = super::natives::player::write_class_data(world, client, &answer.data) { - world.resource_mut::().fault = Some(Fault::at( + world.resource_mut::().fault = Some(Fault::at( &Location { module: "".into(), function: "class selection".into(), @@ -1043,7 +1045,7 @@ impl PlayerSlot { pub(crate) fn script_seats(world: &World) -> Vec<(ClientId, crate::ScriptSeat)> { world - .resource::() + .resource::() .players .iter() .filter(|(_, slot)| &*slot.sessionstate == "spectator" && slot.spectator.target.is_some()) @@ -1068,12 +1070,12 @@ const RADAR_FIELDS: [&str; 3] = ["hasradar", "radarmode", "isradarblocked"]; pub(crate) fn publish_radar(world: &mut World) { let constant = world - .resource::() + .resource::() .dvars .get(crate::CONSTANT_RADAR_DVAR) .is_some_and(|value| value.trim().parse::().is_ok_and(|on| on != 0)); let rows: Vec<(u32, bool, crate::RadarMode, bool)> = world - .resource::() + .resource::() .players .iter() .map(|(client, slot)| (*client, slot.has_radar, slot.radar_mode, slot.radar_blocked)) @@ -1083,7 +1085,7 @@ pub(crate) fn publish_radar(world: &mut World) { Some(Value::String(team)) => team.to_string(), _ => String::new(), }; - let engine = &world.resource::().engine; + let engine = &world.resource::().engine; let team_on = engine.team_radar.get(&team).is_some_and(|on| *on != 0); let team_blocked = engine.team_radar_blocked.contains(&team); let radar = if blocked || team_blocked { @@ -1142,7 +1144,7 @@ pub(crate) fn apply_disconnects(world: &mut World) { { return; } - let disconnects = std::mem::take(&mut world.resource_mut::().disconnects); + let disconnects = std::mem::take(&mut world.resource_mut::().disconnects); for client in disconnects { FrameWorld::from_world(world).retire_client(ClientId(client)); } @@ -1151,17 +1153,25 @@ pub(crate) fn apply_disconnects(world: &mut World) { pub(crate) fn disconnect_player(world: &mut World, client: u32) { super::triggers::release_client_claims(world, client); { - let mut runtime = world.resource_mut::(); + world + .resource_mut::() + .selected_classes + .remove(&client); + let mut runtime = world.resource_mut::(); runtime .personal_classes .retain(|(owner, _), _| *owner != client); runtime.weapon_bridge.remove(&client); - runtime.selected_classes.remove(&client); if runtime.local_presentation_client == Some(ClientId(client)) { runtime.pending_local_dvars.clear(); } } - let Some(slot) = world.resource::().players.get(&client).cloned() else { + let Some(slot) = world + .resource::() + .players + .get(&client) + .cloned() + else { return; }; let now = now_ms(world); @@ -1175,11 +1185,18 @@ pub(crate) fn disconnect_player(world: &mut World, client: u32) { world .resource_mut::() .unbind(crate::ClientId(client)); - let mut runtime = world.resource_mut::(); - runtime.players.remove(&client); - if runtime.local_presentation_client == Some(ClientId(client)) { - runtime.pending_local_dvars.clear(); + if world + .resource::() + .local_presentation_client + == Some(ClientId(client)) + { + world + .resource_mut::() + .pending_local_dvars + .clear(); } + let mut runtime = world.resource_mut::(); + runtime.players.remove(&client); runtime.menu_answers.remove(&client); runtime.joined.remove(&client); runtime.delete_entity(slot.object); @@ -1201,7 +1218,7 @@ pub(crate) fn sync_players(world: &mut World) { return; } let now = i64::from(request.tick.0) * i64::from(crate::MATCH_TICK_MS); - let runtime = world.resource::(); + let runtime = world.resource::(); if runtime.program.is_none() || runtime.fault.is_some() || !runtime.started { return; } @@ -1219,17 +1236,21 @@ pub(crate) fn sync_players(world: &mut World) { .collect() }; for (client, joined) in clients { - let slot = world.resource::().players.get(&client).cloned(); + let slot = world + .resource::() + .players + .get(&client) + .cloned(); match slot { Some(slot) if !slot.begun && joined => { raise(world, Value::Object(slot.object), "begin", Vec::new()); - if let Some(slot) = world.resource_mut::().players.get_mut(&client) { + if let Some(slot) = world.resource_mut::().players.get_mut(&client) { slot.begun = true; } } Some(_) => {} None => { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let object = match runtime.create_player(client) { Ok(object) => object, Err(message) => { @@ -1253,7 +1274,7 @@ pub(crate) fn sync_players(world: &mut World) { }; match pers { Ok(pers) => world - .resource_mut::() + .resource_mut::() .set_object_field(object, "pers", pers), Err(_) => return, } @@ -1284,21 +1305,21 @@ fn client_name(name: &[u8]) -> String { pub(crate) fn load_field(world: &mut World, client: u32, name: &str) -> Option { let id = ClientId(client); if name == "sessionstate" { - let runtime = world.resource::(); + let runtime = world.resource::(); return runtime .players .get(&client) .map(|slot| Value::String(slot.sessionstate.clone().into())); } if SEAT_FIELDS.contains(&name) { - let runtime = world.resource::(); + let runtime = world.resource::(); return runtime .players .get(&client) .map(|slot| load_seat_field(&slot.seat, name)); } if RADAR_FIELDS.contains(&name) { - let slot = world.resource::().players.get(&client)?; + let slot = world.resource::().players.get(&client)?; return Some(match name { "hasradar" => Value::Int(slot.has_radar.into()), "isradarblocked" => Value::Int(slot.radar_blocked.into()), @@ -1331,17 +1352,17 @@ pub(crate) fn load_field(world: &mut World, client: u32, name: &str) -> Option Value { - if let Some(client) = world.resource::().player_client(id) + if let Some(client) = world.resource::().player_client(id) && let Some(value) = load_field(world, client, name) { return value; } - world.resource_mut::().object_field(id, name) + world.resource_mut::().object_field(id, name) } pub(crate) fn alive_on_team(world: &mut World, team: &str) -> i32 { let clients: Vec = world - .resource::() + .resource::() .players .iter() .filter(|(_, slot)| &*slot.sessionstate == "playing") @@ -1371,7 +1392,7 @@ pub(crate) fn store_field( other => Err(format!("player field {name} takes a number, not {other:?}")), }; if SEAT_FIELDS.contains(&name) { - if let Some(slot) = world.resource_mut::().players.get_mut(&client) { + if let Some(slot) = world.resource_mut::().players.get_mut(&client) { store_seat_field(&mut slot.seat, name, value)?; } return Ok(true); @@ -1391,7 +1412,7 @@ pub(crate) fn store_field( Some(_) => false, None => int(value)? != 0, }; - if let Some(slot) = world.resource_mut::().players.get_mut(&client) { + if let Some(slot) = world.resource_mut::().players.get_mut(&client) { match (name, mode) { (_, Some(mode)) => slot.radar_mode = mode, ("hasradar", _) => slot.has_radar = on, @@ -1408,7 +1429,7 @@ pub(crate) fn store_field( if !matches!(&**state, "playing" | "dead" | "spectator" | "intermission") { return Err(format!("invalid sessionstate '{state}'")); } - if let Some(slot) = world.resource_mut::().players.get_mut(&client) { + if let Some(slot) = world.resource_mut::().players.get_mut(&client) { slot.sessionstate = state.clone().into(); } } @@ -1539,7 +1560,7 @@ pub(crate) fn link_player(world: &mut World, client: u32, link: PlayerLink) { ps.link_weapon_angles = ps.viewangles; } } - if let Some(slot) = world.resource_mut::().players.get_mut(&client) { + if let Some(slot) = world.resource_mut::().players.get_mut(&client) { slot.link = Some(link); } } @@ -1547,7 +1568,7 @@ pub(crate) fn link_player(world: &mut World, client: u32, link: PlayerLink) { pub(crate) fn unlink_player(world: &mut World, client: u32) { let id = ClientId(client); let link = world - .resource_mut::() + .resource_mut::() .players .get_mut(&client) .and_then(|slot| slot.link.take()); @@ -1601,7 +1622,7 @@ fn scale_rotation(m: [[f32; 3]; 3], fraction: f32) -> [[f32; 3]; 3] { /// a `PM_TYPE_NORMAL_LINKED` origin alone. pub(crate) fn apply_player_links(world: &mut World) { let links: Vec<(u32, PlayerLink)> = world - .resource::() + .resource::() .players .iter() .filter_map(|(client, slot)| Some((*client, slot.link.clone()?))) @@ -1612,7 +1633,7 @@ pub(crate) fn apply_player_links(world: &mut World) { .client_meta(id) .is_some_and(|meta| meta.controls.linked); let parent_alive = world - .resource::() + .resource::() .objects .contains_key(&link.parent); if !still_linked || !parent_alive { @@ -1628,7 +1649,7 @@ pub(crate) fn apply_player_links(world: &mut World) { ); let parent = math_iw4::axis_to_angles(axis); let entity_num = world - .resource::() + .resource::() .entities .get(&link.parent) .map_or(playerstate_iw4::ENTITYNUM_NONE, |entity| entity.number); @@ -1687,7 +1708,7 @@ pub(crate) fn apply_player_links(world: &mut World) { angles, }); } - if let Some(slot) = world.resource_mut::().players.get_mut(&client) + if let Some(slot) = world.resource_mut::().players.get_mut(&client) && let Some(link) = slot.link.as_mut() { link.parent_axis = axis; diff --git a/crates/sim/src/script/host/presence.rs b/crates/sim/src/script/host/presence.rs index e036925..11cf3d2 100644 --- a/crates/sim/src/script/host/presence.rs +++ b/crates/sim/src/script/host/presence.rs @@ -3,7 +3,7 @@ use crate::ScriptModelId; use crate::bullet_collision::{AuthorityDObjState, EntityCollisionCapabilities}; use crate::frame::FrameWorld; use crate::gentity::ScriptMoverGentity; -use crate::script::{Arc, BTreeMap, Runtime, Value}; +use crate::script::{Arc, BTreeMap, RoundScript, Value}; use bevy_ecs::prelude::World; #[derive(Clone, Debug, PartialEq)] @@ -43,14 +43,14 @@ fn near_angles(a: [f32; 3], b: [f32; 3]) -> bool { .all(|(a, b)| math_iw4::angle_subtract(*a, b).abs() < 0.01) } -fn vector(runtime: &mut Runtime, id: u64, name: &str) -> [f32; 3] { +fn vector(runtime: &mut RoundScript, id: u64, name: &str) -> [f32; 3] { match runtime.object_field(id, name) { Value::Vector(v) => v, _ => [0.0; 3], } } -fn model_field(runtime: &mut Runtime, id: u64) -> Option> { +fn model_field(runtime: &mut RoundScript, id: u64) -> Option> { match runtime.object_field(id, "model") { Value::String(model) if !model.is_empty() && !model.starts_with('*') => Some(model.into()), _ => None, @@ -68,7 +68,7 @@ pub(crate) fn initialize_map_models(world: &mut World) { )) }) .collect(); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let objects: Vec<_> = runtime .entities .iter() @@ -81,7 +81,7 @@ pub(crate) fn initialize_map_models(world: &mut World) { } pub(crate) fn spawn_presence(world: &mut World, origin: [f32; 3]) -> Result { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let serial = runtime.next_spawned_presence; runtime.next_spawned_presence = serial .checked_add(1) @@ -111,7 +111,7 @@ pub(crate) fn settled(world: &mut World) -> FrameWorld<'_> { /// Collision rows only. Networked mover state is left to `present`: posing a /// mover mid-tick would break its tick-to-tick velocity. pub(crate) fn settle_collision(world: &mut World) { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let placed: Vec<(u64, ScriptModelId, bool, bool)> = runtime .entities .iter() @@ -171,7 +171,7 @@ pub(crate) fn sync_presence(world: &mut World) { } fn publish_killcam_cameras(world: &mut World) { - let runtime = world.resource::(); + let runtime = world.resource::(); let cameras: Vec<_> = runtime .entities .iter() @@ -196,7 +196,7 @@ fn publish_killcam_cameras(world: &mut World) { } fn present(world: &mut World, now: i32) { - let retired = std::mem::take(&mut world.resource_mut::().retired_presence); + let retired = std::mem::take(&mut world.resource_mut::().retired_presence); let wanted = collect_wanted(world); if retired.is_empty() && wanted.is_empty() { return; @@ -213,7 +213,6 @@ fn present(world: &mut World, now: i32) { }; if spawned { frame.remove_script_mover_by_number(mover.state.number); - frame.remove_collision_owner(id); } else if let Some(mover) = frame.script_mover_mut_by_number(mover.state.number) { mover.state.e_flags |= entity_iw4::CG_SCRIPT_MOVER_NODRAW; mover.nonsolid = true; @@ -226,7 +225,7 @@ fn present(world: &mut World, now: i32) { }; let first = frame .ecs() - .resource::() + .resource::() .shown .get(&want.object) .cloned(); @@ -278,7 +277,7 @@ fn present(world: &mut World, now: i32) { } let movers = crate::frame::collect_script_movers(frame.ecs()); crate::presence::follow_movers(frame.entity_collision_capabilities_mut(), &movers, now); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); for (object, number, shown) in updates { let Some(entity) = runtime.entities.get_mut(&object) else { continue; @@ -292,7 +291,7 @@ fn present(world: &mut World, now: i32) { fn tag_lookup(world: &mut World, object: u64, tag: &str) -> Option> { let presence = world - .resource::() + .resource::() .entities .get(&object)? .presence?; @@ -317,7 +316,7 @@ pub(crate) fn tag_world( tag: &str, ) -> Option<([f32; 3], [[f32; 3]; 3])> { let presence = world - .resource::() + .resource::() .entities .get(&object)? .presence?; @@ -346,7 +345,7 @@ pub(crate) fn tag_offset(world: &mut World, object: u64, tag: &str) -> Option<[f fn resolve_link_tags(world: &mut World) { let pending: Vec<(u64, u64, Arc)> = world - .resource::() + .resource::() .entities .iter() .filter_map(|(id, e)| { @@ -359,7 +358,7 @@ fn resolve_link_tags(world: &mut World) { continue; }; if let Some(link) = world - .resource_mut::() + .resource_mut::() .entities .get_mut(&id) .and_then(|e| e.linked_to.as_mut()) @@ -370,7 +369,7 @@ fn resolve_link_tags(world: &mut World) { } fn collect_wanted(world: &mut World) -> Vec { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let ids: Vec<(u64, ScriptModelId, bool, u64, bool)> = runtime .entities .iter() @@ -477,7 +476,7 @@ fn present_model(frame: &mut FrameWorld, want: &Wanted) { const UNPRESENTED_LOOP_OWNER: u32 = 0x2000_0000; fn publish_loop_sounds(world: &mut World) { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let speaking: Vec<(u64, Option, Arc, i32)> = runtime .entities .iter() @@ -512,7 +511,7 @@ fn publish_loop_sounds(world: &mut World) { frame.world_objects_mut().set_destructible_loop_sounds(rows); } -impl Runtime { +impl RoundScript { // GSC can link solid visual models to a missile (for example its grenade indicator). // Those models remain hittable, but must not obstruct their own parent missile. pub(crate) fn missile_collision_models(&self, id: crate::ProjectileId) -> Vec { diff --git a/crates/sim/src/script/host/registry.rs b/crates/sim/src/script/host/registry.rs index 72825bb..7ab6f97 100644 --- a/crates/sim/src/script/host/registry.rs +++ b/crates/sim/src/script/host/registry.rs @@ -2,7 +2,7 @@ use bevy_ecs::prelude::{Resource, World}; use std::collections::BTreeMap; use crate::script::host::natives; -use crate::script::{Namespace, Runtime, Value}; +use crate::script::{Namespace, RoundScript, Value}; pub type Native = fn(&mut World, &Value, &[Value]) -> Result; @@ -18,8 +18,8 @@ impl Default for NativeRegistry { let defined = match &args[0] { Value::Undefined => false, Value::Object(id) => { - let runtime = world.resource::(); - runtime.live(id) && !runtime.pending_deletes.contains(id) + let runtime = world.resource::(); + runtime.script_is_defined(id) } _ => true, }; @@ -39,7 +39,7 @@ impl Default for NativeRegistry { if !args.is_empty() { return Err("spawnstruct expects no arguments".into()); } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let id = runtime.next_object; runtime.next_object = id.checked_add(1).ok_or("object identifier exhausted")?; runtime.objects.insert(id, BTreeMap::new()); @@ -81,7 +81,7 @@ pub(crate) fn unavailable( name: &'static str, reason: &str, ) -> Result { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let calls = runtime.unsupported.entry(name).or_default(); *calls = calls.saturating_add(1); Err(format!("unavailable: {reason}")) diff --git a/crates/sim/src/script/host/restart.rs b/crates/sim/src/script/host/restart.rs index d4d6753..b10f3fd 100644 --- a/crates/sim/src/script/host/restart.rs +++ b/crates/sim/src/script/host/restart.rs @@ -1,8 +1,8 @@ use super::entities::{EntityKind, KeyType}; use crate::frame::FrameWorld; -use crate::script::runtime::{install_level, reset, run_now}; +use crate::script::runtime::{install_level, run_now}; use crate::script::{ - Arc, ArrayKey, BTreeMap, Fault, Location, NativeRegistry, Runtime, StringTable, Value, + Arc, ArrayKey, BTreeMap, Fault, Location, NativeRegistry, RoundScript, StringTable, Value, }; use bevy_ecs::prelude::World; @@ -34,7 +34,7 @@ pub(crate) enum Detached { const MAX_DEPTH: usize = 64; -fn detach(runtime: &Runtime, value: &Value, depth: usize) -> Option { +fn detach(runtime: &RoundScript, value: &Value, depth: usize) -> Option { match value { Value::Undefined | Value::Int(_) @@ -54,7 +54,7 @@ fn detach(runtime: &Runtime, value: &Value, depth: usize) -> Option { } } -pub(crate) fn attach(runtime: &mut Runtime, value: Detached) -> Result { +pub(crate) fn attach(runtime: &mut RoundScript, value: Detached) -> Result { match value { Detached::Value(value) => Ok(value), Detached::Array(rows) => { @@ -70,7 +70,7 @@ pub(crate) fn attach(runtime: &mut Runtime, value: Detached) -> Result Option> { +fn symbol_name(runtime: &RoundScript, id: u32) -> Option> { let program = runtime.program.as_ref()?; program.symbols.get(id as usize).cloned().or_else(|| { runtime @@ -81,7 +81,7 @@ fn symbol_name(runtime: &Runtime, id: u32) -> Option> { }) } -fn field(runtime: &Runtime, object: u64, name: &str) -> Option { +fn field(runtime: &RoundScript, object: u64, name: &str) -> Option { let program = runtime.program.as_ref()?; let id = program .symbol_ids @@ -91,7 +91,7 @@ fn field(runtime: &Runtime, object: u64, name: &str) -> Option { } pub(crate) fn restart_level(world: &mut World, tick: crate::Tick) { - let runtime = world.resource::(); + let runtime = world.resource::(); let Some(persist) = runtime.pending_restart else { return; }; @@ -117,13 +117,7 @@ pub(crate) fn restart_level(world: &mut World, tick: crate::Tick) { Some((*client, detach(runtime, &pers, 0)?)) }) .collect(); - let dvars = runtime.dvars.clone(); - let local_presentation_dvars = runtime.local_presentation_dvars; - let local_presentation_client = runtime.local_presentation_client; - let pending_local_dvars = runtime.pending_local_dvars.clone(); - let weapon_bridge = runtime.weapon_bridge.clone(); - let personal_classes = runtime.personal_classes.clone(); - let next_presence = runtime.next_spawned_presence; + let server_info_defaults = runtime.server_info_defaults.clone(); let huds: Vec = runtime.hud_slots.keys().copied().collect(); let clients: Vec = runtime.players.keys().copied().collect(); let spawned: Vec = runtime @@ -144,7 +138,6 @@ pub(crate) fn restart_level(world: &mut World, tick: crate::Tick) { for mover in crate::frame::collect_script_movers(frame.ecs()) { if spawned.contains(&mover.id) { frame.remove_script_mover_by_number(mover.state.number); - frame.remove_collision_owner(mover.id); } } for projectile in crate::frame::collect_projectiles(frame.ecs()) { @@ -163,25 +156,18 @@ pub(crate) fn restart_level(world: &mut World, tick: crate::Tick) { } } - let match_data = std::mem::take(&mut world.resource_mut::().engine.match_data); - reset(world); - { - let mut runtime = world.resource_mut::(); - runtime.dvars = dvars; - runtime.local_presentation_dvars = local_presentation_dvars; - runtime.local_presentation_client = local_presentation_client; - runtime.pending_local_dvars = pending_local_dvars; - runtime.engine.match_data = match_data; - } + world + .resource_mut::() + .dvars + .extend(server_info_defaults.clone()); + super::super::runtime::reset_round(world); + world.resource_mut::().server_info_defaults = server_info_defaults; if let Err(fault) = install_level(world, program, (*plan).clone()) { - world.resource_mut::().fault = Some(fault); + world.resource_mut::().fault = Some(fault); return; } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.last_tick = Some(tick); - runtime.weapon_bridge = weapon_bridge; - runtime.personal_classes = personal_classes; - runtime.next_spawned_presence = next_presence; runtime.restored_pers = pers; for (name, value) in game { match attach(&mut runtime, value) { @@ -209,7 +195,7 @@ pub(crate) fn restart_level(world: &mut World, tick: crate::Tick) { Vec::new(), i64::from(crate::level_time_ms(tick)), ) { - world.resource_mut::().fault = Some(fault); + world.resource_mut::().fault = Some(fault); return; } } diff --git a/crates/sim/src/script/host/rumble.rs b/crates/sim/src/script/host/rumble.rs index d57a3f6..dd0bc29 100644 --- a/crates/sim/src/script/host/rumble.rs +++ b/crates/sim/src/script/host/rumble.rs @@ -1,14 +1,14 @@ use super::args::{string, vector}; use super::natives::engine::entity_id; use crate::frame::FrameWorld; -use crate::script::{Namespace, NativeRegistry, Runtime, Value}; +use crate::script::{Namespace, NativeRegistry, RoundScript, Value}; use bevy_ecs::prelude::World; use entity_iw4::EntityEventKind; fn alias_index(world: &World, args: &[Value]) -> Result { let name = string(args, 0)?; world - .resource::() + .resource::() .precached .get(&("rumble", name.clone())) .copied() @@ -41,7 +41,7 @@ fn entity_change( if args.len() != 1 { return Err("entity rumble requires one rumble name".into()); } - let number = world.resource::().entities[&id].number; + let number = world.resource::().entities[&id].number; let origin = match super::players::entity_field(world, id, "origin") { Value::Vector(origin) => origin, _ => [0.0; 3], diff --git a/crates/sim/src/script/host/sentry_placement/mod.rs b/crates/sim/src/script/host/sentry_placement/mod.rs index d8573a4..c1bcb80 100644 --- a/crates/sim/src/script/host/sentry_placement/mod.rs +++ b/crates/sim/src/script/host/sentry_placement/mod.rs @@ -120,7 +120,7 @@ pub(super) fn place( eye_height: f32, ) -> (bool, [f32; 3], [f32; 3]) { let model = { - let runtime = world.resource::(); + let runtime = world.resource::(); let actor = runtime.players.get(&client).map(|p| p.object); runtime.engine.turrets.iter().find_map(|(object, turret)| { (turret.carried && actor.is_some() && turret.owner == actor) diff --git a/crates/sim/src/script/host/spectators.rs b/crates/sim/src/script/host/spectators.rs index 2281b6f..8274fb1 100644 --- a/crates/sim/src/script/host/spectators.rs +++ b/crates/sim/src/script/host/spectators.rs @@ -1,7 +1,7 @@ use super::args::{int, string, vector}; use super::natives::player::player; use crate::frame::FrameWorld; -use crate::script::{NativeRegistry, Runtime, Value}; +use crate::script::{NativeRegistry, RoundScript, Value}; use crate::{ClientId, ClientLifecycle}; use bevy_ecs::prelude::World; @@ -27,7 +27,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { } let on = int(args, 1)? != 0; world - .resource_mut::() + .resource_mut::() .players .get_mut(&client) .unwrap() @@ -40,14 +40,14 @@ pub(crate) fn register(registry: &mut NativeRegistry) { let client = player(world, receiver)?; let defaults = (vector(args, 0)?, vector(args, 1)?); world - .resource_mut::() + .resource_mut::() .players .get_mut(&client) .unwrap() .spectator .defaults = Some(defaults); update(world, false); - let slot = &world.resource::().players[&client]; + let slot = &world.resource::().players[&client]; if &*slot.sessionstate == "spectator" && slot.spectator.target.is_none() && slot.seat.archive_ms == 0 @@ -60,7 +60,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { registry.register(Method, "getspectatingplayer", |world, receiver, _| { let client = player(world, receiver)?; update(world, false); - let runtime = world.resource::(); + let runtime = world.resource::(); Ok(runtime .players .get(&client) @@ -98,7 +98,8 @@ pub(crate) fn advance(world: &mut World) { } fn update(world: &mut World, input: bool) { - let mut runtime = std::mem::take(&mut *world.resource_mut::()); + let replacement = RoundScript::new(world.resource::()); + let mut runtime = std::mem::replace(&mut *world.resource_mut::(), replacement); let clients: Vec = runtime.players.keys().copied().collect(); let mut frame = FrameWorld::from_world(world); let targets: Vec<(u32, i32, bool)> = clients @@ -234,7 +235,7 @@ fn update(world: &mut World, input: bool) { } } } - *frame.ecs().resource_mut::() = runtime; + *frame.ecs().resource_mut::() = runtime; for object in notes { crate::script::runtime::raise( frame.ecs(), diff --git a/crates/sim/src/script/host/tables.rs b/crates/sim/src/script/host/tables.rs index cea2b90..9c20c3d 100644 --- a/crates/sim/src/script/host/tables.rs +++ b/crates/sim/src/script/host/tables.rs @@ -1,11 +1,11 @@ use super::args::{int, string}; use super::entities::table_key; -use crate::script::{Runtime, StringTable, Value}; +use crate::script::{RoundScript, StringTable, Value}; use bevy_ecs::prelude::World; use std::collections::BTreeMap; use std::sync::Arc; -pub(super) fn table<'a>( +pub(crate) fn table<'a>( tables: &'a BTreeMap, name: &str, ) -> Option<&'a StringTable> { @@ -13,7 +13,7 @@ pub(super) fn table<'a>( } pub(super) fn perk_slot_code(world: &World, name: &str) -> Option<(usize, u32)> { - let tables = &world.resource::().tables; + let tables = &world.resource::().tables; let table = table(tables, "mp/perkTable.csv")?; let row = table_search(table, 1, name)?; let slot = hud_iw4::get_perk_slot_index(table.cell(row, 5)?)?; @@ -21,7 +21,7 @@ pub(super) fn perk_slot_code(world: &World, name: &str) -> Option<(usize, u32)> Some((slot, code)) } -pub(super) fn table_search(table: &StringTable, column: usize, value: &str) -> Option { +pub(crate) fn table_search(table: &StringTable, column: usize, value: &str) -> Option { (0..table.rows).find(|&row| { table .cell(row, column) @@ -33,7 +33,7 @@ pub(crate) fn table_lookup(world: &World, args: &[Value]) -> Result().tables.clone(); + let tables = world.resource::().tables.clone(); let name = string(args, 0)?; let column = int(args, 1)?; let value = string(args, 2)?; @@ -51,7 +51,7 @@ pub(crate) fn table_lookup(world: &World, args: &[Value]) -> Result Result, String> { - let tables = world.resource::().tables.clone(); + let tables = world.resource::().tables.clone(); let name = string(args, 0)?; let (row, column) = (int(args, 1)?, int(args, 2)?); Ok(table(&tables, &name) diff --git a/crates/sim/src/script/host/trace.rs b/crates/sim/src/script/host/trace.rs index e2e635d..b7b5e8d 100644 --- a/crates/sim/src/script/host/trace.rs +++ b/crates/sim/src/script/host/trace.rs @@ -1,9 +1,9 @@ use std::collections::BTreeMap; use crate::EntityCollisionCapabilities; -use crate::script::{ArrayKey, Runtime, Value}; +use crate::script::{ArrayKey, RoundScript, Value}; -fn field<'a>(runtime: &'a Runtime, object: u64, name: &str) -> Option<&'a Value> { +fn field<'a>(runtime: &'a RoundScript, object: u64, name: &str) -> Option<&'a Value> { let symbol = runtime .program .as_ref()? @@ -13,7 +13,7 @@ fn field<'a>(runtime: &'a Runtime, object: u64, name: &str) -> Option<&'a Value> runtime.objects.get(&object)?.get(symbol) } -fn body_state(runtime: &Runtime, object: u64) -> Option<&BTreeMap> { +fn body_state(runtime: &RoundScript, object: u64) -> Option<&BTreeMap> { let Value::Array(parts) = field(runtime, object, "destructible_parts")? else { return None; }; @@ -34,7 +34,7 @@ fn integer(values: &BTreeMap, key: &str) -> Option { } } -pub(crate) fn entities(runtime: &Runtime, models: &[EntityCollisionCapabilities]) { +pub(crate) fn entities(runtime: &RoundScript, models: &[EntityCollisionCapabilities]) { for (object, entity) in &runtime.entities { let Some(id) = entity.presence else { continue; diff --git a/crates/sim/src/script/host/triggers.rs b/crates/sim/src/script/host/triggers.rs index af520c5..c6c1691 100644 --- a/crates/sim/src/script/host/triggers.rs +++ b/crates/sim/src/script/host/triggers.rs @@ -1,7 +1,7 @@ use crate::bullet_collision::{PLAYER_MAXS, PLAYER_MINS}; use crate::frame::FrameWorld; use crate::script::runtime::raise; -use crate::script::{Runtime, Value}; +use crate::script::{RoundScript, Value}; use bevy_ecs::prelude::World; #[derive(Clone, Debug, Default, PartialEq)] @@ -27,7 +27,7 @@ pub(crate) fn record_grenade_touch( end: [f32; 3], damage: i32, ) { - let Some(mut runtime) = world.get_resource_mut::() else { + let Some(mut runtime) = world.get_resource_mut::() else { return; }; if runtime.program.is_some() @@ -46,7 +46,7 @@ pub(crate) fn record_grenade_touch( } } -fn integer(runtime: &mut Runtime, object: u64, field: &str) -> i32 { +fn integer(runtime: &mut RoundScript, object: u64, field: &str) -> i32 { match runtime.object_field(object, field) { Value::Int(value) => value, _ => 0, @@ -82,7 +82,7 @@ fn accepts_means(flags: i32, means: &str) -> bool { } fn damage_trigger( - runtime: &mut Runtime, + runtime: &mut RoundScript, trigger: u64, hit: &TriggerHit, raised: &mut Vec, @@ -122,25 +122,25 @@ fn damage_trigger( }; if once { runtime.fired_once.insert(trigger); - runtime.pending_deletes.push(trigger); + runtime.request_delete(trigger); } } -fn active_damage_triggers(runtime: &Runtime) -> Vec { +fn active_damage_triggers(runtime: &RoundScript) -> Vec { runtime .entities .iter() .filter(|(object, entity)| { entity.classname.as_ref() == "trigger_damage" && !runtime.fired_once.contains(object) - && !runtime.pending_deletes.contains(object) + && runtime.can_receive_call(object) }) .map(|(object, _)| *object) .collect() } fn attacker( - runtime: &Runtime, + runtime: &RoundScript, client: Option, missile: Option, ) -> Value { @@ -180,12 +180,13 @@ pub(crate) fn damage_line( ) { if amount <= 0 || world - .get_resource::() + .get_resource::() .is_none_or(|runtime| runtime.program.is_none() || !runtime.started) { return; } - let mut runtime = std::mem::take(&mut *world.resource_mut::()); + let replacement = RoundScript::new(world.resource::()); + let mut runtime = std::mem::replace(&mut *world.resource_mut::(), replacement); let hit = TriggerHit { activator: attacker(&runtime, Some(client), missile), amount, @@ -204,7 +205,7 @@ pub(crate) fn damage_line( } } } - *world.resource_mut::() = runtime; + *world.resource_mut::() = runtime; for (trigger, name, args) in raised { raise(world, Value::Object(trigger), name, args); } @@ -224,7 +225,7 @@ pub(crate) struct TriggerBlast { } fn damage_bounds( - runtime: &mut Runtime, + runtime: &mut RoundScript, frame: &FrameWorld, object: u64, ) -> Option<([f32; 3], [f32; 3])> { @@ -310,13 +311,14 @@ pub(crate) fn damage_blast(world: &mut World, blast: &TriggerBlast) { .chain([blast.radius, blast.max, blast.min]) .all(f32::is_finite) || world - .get_resource::() + .get_resource::() .is_none_or(|runtime| runtime.program.is_none() || !runtime.started) { return; } super::presence::settle_collision(world); - let mut runtime = std::mem::take(&mut *world.resource_mut::()); + let replacement = RoundScript::new(world.resource::()); + let mut runtime = std::mem::replace(&mut *world.resource_mut::(), replacement); let activator = attacker(&runtime, blast.client, blast.missile); let mut raised = Vec::new(); { @@ -362,14 +364,15 @@ pub(crate) fn damage_blast(world: &mut World, blast: &TriggerBlast) { damage_trigger(&mut runtime, trigger, &hit, &mut raised); } } - *world.resource_mut::() = runtime; + *world.resource_mut::() = runtime; for (trigger, name, args) in raised { raise(world, Value::Object(trigger), name, args); } } pub(crate) fn dispatch_grenade_touches(world: &mut World) { - let mut runtime = std::mem::take(&mut *world.resource_mut::()); + let replacement = RoundScript::new(world.resource::()); + let mut runtime = std::mem::replace(&mut *world.resource_mut::(), replacement); let mut raised = Vec::new(); { let frame = FrameWorld::from_world(world); @@ -384,7 +387,7 @@ pub(crate) fn dispatch_grenade_touches(world: &mut World) { entity.classname.as_ref() == "trigger_damage" && entity.trigger_policy.grenade_touch && !runtime.fired_once.contains(object) - && !runtime.pending_deletes.contains(object) + && runtime.can_receive_call(object) }) .map(|(object, _)| *object) .collect(); @@ -405,7 +408,7 @@ pub(crate) fn dispatch_grenade_touches(world: &mut World) { } } } - *world.resource_mut::() = runtime; + *world.resource_mut::() = runtime; for (trigger, name, args) in raised { raise(world, Value::Object(trigger), name, args); } @@ -423,7 +426,7 @@ impl TriggerPolicy { } } -impl Runtime { +impl RoundScript { pub(crate) fn release_trigger_claims(&mut self, client: u32) { for entity in self.entities.values_mut() { if entity.trigger_policy.claimed_by == Some(client) { @@ -435,14 +438,14 @@ impl Runtime { } pub(crate) fn release_client_claims(world: &mut World, client: u32) { - if let Some(mut runtime) = world.get_resource_mut::() { + if let Some(mut runtime) = world.get_resource_mut::() { runtime.release_trigger_claims(client); } } pub(crate) fn refresh_claims(world: &mut World) { let claims: Vec<(u32, TriggerPolicy)> = world - .resource::() + .resource::() .entities .values() .filter_map(|entity| { @@ -453,7 +456,10 @@ pub(crate) fn refresh_claims(world: &mut World) { }) .collect(); for (client, policy) in claims { - let connected = world.resource::().players.contains_key(&client); + let connected = world + .resource::() + .players + .contains_key(&client); if !connected || !policy.allows(&FrameWorld::from_world(world), client) { release_client_claims(world, client); } @@ -470,7 +476,7 @@ fn set_grenade_touch( return Err("grenade touch damage controls take no arguments".into()); } let object = super::natives::engine::entity_id(world, receiver)?; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let entity = runtime.entities.get_mut(&object).unwrap(); if entity.classname.as_ref() != "trigger_damage" { return Err("grenade touch damage requires a damage trigger".into()); @@ -505,7 +511,7 @@ pub(crate) fn register(registry: &mut crate::script::NativeRegistry) { _ => return Err(format!("invalid trigger team '{name}'")), }; world - .resource_mut::() + .resource_mut::() .entities .get_mut(&object) .unwrap() @@ -518,7 +524,7 @@ pub(crate) fn register(registry: &mut crate::script::NativeRegistry) { let client = player(world, receiver)?; let object = entity_id(world, arg(args, 0)?)?; refresh_claims(world); - let policy = world.resource::().entities[&object] + let policy = world.resource::().entities[&object] .trigger_policy .clone(); if !policy.allows(&FrameWorld::from_world(world), client) { @@ -526,7 +532,7 @@ pub(crate) fn register(registry: &mut crate::script::NativeRegistry) { } release_client_claims(world, client); world - .resource_mut::() + .resource_mut::() .entities .get_mut(&object) .unwrap() @@ -537,7 +543,7 @@ pub(crate) fn register(registry: &mut crate::script::NativeRegistry) { registry.register(Method, "clientreleasetrigger", |world, receiver, args| { let client = player(world, receiver)?; let object = entity_id(world, arg(args, 0)?)?; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let policy = &mut runtime.entities.get_mut(&object).unwrap().trigger_policy; if policy.claimed_by == Some(client) { policy.claimed_by = None; @@ -547,7 +553,7 @@ pub(crate) fn register(registry: &mut crate::script::NativeRegistry) { registry.register(Method, "releaseclaimedtrigger", |world, receiver, _| { let object = entity_id(world, receiver)?; world - .resource_mut::() + .resource_mut::() .entities .get_mut(&object) .unwrap() @@ -557,7 +563,13 @@ pub(crate) fn register(registry: &mut crate::script::NativeRegistry) { }); } -fn eligible(runtime: &Runtime, frame: &FrameWorld, trigger: u64, client: u32, using: bool) -> bool { +fn eligible( + runtime: &RoundScript, + frame: &FrameWorld, + trigger: u64, + client: u32, + using: bool, +) -> bool { runtime.entities.get(&trigger).is_some_and(|entity| { entity.trigger_policy.allows(frame, client) && (!using @@ -587,7 +599,7 @@ fn fires(classname: &str) -> Option { } } -fn volume<'f>(runtime: &mut Runtime, frame: &'f FrameWorld, object: u64) -> Option> { +fn volume<'f>(runtime: &mut RoundScript, frame: &'f FrameWorld, object: u64) -> Option> { let entity = runtime.entities.get(&object)?; let (cylinder, brush, trigger_model) = (entity.cylinder, entity.brush, entity.trigger_model); let origin = match runtime.object_field(object, "origin") { @@ -849,7 +861,7 @@ fn looks_into(frame: &FrameWorld, client: u32, volume: &Volume<'_>) -> bool { }) } -fn toucher(runtime: &mut Runtime, frame: &FrameWorld, object: u64) -> ([f32; 3], [f32; 3]) { +fn toucher(runtime: &mut RoundScript, frame: &FrameWorld, object: u64) -> ([f32; 3], [f32; 3]) { if let Some(client) = runtime.player_client(object) && let Some(ps) = frame.player(crate::ClientId(client)) { @@ -866,7 +878,7 @@ fn toucher(runtime: &mut Runtime, frame: &FrameWorld, object: u64) -> ([f32; 3], } pub(crate) fn entity_bounds(world: &mut World, object: u64) -> ([f32; 3], [f32; 3]) { - let runtime = world.resource::(); + let runtime = world.resource::(); if runtime.player_client(object).is_some() { return (PLAYER_MINS, PLAYER_MAXS); } @@ -884,7 +896,8 @@ pub(crate) fn entity_bounds(world: &mut World, object: u64) -> ([f32; 3], [f32; } pub(crate) fn is_touching(world: &mut World, a: u64, b: u64) -> bool { - let mut runtime = std::mem::take(&mut *world.resource_mut::()); + let replacement = RoundScript::new(world.resource::()); + let mut runtime = std::mem::replace(&mut *world.resource_mut::(), replacement); let touching = { let frame = FrameWorld::from_world(world); [(a, b), (b, a)].into_iter().find_map(|(trigger, other)| { @@ -893,13 +906,14 @@ pub(crate) fn is_touching(world: &mut World, a: u64, b: u64) -> bool { Some(volume.touches(mins, maxs)) }) }; - *world.resource_mut::() = runtime; + *world.resource_mut::() = runtime; touching.unwrap_or(false) } pub(crate) fn dispatch_triggers(world: &mut World) { refresh_claims(world); - let mut runtime = std::mem::take(&mut *world.resource_mut::()); + let replacement = RoundScript::new(world.resource::()); + let mut runtime = std::mem::replace(&mut *world.resource_mut::(), replacement); let mut raised = Vec::new(); { let mut frame = FrameWorld::from_world(world); @@ -969,7 +983,7 @@ pub(crate) fn dispatch_triggers(world: &mut World) { } } } - *world.resource_mut::() = runtime; + *world.resource_mut::() = runtime; raised.sort_unstable(); raised.dedup(); for (trigger, player) in raised { diff --git a/crates/sim/src/script/host/turrets.rs b/crates/sim/src/script/host/turrets.rs index 9eea319..bbf8ffb 100644 --- a/crates/sim/src/script/host/turrets.rs +++ b/crates/sim/src/script/host/turrets.rs @@ -5,7 +5,7 @@ use crate::bullet_collision::{ColliderId, MASK_SHOT, TraceOutcome}; use crate::frame::FrameWorld; use crate::script::Namespace::{Function, Method}; use crate::script::runtime::raise; -use crate::script::{Arc, NativeRegistry, Runtime, Value}; +use crate::script::{Arc, NativeRegistry, RoundScript, Value}; use crate::world::ClientId; use bevy_ecs::prelude::World; use glam::Vec3; @@ -39,7 +39,7 @@ pub(crate) struct Turret { } fn turret_of(world: &World, receiver: &Value) -> Result { - let runtime = world.resource::(); + let runtime = world.resource::(); match runtime.entity(receiver) { Some((object, _)) if runtime.engine.turrets.contains_key(&object) => Ok(object), _ => Err("receiver is not a turret".into()), @@ -54,7 +54,7 @@ fn edit( let object = turret_of(world, receiver)?; change( world - .resource_mut::() + .resource_mut::() .engine .turrets .get_mut(&object) @@ -66,7 +66,7 @@ fn edit( fn player_object(world: &World, value: &Value) -> Result, String> { match value { Value::Undefined => Ok(None), - Value::Object(id) if world.resource::().player_client(*id).is_some() => { + Value::Object(id) if world.resource::().player_client(*id).is_some() => { Ok(Some(*id)) } _ => Err("owner is not a player".into()), @@ -91,7 +91,7 @@ pub(crate) fn muzzle(world: &mut World, object: u64) -> [f32; 3] { pub(crate) fn ignore_self(world: &World, object: u64) -> TraceIgnore { TraceIgnore { model: world - .resource::() + .resource::() .entities .get(&object) .and_then(|e| e.presence), @@ -116,7 +116,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { crate::script_player::weapon_named(&FrameWorld::from_world(world), &weaponinfo)?; let presence = super::presence::spawn_presence(world, origin)?; let birthtime = super::players::now_ms(world) as i32; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let id = runtime.create_entity(EntityKind::Spawned, &classname)?; runtime.set_object_field(id, "birthtime", Value::Int(birthtime)); runtime.set_object_field(id, "origin", Value::Vector(origin)); @@ -176,7 +176,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }); registry.register(Method, "getturrettarget", |world, receiver, _| { let object = turret_of(world, receiver)?; - let runtime = world.resource::(); + let runtime = world.resource::(); Ok(runtime.engine.turrets[&object] .target .filter(|t| runtime.live(t)) @@ -184,12 +184,12 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }); registry.register(Method, "getturretowner", |world, receiver, _| { let object = turret_of(world, receiver)?; - Ok(world.resource::().engine.turrets[&object] + Ok(world.resource::().engine.turrets[&object] .owner .map_or(Value::Undefined, Value::Object)) }); registry.register(Method, "isfiringturret", |world, receiver, _| { - let runtime = world.resource::(); + let runtime = world.resource::(); Ok(Value::Int( runtime .entity(receiver) @@ -296,7 +296,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { } fn aim_point(world: &mut World, object: u64, offset: [f32; 3]) -> Option { - if !world.resource::().live(&object) { + if !world.resource::().live(&object) { return None; } let origin = field_vector(world, object, "origin"); @@ -352,7 +352,7 @@ fn acquire(world: &mut World, object: u64, turret: &Turret, from: [f32; 3]) -> O let range = weapon_range(world, turret.weapon); let ignore = ignore_self(world, object); let candidates: Vec<(u32, u64)> = world - .resource::() + .resource::() .players .iter() .filter(|(_, slot)| &*slot.sessionstate == "playing") @@ -389,22 +389,26 @@ fn acquire(world: &mut World, object: u64, turret: &Turret, from: [f32; 3]) -> O pub(crate) fn advance(world: &mut World) { let objects: Vec = world - .resource::() + .resource::() .engine .turrets .keys() .copied() .collect(); for object in objects { - if !world.resource::().entities.contains_key(&object) { + if !world + .resource::() + .entities + .contains_key(&object) + { world - .resource_mut::() + .resource_mut::() .engine .turrets .remove(&object); continue; } - let mut turret = world.resource::().engine.turrets[&object].clone(); + let mut turret = world.resource::().engine.turrets[&object].clone(); let from = muzzle(world, object); let active = !turret.carried && &*turret.mode != "sentry_offline"; let found = if active { @@ -438,7 +442,7 @@ pub(crate) fn advance(world: &mut World) { let raise_on_target = turret.on_target && !was_on_target; let raise_state = turret.firing != was_firing; world - .resource_mut::() + .resource_mut::() .engine .turrets .insert(object, turret); @@ -457,10 +461,10 @@ pub(crate) fn advance(world: &mut World) { } fn shoot(world: &mut World, object: u64) { - let turret = world.resource::().engine.turrets[&object].clone(); + let turret = world.resource::().engine.turrets[&object].clone(); let from = muzzle(world, object); let attacker = { - let runtime = world.resource::(); + let runtime = world.resource::(); turret .owner .and_then(|o| runtime.player_client(o)) @@ -500,7 +504,7 @@ pub(crate) fn fire_bullet( _ => return, }, }; - let runtime = world.resource::(); + let runtime = world.resource::(); let number = runtime.entities[&object].number; let other_entity_num = match collider { ColliderId::Player { client, .. } => client.0 as i32, @@ -554,7 +558,7 @@ pub(crate) fn fire_bullet( flesh_flags: 0, }); } - let runtime = world.resource::(); + let runtime = world.resource::(); let target = match collider { ColliderId::Player { client, .. } => crate::script::HitTarget::Player(client), ColliderId::World { .. } => return, @@ -568,7 +572,7 @@ pub(crate) fn fire_bullet( }; let inflictor = runtime.entities.get(&object).and_then(|e| e.presence); world - .resource_mut::() + .resource_mut::() .hits .push(crate::script::ScriptHit { piece: None, diff --git a/crates/sim/src/script/host/vehicles.rs b/crates/sim/src/script/host/vehicles.rs index 491e520..cbf1a9a 100644 --- a/crates/sim/src/script/host/vehicles.rs +++ b/crates/sim/src/script/host/vehicles.rs @@ -2,7 +2,7 @@ use super::args::{arg, float, int, optional, string, vector}; use super::entities::EntityKind; use crate::script::Namespace::{Function, Method}; use crate::script::runtime::{raise, run_now}; -use crate::script::{NativeRegistry, Runtime, Value}; +use crate::script::{NativeRegistry, RoundScript, Value}; use bevy_ecs::prelude::World; pub(crate) const DAMAGE: &str = "maps/mp/gametypes/_callbacksetup::codecallback_vehicledamage"; @@ -143,7 +143,7 @@ fn heli<'a>(world: &'a mut World, receiver: &Value) -> Result<&'a mut Heli, Stri return Err("receiver is not a vehicle".into()); }; world - .resource_mut::() + .resource_mut::() .into_inner() .vehicles .get_mut(id) @@ -151,7 +151,7 @@ fn heli<'a>(world: &'a mut World, receiver: &Value) -> Result<&'a mut Heli, Stri } pub(crate) fn is_heli(world: &World, receiver: &Value) -> bool { - matches!(receiver, Value::Object(id) if world.resource::().vehicles.contains_key(id)) + matches!(receiver, Value::Object(id) if world.resource::().vehicles.contains_key(id)) } pub(crate) fn aim_turret( @@ -169,7 +169,7 @@ fn aim_point(world: &mut World, aim: TurretAim) -> Option<[f32; 3]> { match aim { TurretAim::Point(point) => Some(point), TurretAim::Entity(object, offset) => { - if !world.resource::().live(&object) { + if !world.resource::().live(&object) { return None; } match super::players::entity_field(world, object, "origin") { @@ -231,13 +231,13 @@ fn fire_weapon(world: &mut World, receiver: &Value, args: &[Value]) -> Result().players.contains_key(client)) + .filter(|client| world.resource::().players.contains_key(client)) .ok_or("vehicle owner is not connected")?; let end = std::array::from_fn(|i| from[i] + dir[i] * 1000.0); super::weapons::launch(world, crate::ClientId(owner), weapon, from, end) } -fn vec_field(runtime: &mut Runtime, object: u64, name: &str) -> [f32; 3] { +fn vec_field(runtime: &mut RoundScript, object: u64, name: &str) -> [f32; 3] { match runtime.object_field(object, name) { Value::Vector(v) => v, _ => [0.0; 3], @@ -253,7 +253,7 @@ fn spawn_vehicle( flight: Option, ) -> Result { let slot = if flight.is_some() { - let runtime = world.resource::(); + let runtime = world.resource::(); Some( (0..VEHICLE_SLOTS) .find(|slot| { @@ -269,7 +269,7 @@ fn spawn_vehicle( }; let presence = super::presence::spawn_presence(world, origin)?; let birthtime = super::players::now_ms(world) as i32; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let id = runtime.create_entity(EntityKind::Vehicle, classname)?; runtime.set_object_field(id, "birthtime", Value::Int(birthtime)); runtime.set_object_field(id, "origin", Value::Vector(origin)); @@ -298,7 +298,7 @@ fn set_goal(heli: &mut Heli, goal: [f32; 3], stop: bool) { heli.near_notified = false; } -fn node(runtime: &mut Runtime, object: u64) -> Result<[f32; 3], String> { +fn node(runtime: &mut RoundScript, object: u64) -> Result<[f32; 3], String> { if !runtime .entities .get(&object) @@ -312,7 +312,7 @@ fn node(runtime: &mut Runtime, object: u64) -> Result<[f32; 3], String> { } } -fn next_node(runtime: &mut Runtime, object: u64) -> Result, String> { +fn next_node(runtime: &mut RoundScript, object: u64) -> Result, String> { node(runtime, object)?; let target = match runtime.object_field(object, "target") { Value::Undefined => return Ok(None), @@ -339,7 +339,7 @@ fn next_node(runtime: &mut Runtime, object: u64) -> Result, String> } fn vehicle_array(world: &mut World, prefix: Option<&str>) -> Result { - let runtime = world.resource::(); + let runtime = world.resource::(); let ids = match prefix { None => { let mut vehicles: Vec<_> = runtime @@ -375,7 +375,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { vehicle_array(world, None) }); registry.register(Function, "getnumvehicles", |world, _, _| { - let runtime = world.resource::(); + let runtime = world.resource::(); Ok(Value::Int( runtime .vehicles @@ -389,7 +389,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }); registry.register(Method, "getvehicleowner", |world, receiver, _| { let object = super::natives::engine::entity_id(world, receiver)?; - let owner = match world.resource::().planes.get(&object) { + let owner = match world.resource::().planes.get(&object) { Some(plane) => Some(plane.owner), None => heli(world, receiver)?.owner, }; @@ -399,7 +399,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }); registry.register(Method, "vehicle_getvelocity", |world, receiver, _| { let object = super::natives::engine::entity_id(world, receiver)?; - let velocity = match world.resource::().planes.get(&object) { + let velocity = match world.resource::().planes.get(&object) { Some(plane) => plane.velocity, None => heli(world, receiver)?.velocity, }; @@ -426,7 +426,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { let angles = vector(args, 1)?; let object = super::natives::engine::entity_id(world, receiver)?; { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if let Some(plane) = runtime.planes.get_mut(&object) { plane.origin = origin; plane.velocity = [0.0; 3]; @@ -469,7 +469,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { registry.register(Method, "attachpath", |world, receiver, args| { heli(world, receiver)?; let object = super::natives::engine::entity_id(world, arg(args, 0)?)?; - node(&mut world.resource_mut::(), object)?; + node(&mut world.resource_mut::(), object)?; let vehicle = heli(world, receiver)?; vehicle.path_node = Some(object); vehicle.path_running = false; @@ -482,7 +482,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { Some(value) => super::natives::engine::entity_id(world, value)?, None => attached.ok_or("vehicle has no attached path")?, }; - let origin = node(&mut world.resource_mut::(), object)?; + let origin = node(&mut world.resource_mut::(), object)?; let vehicle = heli(world, receiver)?; vehicle.path_node = Some(object); vehicle.path_running = true; @@ -493,11 +493,11 @@ pub(crate) fn register(registry: &mut NativeRegistry) { let object = super::natives::engine::entity_id(world, receiver)?; let definition = string(args, 0)?; let owner = world - .resource::() + .resource::() .player_client_of(arg(args, 1)?) .ok_or("vehicle owner is not a player")?; let (origin, angles, model) = { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if !runtime.entities[&object] .classname .starts_with("script_vehicle") @@ -557,7 +557,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }), )?; if let Value::Object(object) = vehicle { - world.resource_mut::().set_object_field( + world.resource_mut::().set_object_field( object, "targetname", Value::string(&targetname), @@ -567,7 +567,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }); registry.register(Function, "spawnhelicopter", |world, _, args| { let owner = world - .resource::() + .resource::() .player_client_of(arg(args, 0)?) .ok_or("spawnHelicopter owner is not a player")?; let (origin, angles) = (vector(args, 1)?, vector(args, 2)?); @@ -594,7 +594,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }); registry.register(Function, "spawnplane", |world, _, args| { let owner = world - .resource::() + .resource::() .player_client_of(arg(args, 0)?) .ok_or("spawnPlane owner is not a player")?; let classname = string(args, 1)?; @@ -605,7 +605,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { friendly.map(|friendly| ([friendly.clone(), enemy.unwrap_or(friendly)], [32, 32])); let vehicle = spawn_vehicle(world, &classname, origin, [0.0; 3], "", None)?; if let Value::Object(object) = vehicle { - world.resource_mut::().planes.insert( + world.resource_mut::().planes.insert( object, Plane { owner, @@ -746,7 +746,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }); registry.register(Method, "vehicleturretcontrolon", |world, receiver, args| { let client = world - .resource::() + .resource::() .player_client_of(arg(args, 0)?) .ok_or("parameter 1: not a player")?; let heli = heli(world, receiver)?; @@ -759,7 +759,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { "vehicleturretcontroloff", |world, receiver, args| { world - .resource::() + .resource::() .player_client_of(arg(args, 0)?) .ok_or("parameter 1: not a player")?; let heli = heli(world, receiver)?; @@ -815,7 +815,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { }, ); registry.register(Method, "vehicle_finishdamage", |world, receiver, args| { - let Some((object, _)) = world.resource::().entity(receiver) else { + let Some((object, _)) = world.resource::().entity(receiver) else { return Err("receiver is not a vehicle".into()); }; let attacker = arg(args, 1)?.clone(); @@ -826,7 +826,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { let point = vector(args, 6)?; let dir = vector(args, 7)?; let part = optional(args, 11, string)?.unwrap_or_default(); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let before = match runtime.object_field(object, "health") { Value::Int(health) => health, Value::Float(health) => health as i32, @@ -964,7 +964,7 @@ fn body_tilt( pub(crate) fn advance(world: &mut World) { { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let ids: Vec = runtime.planes.keys().copied().collect(); for id in ids { if !runtime.entities.contains_key(&id) { @@ -978,7 +978,7 @@ pub(crate) fn advance(world: &mut World) { } } let ids: Vec = world - .resource::() + .resource::() .vehicles .keys() .copied() @@ -986,17 +986,17 @@ pub(crate) fn advance(world: &mut World) { let now = crate::level_time_ms(world.resource::().tick); for id in ids { let (gunner, weapon) = world - .resource::() + .resource::() .vehicles .get(&id) .map_or((None, None), |heli| (heli.gunner, heli.weapon)); let control = gunner - .filter(|client| world.resource::().players.contains_key(client)) + .filter(|client| world.resource::().players.contains_key(client)) .and_then(|client| gunner_input(world, client)); let fire_ms = weapon .and_then(|weapon| crate::frame::FrameWorld::from_world(world).combat_facts_for(weapon)) .map_or(100, |facts| facts.fire_time_ms.max(1)); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if !runtime.entities.contains_key(&id) { runtime.vehicles.remove(&id); continue; @@ -1177,7 +1177,7 @@ pub(crate) fn advance(world: &mut World) { } pub(crate) fn compass_rows(world: &mut World) -> Vec { - let runtime = world.resource::(); + let runtime = world.resource::(); let mut rows: Vec<_> = runtime .vehicles .iter() @@ -1192,7 +1192,7 @@ pub(crate) fn compass_rows(world: &mut World) -> Vec { rows.sort_by_key(|(id, _, _)| *id); rows.into_iter() .filter_map(|(id, owner, (icons, size))| { - if !world.resource::().live(&id) { + if !world.resource::().live(&id) { return None; } let Value::Vector(origin) = super::players::entity_field(world, id, "origin") else { @@ -1225,7 +1225,7 @@ pub(crate) fn compass_rows(world: &mut World) -> Vec { } pub(crate) fn hud_targets(world: &World) -> Vec { - let runtime = world.resource::(); + let runtime = world.resource::(); let mut rows: Vec<_> = runtime .vehicles .iter() diff --git a/crates/sim/src/script/host/weapons.rs b/crates/sim/src/script/host/weapons.rs index d9b6a2e..ecc27f4 100644 --- a/crates/sim/src/script/host/weapons.rs +++ b/crates/sim/src/script/host/weapons.rs @@ -5,7 +5,7 @@ use crate::equipment::WeaponNote; use crate::frame::FrameWorld; use crate::script::Namespace::{Function, Method}; use crate::script::runtime::raise; -use crate::script::{Arc, NativeRegistry, Runtime, Value}; +use crate::script::{Arc, NativeRegistry, RoundScript, Value}; use crate::world::ClientId; use bevy_ecs::prelude::World; pub(crate) use weapon_iw4::WEAPTYPE_PROJECTILE; @@ -40,7 +40,7 @@ fn now_ms(world: &World) -> i32 { } fn missile_of(world: &World, receiver: &Value) -> Result<(u64, ProjectileId, i32), String> { - match world.resource::().entity(receiver) { + match world.resource::().entity(receiver) { Some((object, e)) => match e.kind { EntityKind::Missile(id) => Ok((object, id, e.number)), _ => Err("receiver is not a missile".into()), @@ -63,7 +63,7 @@ fn adopt( } else { None }; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let object = runtime.create_entity(EntityKind::Missile(projectile.id), classname)?; let owner = runtime .players @@ -111,7 +111,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { return Err("GetMissileOwner requires one missile argument".into()); } let (object, _, _) = missile_of(world, &args[0])?; - let runtime = world.resource::(); + let runtime = world.resource::(); let owner = runtime.entities[&object] .missile_owner .filter(|owner| runtime.players.values().any(|slot| slot.object == *owner)); @@ -133,7 +133,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { let (start, end) = (vector(args, 1)?, vector(args, 2)?); let owner = match args.get(3) { Some(value) => world - .resource::() + .resource::() .player_client_of(value) .map(ClientId) .ok_or("MagicBullet owner is not a player")?, @@ -192,7 +192,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { let client = super::natives::player::player(world, receiver)?; super::natives::engine::entity_id(world, arg(args, 0)?)?; world - .resource::() + .resource::() .players .get(&client) .ok_or("player has disconnected")?; @@ -214,7 +214,7 @@ pub(crate) fn sync_engine_events(world: &mut World) { return; } let notes = std::mem::take(&mut FrameWorld::from_world(world).weapon_notes); - let runtime = world.resource::(); + let runtime = world.resource::(); if runtime.program.is_none() || runtime.fault.is_some() || !runtime.started { return; } @@ -252,7 +252,7 @@ pub(crate) fn sync_engine_events(world: &mut World) { fn notify_weapon_changes(world: &mut World) { let slots: Vec<(u32, u64, u32, bool)> = world - .resource::() + .resource::() .players .iter() .map(|(client, slot)| (*client, slot.object, slot.weapon, slot.switching)) @@ -279,7 +279,7 @@ fn notify_weapon_changes(world: &mut World) { let name = weapon_name(world, current); raise(world, Value::Object(object), "weapon_change", vec![name]); } - if let Some(slot) = world.resource_mut::().players.get_mut(&client) { + if let Some(slot) = world.resource_mut::().players.get_mut(&client) { slot.weapon = ps.weapon; slot.switching = switching; } @@ -291,7 +291,7 @@ pub(crate) fn publish_projectile_launches(world: &mut World) { .resource::() .reason .advances_authority_world() - && world.resource::().started + && world.resource::().started { adopt_fired(world); } @@ -299,9 +299,12 @@ pub(crate) fn publish_projectile_launches(world: &mut World) { fn adopt_fired(world: &mut World) { let now = now_ms(world); - let seen = std::mem::replace(&mut world.resource_mut::().missiles_seen_ms, now); + let seen = std::mem::replace( + &mut world.resource_mut::().missiles_seen_ms, + now, + ); let touches = world - .resource::() + .resource::() .grenade_touches .iter() .map(|touch| touch.projectile) @@ -311,7 +314,7 @@ fn adopt_fired(world: &mut World) { .into_iter() .chain(touches) .filter(|p| p.live && p.spawn_time_ms > seen) - .filter(|p| !world.resource::().missiles.contains_key(&p.id)) + .filter(|p| !world.resource::().missiles.contains_key(&p.id)) .filter(|p| adopted.insert(p.id)) .collect(); for projectile in fresh { @@ -346,7 +349,7 @@ fn adopt_fired(world: &mut World) { let model = format!("{}{}", crate::WEAPON_MODEL_PREFIX, projectile.weapon); let native = weapon_name(world, projectile.weapon); { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.set_object_field(object, "nativename", native); runtime.set_object_field(object, "weaponname", name.clone()); runtime.set_object_field(object, "weaponmodel", Value::string(&model)); @@ -357,11 +360,11 @@ fn adopt_fired(world: &mut World) { fn settle_items(world: &mut World) { let items: Vec<(u64, i32, Arc)> = { - let runtime = world.resource::(); + let runtime = world.resource::(); runtime .entities .iter() - .filter(|(id, _)| !runtime.pending_deletes.contains(id)) + .filter(|(id, _)| runtime.script_is_defined(id)) .filter_map(|(id, e)| match e.kind { EntityKind::Item(number) => Some((*id, number, e.classname.clone())), _ => None, @@ -396,11 +399,9 @@ fn settle_items(world: &mut World) { } // Retire only after every pickup is raised: a death between two triggers on one // item ends the script thread still waiting on it. - for (object, number, _) in items { - if FrameWorld::from_world(world) - .dropped_item_by_number(number) - .is_none() - { + let retired = world.resource_mut::().take_retired_items(); + for object in retired { + if world.resource::().script_is_defined(&object) { retire(world, object); } } @@ -408,25 +409,25 @@ fn settle_items(world: &mut World) { fn retire(world: &mut World, object: u64) { raise(world, Value::Object(object), "death", Vec::new()); - world.resource_mut::().pending_deletes.push(object); + world.resource_mut::().request_delete(object); } fn settle_projectiles(world: &mut World, notes: &[WeaponNote]) { let now = now_ms(world); let tracked: Vec<(ProjectileId, u64)> = world - .resource::() + .resource::() .missiles .iter() .map(|(id, object)| (*id, *object)) .collect(); for (id, object) in tracked { let Some(number) = world - .resource::() + .resource::() .entities .get(&object) .map(|e| e.number) else { - world.resource_mut::().missiles.remove(&id); + world.resource_mut::().missiles.remove(&id); if let Some(number) = crate::frame::collect_projectiles(world) .iter() .find(|p| p.id == id) @@ -449,7 +450,7 @@ fn settle_projectiles(world: &mut World, notes: &[WeaponNote]) { let receiver = Value::Object(object); match (flying, detonated) { (Some(projectile), None) => { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.set_object_field( object, "origin", @@ -466,7 +467,7 @@ fn settle_projectiles(world: &mut World, notes: &[WeaponNote]) { } } _ => { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.missiles.remove(&id); if let Some(origin) = detonated { runtime.set_object_field(object, "origin", Value::Vector(origin)); @@ -488,18 +489,18 @@ fn settle_projectiles(world: &mut World, notes: &[WeaponNote]) { } if lingers { world - .resource_mut::() + .resource_mut::() .lingering .push((i64::from(now) + GRENADE_LINGER_MS, object)); } else { raise(world, receiver, "death", Vec::new()); - world.resource_mut::().pending_deletes.push(object); + world.resource_mut::().request_delete(object); } } } } let due: Vec = { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let (due, kept): (Vec<_>, Vec<_>) = std::mem::take(&mut runtime.lingering) .into_iter() .partition(|(at, _)| *at <= i64::from(now)); @@ -507,9 +508,13 @@ fn settle_projectiles(world: &mut World, notes: &[WeaponNote]) { due.into_iter().map(|(_, object)| object).collect() }; for object in due { - if world.resource::().entities.contains_key(&object) { + if world + .resource::() + .entities + .contains_key(&object) + { raise(world, Value::Object(object), "death", Vec::new()); - world.resource_mut::().pending_deletes.push(object); + world.resource_mut::().request_delete(object); } } } diff --git a/crates/sim/src/script/mod.rs b/crates/sim/src/script/mod.rs index 466b1bf..3e69565 100644 --- a/crates/sim/src/script/mod.rs +++ b/crates/sim/src/script/mod.rs @@ -13,7 +13,7 @@ pub(crate) use bevy_ecs::prelude::Resource; pub(crate) use std::collections::{BTreeMap, VecDeque}; pub(crate) use std::sync::Arc; -pub(crate) use runtime::Runtime; +pub(crate) use runtime::{MatchScript, RoundScript}; pub use error::{Fault, Location}; pub(crate) use host::controls::{ @@ -27,7 +27,7 @@ pub(crate) use host::entity_damage::{ }; pub(crate) use host::mechanics::Mechanics; pub(crate) use host::mechanics::advance_mechanics; -pub use host::natives::engine::{EXIT_LEVEL, GlassNames, MAP_RESTART}; +pub use host::natives::engine::{EXIT_LEVEL, MAP_RESTART}; pub(crate) use host::natives::iw4::set_dvar; pub(crate) use host::players::{ answer_join, answer_menu, apply_disconnects, apply_player_links, choose_class, diff --git a/crates/sim/src/script/runtime/lifecycle.rs b/crates/sim/src/script/runtime/lifecycle.rs index 97cd3c6..40ecd9f 100644 --- a/crates/sim/src/script/runtime/lifecycle.rs +++ b/crates/sim/src/script/runtime/lifecycle.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use crate::script::host; use crate::script::{ - Fault, LevelData, Location, NativeRegistry, Program, Runtime, Thread, ThreadState, Value, + Fault, LevelData, Location, NativeRegistry, Program, RoundScript, Thread, ThreadState, Value, }; use super::{INSTRUCTION_BUDGET, execute, new_thread}; @@ -14,7 +14,8 @@ use super::{INSTRUCTION_BUDGET, execute, new_thread}; pub const STRUCT_INIT: &str = "codescripts/struct::initstructs"; pub(crate) fn copy_state(source: &World, target: &mut World) { - target.insert_resource(source.resource::().clone()); + target.insert_resource(source.resource::().clone()); + target.insert_resource(source.resource::().clone()); target.insert_resource(source.resource::().clone()); target.insert_resource(source.resource::().clone()); for entity in source.iter_entities() { @@ -24,7 +25,7 @@ pub(crate) fn copy_state(source: &World, target: &mut World) { } } -pub(crate) fn reset(world: &mut World) { +pub(crate) fn reset_round(world: &mut World) { let ids: Vec<_> = world .query_filtered::>() .iter(world) @@ -32,10 +33,16 @@ pub(crate) fn reset(world: &mut World) { for id in ids { world.despawn(id); } - world.insert_resource(Runtime::default()); + let round = RoundScript::new(world.resource::()); + world.insert_resource(round); world.insert_resource(host::mechanics::Mechanics::default()); } +pub(crate) fn reset(world: &mut World) { + world.insert_resource(crate::script::MatchScript::default()); + reset_round(world); +} + pub(crate) fn install( world: &mut World, program: Program, @@ -67,7 +74,7 @@ pub(crate) fn install_level( line: 0, column: 0, }; - if world.resource::().program.is_some() { + if world.resource::().program.is_some() { return Err(Fault::at( &location, "program already installed; reset the match before replacing scripts", @@ -98,7 +105,7 @@ pub(crate) fn install_level( .iter() .map(|b| natives.get(b.namespace, b.name).unwrap()) .collect(); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.program = Some(program.clone()); runtime.natives = bound; runtime.objects.insert(0, BTreeMap::new()); @@ -113,10 +120,10 @@ pub(crate) fn install_level( if let Some(&function) = program.names.get(STRUCT_INIT) { let mut thread = new_thread(world, &program, function, Value::Object(0), Vec::new()) .map_err(|m| Fault::at(&location, m))?; - world.resource_mut::().budget = INSTRUCTION_BUDGET; + world.resource_mut::().budget = INSTRUCTION_BUDGET; thread.state = ThreadState::Runnable; execute(world, &program, &mut thread, 0); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if let Some(fault) = runtime.fault.clone() { return Err(fault); } @@ -127,9 +134,9 @@ pub(crate) fn install_level( } let entities = plan.entities.clone(); let keys = plan.keys.clone(); - world.resource_mut::().restart = Some(Arc::new(plan)); + world.resource_mut::().restart = Some(Arc::new(plan)); world - .resource_mut::() + .resource_mut::() .spawn_map_entities(&entities, &keys) .map_err(|m| Fault::at(&location, m))?; host::presence::initialize_map_models(world); diff --git a/crates/sim/src/script/runtime/mod.rs b/crates/sim/src/script/runtime/mod.rs index 74c868e..2a20ed0 100644 --- a/crates/sim/src/script/runtime/mod.rs +++ b/crates/sim/src/script/runtime/mod.rs @@ -10,19 +10,19 @@ use bevy_ecs::prelude::{Resource, World}; use std::collections::BTreeMap; use std::sync::Arc; -pub(crate) use lifecycle::{copy_state, install, install_level, reset}; -pub(crate) use state::Runtime; +pub(crate) use lifecycle::{copy_state, install, install_level, reset, reset_round}; +pub(crate) use state::{MatchScript, RoundScript}; /// Instructions one resumed thread may run before it is killed as a runaway loop. /// Level startup runs in one frame and needs over a million on the larger maps. pub(super) const INSTRUCTION_BUDGET: usize = 16 * 1_000_000; pub(crate) fn take_signals(world: &mut World) -> Vec> { - std::mem::take(&mut world.resource_mut::().signals) + std::mem::take(&mut world.resource_mut::().signals) } pub(super) fn level_endon_armed(world: &World, name: &str) -> bool { - world.resource::().waiters.iter().any(|w| { + world.resource::().waiters.iter().any(|w| { w.receiver == Value::level() && &*w.name == name && matches!(w.kind, WaiterKind::Endon { .. }) @@ -31,7 +31,7 @@ pub(super) fn level_endon_armed(world: &World, name: &str) -> bool { pub(super) fn return_from(world: &mut World, function: &str, now: i64) -> usize { let Some(&function) = world - .resource::() + .resource::() .program .as_ref() .and_then(|program| program.names.get(function)) @@ -56,14 +56,14 @@ pub(super) fn return_from(world: &mut World, function: &str, now: i64) -> usize pub(super) fn raise(world: &mut World, receiver: Value, name: &str, args: Vec) { world - .resource_mut::() + .resource_mut::() .pending_notifies .push((receiver, name.into(), args)); } fn deliver_pending(world: &mut World, thread: &mut Thread, now: i64) -> Result<(), String> { loop { - let pending = std::mem::take(&mut world.resource_mut::().pending_notifies); + let pending = std::mem::take(&mut world.resource_mut::().pending_notifies); if pending.is_empty() { return Ok(()); } @@ -80,12 +80,12 @@ fn deliver_external(world: &mut World, now: i64) { stack: Vec::new(), state: ThreadState::Complete, }; - let pending = std::mem::take(&mut world.resource_mut::().pending_notifies); - let program = world.resource::().program.clone().unwrap(); + let pending = std::mem::take(&mut world.resource_mut::().pending_notifies); + let program = world.resource::().program.clone().unwrap(); for (receiver, name, args) in pending { let result = notify(world, &mut carrier, &receiver, &name, &args, now); if let Err(message) = result { - world.resource_mut::().fault = Some(Fault::at( + world.resource_mut::().fault = Some(Fault::at( &Location { module: "".into(), function: "notify".into(), @@ -98,7 +98,7 @@ fn deliver_external(world: &mut World, now: i64) { } // A touch handler can wait again before the next toucher is notified. run_ready(world, &program, now); - if world.resource::().fault.is_some() { + if world.resource::().fault.is_some() { break; } } @@ -132,7 +132,7 @@ fn entry(world: &World, name: &str) -> Result<(Arc, usize, Location), F line: 0, column: 0, }; - let runtime = world.resource::(); + let runtime = world.resource::(); if let Some(fault) = &runtime.fault { return Err(fault.clone()); } @@ -155,7 +155,7 @@ pub(crate) fn start( args: Vec, ) -> Result { let (program, function, location) = entry(world, name)?; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if runtime.last_tick.is_none() && receiver == Value::level() && args.is_empty() @@ -178,17 +178,17 @@ pub(super) fn run_now( .map_err(|m| Fault::at(&location, m))?; thread.state = ThreadState::Runnable; let budget = std::mem::replace( - &mut world.resource_mut::().budget, + &mut world.resource_mut::().budget, INSTRUCTION_BUDGET, ); execute(world, &program, &mut thread, now); - world.resource_mut::().budget = budget; + world.resource_mut::().budget = budget; if thread.state == ThreadState::Complete { - retire(&mut world.resource_mut::(), thread.serial); + retire(&mut world.resource_mut::(), thread.serial); } else { world.spawn(thread); } - match world.resource::().fault.clone() { + match world.resource::().fault.clone() { Some(fault) => Err(fault), None => Ok(()), } @@ -203,7 +203,7 @@ fn spawn_thread( ) -> Result { let thread = new_thread(world, program, function, receiver, args)?; let serial = thread.serial; - world.resource_mut::().spawned.push(serial); + world.resource_mut::().spawned.push(serial); world.spawn(thread); Ok(serial) } @@ -219,7 +219,7 @@ pub(super) fn new_thread( .into_iter() .map(|value| copy_value(world, value)) .collect::, _>>()?; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.started = true; let serial = runtime.next_serial; runtime.next_serial = serial.checked_add(1).ok_or("thread identifier exhausted")?; @@ -234,7 +234,7 @@ pub(super) fn new_thread( const MAX_FRAMES: usize = 31; fn frame_room(world: &World, thread: &Thread) -> Result<(), String> { - if world.resource::().suspended_frames + thread.frames.len() >= MAX_FRAMES { + if world.resource::().suspended_frames + thread.frames.len() >= MAX_FRAMES { return Err("script stack overflow (too many embedded function calls)".into()); } Ok(()) @@ -242,23 +242,23 @@ fn frame_room(world: &World, thread: &Thread) -> Result<(), String> { fn settle_owed_deaths(world: &mut World, parent: &mut Thread, now: i64) -> Result<(), String> { { - let runtime = world.resource::(); + let runtime = world.resource::(); if runtime.deaths.is_empty() || runtime.current_hit.is_some() { return Ok(()); } } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.suspended.push(parent.serial); runtime.suspended_frames += parent.frames.len(); super::host::players::settle_deaths(world); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.suspended.pop(); runtime.suspended_frames -= parent.frames.len(); resume_suspended(world, parent, now) } fn resume_suspended(world: &mut World, parent: &mut Thread, now: i64) -> Result<(), String> { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if runtime.fault.is_some() { return Err(String::new()); } @@ -284,12 +284,12 @@ fn run_inline( mut child: Thread, now: i64, ) -> Result<(), String> { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.suspended.push(parent.serial); runtime.suspended_frames += parent.frames.len(); child.state = ThreadState::Runnable; execute(world, program, &mut child, now); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.suspended.pop(); runtime.suspended_frames -= parent.frames.len(); if child.state == ThreadState::Complete { @@ -542,7 +542,7 @@ pub(super) fn binary(op: Binary, a: Value, b: Value) -> Result { } } -fn object_key(runtime: &mut Runtime, key: ArrayKey) -> Result { +fn object_key(runtime: &mut RoundScript, key: ArrayKey) -> Result { match key { ArrayKey::String(key) => Ok(runtime.symbol(&key.symbol_key())), ArrayKey::Integer(_) => Err("object index must be a string".into()), @@ -600,7 +600,7 @@ fn instruction( return Err("foreach requires an array".into()); }; let keys: Vec<_> = world - .resource::() + .resource::() .arrays .get(&id) .ok_or("invalid array reference")? @@ -614,7 +614,7 @@ fn instruction( let Value::Array(id) = value else { unreachable!() }; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let array = runtime.arrays.get_mut(&id).unwrap(); for (i, key) in keys.into_iter().enumerate() { array.insert(ArrayKey::Integer(i as i32), key); @@ -633,7 +633,7 @@ fn instruction( return Err("array field requires an object".into()); }; let value = world - .resource::() + .resource::() .objects .get(&id) .ok_or("invalid object reference")? @@ -643,7 +643,7 @@ fn instruction( let value = if value == Value::Undefined { let value = allocate_array(world)?; world - .resource_mut::() + .resource_mut::() .objects .get_mut(&id) .unwrap() @@ -657,7 +657,7 @@ fn instruction( Op::EnsureIndexArray => { let key = array_key(pop(thread)?)?; let receiver = pop(thread)?; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let (value, field) = match &receiver { Value::Array(id) => ( runtime @@ -686,7 +686,7 @@ fn instruction( Some(value) if value != Value::Undefined => value, _ => { let value = allocate_array(world)?; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); match receiver { Value::Array(id) => { runtime @@ -715,7 +715,7 @@ fn instruction( Op::LoadIndex => { let key = array_key(pop(thread)?)?; let receiver = pop(thread)?; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let value = match receiver { Value::Array(id) => runtime .arrays @@ -757,7 +757,7 @@ fn instruction( let value = copy_value(world, pop(thread)?)?; let key = array_key(pop(thread)?)?; let receiver = pop(thread)?; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); match receiver { Value::Array(id) => { let values = runtime @@ -863,7 +863,7 @@ fn instruction( return Err("threading native calls is unsupported".into()); } Callee::Native(index) => { - let native = world.resource::().natives[index as usize]; + let native = world.resource::().natives[index as usize]; let name = &program.natives[index as usize].name; // A defect in one builtin costs its caller an undefined result, not the match. let value = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { @@ -881,7 +881,7 @@ fn instruction( Op::FrameEnd => { thread.state = ThreadState::Queued; world - .resource_mut::() + .resource_mut::() .buckets .entry(now) .or_default() @@ -909,7 +909,7 @@ fn instruction( let until = now + i64::from(frames) * i64::from(crate::MATCH_TICK_MS); thread.state = ThreadState::Queued; world - .resource_mut::() + .resource_mut::() .buckets .entry(until) .or_default() @@ -919,7 +919,7 @@ fn instruction( let receiver = pop(thread)?; let size = match &receiver { Value::Array(id) => world - .resource::() + .resource::() .arrays .get(id) .ok_or("invalid array reference")? @@ -938,7 +938,7 @@ fn instruction( type_name(&receiver) )); }; - if let Some(client) = world.resource::().player_client(id) + if let Some(client) = world.resource::().player_client(id) && let Some(value) = super::host::players::load_field( world, client, @@ -956,7 +956,7 @@ fn instruction( thread.stack.push(value); return Ok(()); } - let runtime = world.resource::(); + let runtime = world.resource::(); let fields = runtime .objects .get(&id) @@ -971,7 +971,7 @@ fn instruction( let Value::Object(id) = receiver else { return Err("native entity fields are not bound".into()); }; - if let Some(client) = world.resource::().player_client(id) + if let Some(client) = world.resource::().player_client(id) && super::host::players::store_field( world, client, @@ -990,7 +990,7 @@ fn instruction( return Ok(()); } super::host::hud::store_field(world, id, &program.symbols[field as usize], &value)?; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let fields = runtime .objects .get_mut(&id) @@ -1038,7 +1038,7 @@ fn instruction( let name = event_name(pop(thread)?)?; let receiver = event_receiver(pop(thread)?)?; let frame = thread.frames.len() - 1; - world.resource_mut::().waiters.push(Waiter { + world.resource_mut::().waiters.push(Waiter { receiver, name, thread: thread.serial, @@ -1049,7 +1049,7 @@ fn instruction( let value = pop(thread)?; let depth = thread.frames.len() - 1; let serial = thread.serial; - world.resource_mut::().waiters.retain(|w| { + world.resource_mut::().waiters.retain(|w| { w.thread != serial || !matches!(w.kind, WaiterKind::Endon { frame } if frame >= depth) }); @@ -1092,7 +1092,7 @@ fn register( kind: WaiterKind, ) { thread.state = ThreadState::Awaiting; - world.resource_mut::().waiters.push(Waiter { + world.resource_mut::().waiters.push(Waiter { receiver, name, thread: thread.serial, @@ -1145,12 +1145,12 @@ fn with_thread( Some(result) } -fn retire(runtime: &mut Runtime, serial: u64) { +fn retire(runtime: &mut RoundScript, serial: u64) { runtime.waiters.retain(|w| w.thread != serial); dequeue(runtime, serial); } -fn dequeue(runtime: &mut Runtime, serial: u64) { +fn dequeue(runtime: &mut RoundScript, serial: u64) { for bucket in runtime.buckets.values_mut() { bucket.retain(|s| *s != serial); } @@ -1160,7 +1160,7 @@ fn dequeue(runtime: &mut Runtime, serial: u64) { fn resume_now(world: &mut World, thread: &mut Thread, now: i64) { thread.state = ThreadState::Queued; world - .resource_mut::() + .resource_mut::() .buckets .entry(now) .or_default() @@ -1169,7 +1169,7 @@ fn resume_now(world: &mut World, thread: &mut Thread, now: i64) { fn unwind(world: &mut World, thread: &mut Thread, depth: usize, now: i64, running: bool) { let serial = thread.serial; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); runtime.waiters.retain(|w| { w.thread != serial || matches!(w.kind, WaiterKind::Endon { frame } if frame < depth) }); @@ -1208,10 +1208,13 @@ fn notify( now: i64, ) -> Result<(), String> { if *receiver == Value::Object(0) { - world.resource_mut::().signals.push(name.clone()); + world + .resource_mut::() + .signals + .push(name.clone()); } loop { - let runtime = world.resource::(); + let runtime = world.resource::(); let Some(index) = runtime.waiters.iter().position(|w| { &w.receiver == receiver && &w.name == name @@ -1222,10 +1225,10 @@ fn notify( }) else { return Ok(()); }; - let waiter = world.resource_mut::().waiters.remove(index); + let waiter = world.resource_mut::().waiters.remove(index); let result = match waiter.kind { WaiterKind::Endon { frame } => { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); if runtime.suspended.contains(&waiter.thread) { runtime.pending_unwinds.push((waiter.thread, frame)); runtime.waiters.retain(|w| { @@ -1265,7 +1268,7 @@ fn notify( fn kill(world: &mut World, entity: Entity, serial: u64) { world.despawn(entity); - retire(&mut world.resource_mut::(), serial); + retire(&mut world.resource_mut::(), serial); } const HEAP_COLLECT_TICKS: u32 = 10; @@ -1276,7 +1279,7 @@ pub(crate) fn advance_scheduler(world: &mut World) { return; } let tick = request.tick; - let runtime = world.resource::(); + let runtime = world.resource::(); if runtime.fault.is_some() { return; } @@ -1287,7 +1290,7 @@ pub(crate) fn advance_scheduler(world: &mut World) { .last_tick .is_some_and(|previous| previous.0 >= tick.0) { - world.resource_mut::().fault = Some(Fault::at( + world.resource_mut::().fault = Some(Fault::at( &Location { module: "".into(), function: String::new(), @@ -1298,7 +1301,7 @@ pub(crate) fn advance_scheduler(world: &mut World) { )); return; } - world.resource_mut::().last_tick = Some(tick); + world.resource_mut::().last_tick = Some(tick); let now = i64::from(tick.0) * i64::from(crate::MATCH_TICK_MS); super::host::mechanics::deliver_finished(world); deliver_external(world, now); @@ -1317,7 +1320,7 @@ pub(crate) fn advance_scheduler(world: &mut World) { kill(world, entity, serial); } { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let due: Vec<_> = runtime.buckets.range(..=now).map(|(k, _)| *k).collect(); let mut current = VecDeque::new(); for key in due { @@ -1329,11 +1332,30 @@ pub(crate) fn advance_scheduler(world: &mut World) { runtime.buckets.insert(now, current); } run_ready(world, &program, now); - let deletes = std::mem::take(&mut world.resource_mut::().pending_deletes); + let deletes = world.resource_mut::().take_finishing(); for object in deletes { - world.resource_mut::().delete_entity(object); + // Map objects and HUD elements use script-local numbers. Model presence + // is retired by its stable identity, never by those synthetic numbers. + let missile = world + .resource::() + .entities + .get(&object) + .and_then(|entity| match entity.kind { + super::host::entities::EntityKind::Missile(id) => Some((id, entity.number)), + _ => None, + }); + if let Some((id, number)) = missile { + let mut frame = crate::frame::FrameWorld::from_world(world); + if frame + .projectile_by_number(number) + .is_some_and(|projectile| projectile.id == id) + { + frame.despawn_projectile(number); + } + } + world.resource_mut::().delete_entity(object); } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); for id in std::mem::take(&mut runtime.dying) { if let Some(fields) = runtime.objects.get_mut(&id) { fields.clear(); @@ -1351,7 +1373,7 @@ pub(crate) fn advance_scheduler(world: &mut World) { fn run_ready(world: &mut World, program: &Program, now: i64) { loop { let next = world - .resource_mut::() + .resource_mut::() .buckets .get_mut(&now) .and_then(VecDeque::pop_front); @@ -1368,14 +1390,14 @@ fn run_ready(world: &mut World, program: &Program, now: i64) { } let mut thread = world.entity_mut(entity).take::().unwrap(); thread.state = ThreadState::Runnable; - world.resource_mut::().budget = INSTRUCTION_BUDGET; + world.resource_mut::().budget = INSTRUCTION_BUDGET; execute(world, program, &mut thread, now); if thread.state == ThreadState::Complete { kill(world, entity, serial); } else { world.entity_mut(entity).insert(thread); } - if world.resource::().fault.is_some() { + if world.resource::().fault.is_some() { break; } } @@ -1384,13 +1406,13 @@ fn run_ready(world: &mut World, program: &Program, now: i64) { pub(super) fn execute(world: &mut World, program: &Program, thread: &mut Thread, now: i64) { // The runaway-loop budget lives here between instructions; the runtime's // copy is only current around instructions that can run other threads. - let mut budget = world.resource::().budget; + let mut budget = world.resource::().budget; while thread.state == ThreadState::Runnable { let frame = thread.frames.last().unwrap(); let function = &program.functions[frame.function]; let (at_function, at_pc) = (frame.function, frame.pc); let Some(op) = function.code.get(frame.pc).map(|(_, op)| op.clone()) else { - world.resource_mut::().fault = Some(Fault::at( + world.resource_mut::().fault = Some(Fault::at( &function.location, "invalid IR: instruction position out of range", )); @@ -1410,11 +1432,11 @@ pub(super) fn execute(world: &mut World, program: &Program, thread: &mut Thread, Op::Call(..) | Op::Spawn(..) | Op::Indirect(..) | Op::Notify(_) ); if nested { - world.resource_mut::().budget = budget; + world.resource_mut::().budget = budget; } let result = instruction(world, program, thread, op, now); if nested { - budget = world.resource::().budget; + budget = world.resource::().budget; } result }; @@ -1422,7 +1444,7 @@ pub(super) fn execute(world: &mut World, program: &Program, thread: &mut Thread, continue; }; let location = &program.functions[at_function].code[at_pc].0; - if world.resource::().fault.is_some() { + if world.resource::().fault.is_some() { break; } let mut fault = Fault::at(location, message); @@ -1439,13 +1461,13 @@ pub(super) fn execute(world: &mut World, program: &Program, thread: &mut Thread, }) .collect(); if terminal(&fault.message) { - world.resource_mut::().fault = Some(fault); + world.resource_mut::().fault = Some(fault); break; } report(world, &fault); if exhausted { let serial = thread.serial; - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); budget = INSTRUCTION_BUDGET; runtime.waiters.retain(|w| w.thread != serial); thread.frames.clear(); @@ -1457,7 +1479,7 @@ pub(super) fn execute(world: &mut World, program: &Program, thread: &mut Thread, // its results and the thread carries on. let base = thread.frames.last().map_or(0, |f| f.stack_base); let Some(kept) = before.checked_sub(pops).filter(|kept| *kept >= base) else { - world.resource_mut::().fault = + world.resource_mut::().fault = Some(Fault::at(location, "invalid IR: stack underflow")); break; }; @@ -1469,13 +1491,13 @@ pub(super) fn execute(world: &mut World, program: &Program, thread: &mut Thread, match allocate_array(world) { Ok(empty) => *thread.stack.last_mut().unwrap() = empty, Err(message) => { - world.resource_mut::().fault = Some(Fault::at(location, message)); + world.resource_mut::().fault = Some(Fault::at(location, message)); break; } } } } - world.resource_mut::().budget = budget; + world.resource_mut::().budget = budget; } fn stack_effect(op: &Op) -> (usize, usize) { @@ -1510,7 +1532,7 @@ fn report(world: &mut World, fault: &Fault) { "{}:{}:{}", fault.location.module, fault.location.line, fault.location.column ); - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let hits = runtime .errors .entry((site.clone(), fault.message.clone())) @@ -1529,7 +1551,7 @@ fn report(world: &mut World, fault: &Fault) { } } -pub(crate) fn healthy(runtime: bevy_ecs::prelude::Res) -> bool { +pub(crate) fn healthy(runtime: bevy_ecs::prelude::Res) -> bool { runtime.fault.is_none() } @@ -1538,7 +1560,7 @@ pub(crate) fn preflight( tick: crate::Tick, reason: crate::StepReason, ) -> Result<(), Fault> { - let runtime = world.resource::(); + let runtime = world.resource::(); if let Some(fault) = &runtime.fault { return Err(fault.clone()); } @@ -1579,7 +1601,7 @@ fn array_key(value: Value) -> Result { } fn allocate_array(world: &mut World) -> Result { - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let id = runtime.next_object; runtime.next_object = id.checked_add(1).ok_or("object identifier exhausted")?; runtime.arrays.insert(id, BTreeMap::new()); @@ -1601,7 +1623,7 @@ fn copy_value(world: &mut World, value: Value) -> Result { } *remaining -= 1; let entries = world - .resource::() + .resource::() .arrays .get(&id) .ok_or("invalid array reference")? @@ -1617,7 +1639,7 @@ fn copy_value(world: &mut World, value: Value) -> Result { for (key, value) in entries { let value = copy(world, value, depth + 1, remaining)?; world - .resource_mut::() + .resource_mut::() .arrays .get_mut(&new_id) .unwrap() @@ -1631,7 +1653,7 @@ fn copy_value(world: &mut World, value: Value) -> Result { /// Deleting a waited-on object ends the thread; a thread whose self is deleted keeps running. fn entity_receivers(world: &World, thread: &Thread) -> Vec { world - .resource::() + .resource::() .waiters .iter() .filter(|w| w.thread == thread.serial) @@ -1642,7 +1664,7 @@ fn entity_receivers(world: &World, thread: &Thread) -> Vec { } fn threads_waiting_on_deleted(world: &World) -> std::collections::HashSet { - let runtime = world.resource::(); + let runtime = world.resource::(); runtime .waiters .iter() @@ -1654,13 +1676,13 @@ fn threads_waiting_on_deleted(world: &World) -> std::collections::HashSet { } fn any_deleted(world: &World, receivers: &[Value]) -> bool { - let runtime = world.resource::(); + let runtime = world.resource::(); receivers.iter().any(|value| { matches!(value, Value::Object(id) if runtime.dead.contains(id) || !runtime.objects.contains_key(id)) }) } -impl Runtime { +impl RoundScript { /// Values held outside script objects by the engine side: they keep what /// they reference alive exactly like a script variable would. fn native_roots(&self, pending: &mut Vec) { @@ -1715,7 +1737,7 @@ fn collect_heap(world: &mut World) { for id in [0, 1, 2] { marks.reach(&Value::Object(id)); } - for id in world.resource::().entities.keys() { + for id in world.resource::().entities.keys() { marks.reach(&Value::Object(*id)); } for thread in world.query::<&Thread>().iter(world) { @@ -1725,7 +1747,7 @@ fn collect_heap(world: &mut World) { frame.locals.iter().for_each(|value| marks.reach(value)); } } - let mut runtime = world.resource_mut::(); + let mut runtime = world.resource_mut::(); let mut native = Vec::new(); runtime.native_roots(&mut native); native.iter().for_each(|value| marks.reach(value)); diff --git a/crates/sim/src/script/runtime/state.rs b/crates/sim/src/script/runtime/state.rs index a985c27..09500da 100644 --- a/crates/sim/src/script/runtime/state.rs +++ b/crates/sim/src/script/runtime/state.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use crate::script::host; use crate::script::{ArrayKey, Fault, Native, Program, StringTable, Value}; -#[derive(Resource, Clone, Debug, Default)] -pub(crate) struct Runtime { +#[derive(Resource, Clone, Debug)] +pub(crate) struct RoundScript { pub(crate) program: Option>, pub(crate) natives: Vec, pub(crate) next_serial: u64, @@ -26,12 +26,7 @@ pub(crate) struct Runtime { pub(crate) buckets: BTreeMap>, pub(crate) spawned: Vec, pub(crate) waiters: Vec, - pub(crate) dvars: BTreeMap, - pub(crate) local_presentation_dvars: bool, - pub(crate) local_presentation_client: Option, - pub(crate) pending_local_dvars: Vec<(crate::TargetBoxDvar, String)>, pub(crate) loading: bool, - pub(crate) precached: BTreeMap<(&'static str, String), i32>, pub(crate) presented: BTreeMap<&'static str, Vec>, pub(crate) unsupported: BTreeMap<&'static str, u64>, pub(crate) budget: usize, @@ -49,9 +44,9 @@ pub(crate) struct Runtime { pub(crate) engine: host::entities::EngineState, pub(crate) players: BTreeMap, pub(crate) menu_answers: BTreeMap>, + pub(crate) team_ranks: BTreeMap, + pub(crate) ranks_sent: bool, pub(crate) selected_classes: BTreeMap, - pub(crate) personal_classes: BTreeMap<(u32, u32), crate::ClassDef>, - pub(crate) weapon_bridge: BTreeMap>, pub(crate) thrown_insertions: BTreeMap, u64)>, pub(crate) insertion_spots: Vec<([f32; 3], Arc)>, pub(crate) disconnects: std::collections::BTreeSet, @@ -62,18 +57,20 @@ pub(crate) struct Runtime { pub(crate) exit_level: bool, pub(crate) shown: BTreeMap, pub(crate) retired_presence: Vec<(crate::ScriptModelId, bool)>, - pub(crate) next_spawned_presence: u32, pub(crate) blasts: Vec, pub(crate) hits: Vec, pub(crate) use_held: std::collections::BTreeSet, pub(crate) fired_once: std::collections::BTreeSet, pub(crate) require_look_at: std::collections::BTreeSet, pub(crate) server_info: std::collections::BTreeSet, + pub(crate) server_info_defaults: BTreeMap, pub(crate) missiles: BTreeMap, pub(crate) missiles_seen_ms: i32, pub(crate) grenade_touches: Vec, pub(crate) lingering: Vec<(i64, u64)>, - pub(crate) pending_deletes: Vec, + retired_items: Vec, + finishing: std::collections::BTreeSet, + finishing_order: Vec, pub(crate) vehicles: BTreeMap, pub(crate) planes: BTreeMap, pub(crate) use_selected: BTreeMap, @@ -84,7 +81,81 @@ pub(crate) struct Runtime { pub(crate) restored_pers: BTreeMap, } -impl Runtime { +impl RoundScript { + pub(crate) fn new(_: &MatchScript) -> Self { + Self { + program: Default::default(), + natives: Default::default(), + next_serial: Default::default(), + fault: Default::default(), + fault_reported: Default::default(), + errors: Default::default(), + dead: Default::default(), + dying: Default::default(), + last_tick: Default::default(), + started: Default::default(), + objects: Default::default(), + next_object: Default::default(), + arrays: Default::default(), + dynamic_symbols: Default::default(), + buckets: Default::default(), + spawned: Default::default(), + waiters: Default::default(), + loading: Default::default(), + presented: Default::default(), + unsupported: Default::default(), + budget: Default::default(), + suspended: Default::default(), + suspended_frames: Default::default(), + pending_unwinds: Default::default(), + entities: Default::default(), + hud_slots: Default::default(), + next_entity_number: Default::default(), + tables: Default::default(), + rng: Default::default(), + pending_notifies: Default::default(), + signals: Default::default(), + engine: Default::default(), + players: Default::default(), + menu_answers: Default::default(), + team_ranks: Default::default(), + ranks_sent: Default::default(), + selected_classes: Default::default(), + thrown_insertions: Default::default(), + insertion_spots: Default::default(), + disconnects: Default::default(), + kicks: Default::default(), + joined: Default::default(), + current_hit: Default::default(), + deaths: Default::default(), + exit_level: Default::default(), + shown: Default::default(), + retired_presence: Default::default(), + blasts: Default::default(), + hits: Default::default(), + use_held: Default::default(), + fired_once: Default::default(), + require_look_at: Default::default(), + server_info: Default::default(), + server_info_defaults: Default::default(), + missiles: Default::default(), + missiles_seen_ms: Default::default(), + grenade_touches: Default::default(), + lingering: Default::default(), + retired_items: Default::default(), + finishing: Default::default(), + finishing_order: Default::default(), + vehicles: Default::default(), + planes: Default::default(), + use_selected: Default::default(), + t5: Default::default(), + restart: Default::default(), + finished: Default::default(), + pending_restart: Default::default(), + restored_pers: Default::default(), + } + } + pub(crate) fn program_fingerprint(&self) -> Option<[u8; 32]> { self.program.as_ref().map(|program| program.fingerprint()) } @@ -93,6 +164,52 @@ impl Runtime { self.objects.contains_key(id) && !self.dead.contains(id) } + pub(crate) fn script_is_defined(&self, id: &u64) -> bool { + self.live(id) && !self.finishing.contains(id) + } + + pub(crate) fn can_receive_call(&self, id: &u64) -> bool { + self.script_is_defined(id) && !self.retired_items.contains(id) + } + + pub(crate) fn request_delete(&mut self, id: u64) -> bool { + if !self.live(&id) || !self.finishing.insert(id) { + return false; + } + self.finishing_order.push(id); + true + } + + pub(crate) fn retire_item_payload(&mut self, number: i32) { + self.retired_items + .extend(self.entities.iter().filter_map(|(id, entity)| { + matches!(entity.kind, host::entities::EntityKind::Item(at) if at == number) + .then_some(*id) + })); + } + + pub(crate) fn take_retired_items(&mut self) -> Vec { + std::mem::take(&mut self.retired_items) + } + + pub(crate) fn take_finishing(&mut self) -> Vec { + self.finishing.clear(); + std::mem::take(&mut self.finishing_order) + } + + pub(crate) fn request_round_restart(&mut self, persist: bool) -> Result<(), String> { + if self.program.is_none() || !self.started || self.fault.is_some() { + return Err("no healthy started match scripts".into()); + } + if self.finished { + return Err("match end already requested".into()); + } + self.finished = true; + self.pending_restart = Some(persist); + self.signals.push(crate::script::MAP_RESTART.into()); + Ok(()) + } + pub(crate) fn symbol(&mut self, name: &str) -> u32 { let program = self.program.as_ref().unwrap(); if let Some(&id) = program.symbol_ids.get(name) { @@ -102,3 +219,16 @@ impl Runtime { *self.dynamic_symbols.entry(name.into()).or_insert(next) } } + +#[derive(Resource, Clone, Debug, Default)] +pub(crate) struct MatchScript { + pub(crate) precached: BTreeMap<(&'static str, String), i32>, + pub(crate) dvars: BTreeMap, + pub(crate) local_presentation_dvars: bool, + pub(crate) local_presentation_client: Option, + pub(crate) pending_local_dvars: Vec<(crate::TargetBoxDvar, String)>, + pub(crate) personal_classes: BTreeMap<(u32, u32), crate::ClassDef>, + pub(crate) weapon_bridge: BTreeMap>, + pub(crate) next_spawned_presence: u32, + pub(crate) match_data: host::match_data::MatchData, +} diff --git a/crates/sim/src/script_player.rs b/crates/sim/src/script_player.rs index 32be22d..667597d 100644 --- a/crates/sim/src/script_player.rs +++ b/crates/sim/src/script_player.rs @@ -16,6 +16,23 @@ pub(crate) fn spectator_lifecycle(team: i32) -> ClientLifecycle { } } +fn spawn_command_angles(world: &mut FrameWorld, id: ClientId) -> [i32; 3] { + world + .ecs() + .get_resource::() + .and_then(|request| { + request + .input + .cmds + .iter() + .rev() + .find(|command| command.client == id) + .map(|command| command.command.angles) + }) + .or_else(|| world.old_cmd_angles(id)) + .unwrap_or([0; 3]) +} + pub(crate) fn spawn( world: &mut FrameWorld, tick: Tick, @@ -41,9 +58,8 @@ pub(crate) fn spawn( ps.max_health = max; ps.health = max; } - if let Some(cmd) = world.old_cmd_angles(id) { - ps.delta_angles = std::array::from_fn(|axis| angles[axis] - cmd[axis] as f32 * SHORT2ANGLE); - } + let cmd = spawn_command_angles(world, id); + ps.delta_angles = std::array::from_fn(|axis| angles[axis] - cmd[axis] as f32 * SHORT2ANGLE); let prev_teleport = world .player(id) .map(|p| p.e_flags & playerstate_iw4::eflags::TELEPORT) @@ -426,10 +442,13 @@ pub(crate) fn move_to_forced_spawn(world: &mut FrameWorld, id: ClientId, pick: c let Some((origin, angles)) = forced_point(world, id, pick) else { return; }; + let cmd_angles = spawn_command_angles(world, id); world.unlink_player_area(id); world.set_origin(id, origin); world.set_viewangles(id, angles); if let Some(ps) = world.player_mut(id) { + ps.delta_angles = + std::array::from_fn(|axis| angles[axis] - cmd_angles[axis] as f32 * SHORT2ANGLE); ps.velocity = [0.0; 3]; ps.health = ps.max_health; ps.e_flags ^= playerstate_iw4::eflags::TELEPORT; @@ -740,10 +759,13 @@ fn perk_bits(name: &str) -> ([u32; 2], u32) { "specialty_fastmantle" => playerstate_iw4::PERK_FASTMANTLE, "specialty_quickdraw" => playerstate_iw4::PERK_QUICKDRAW, "specialty_holdbreath" => playerstate_iw4::PERK_HOLDBREATH, + "specialty_selectivehearing" => playerstate_iw4::PERK_SELECTIVEHEARING, _ => 0, }; let perk1 = match name { "specialty_spygame" => playerstate_iw4::PERK1_SPYGAME, + "specialty_extendedmelee" => playerstate_iw4::PERK1_EXTENDEDMELEE, + "specialty_fastsprintrecovery" => playerstate_iw4::PERK1_FASTSPRINTRECOVERY, _ => 0, }; let e_flags = match name { diff --git a/crates/sim/src/skill_rating.rs b/crates/sim/src/skill_rating.rs index 5ae34e3..d0bf0d6 100644 --- a/crates/sim/src/skill_rating.rs +++ b/crates/sim/src/skill_rating.rs @@ -64,6 +64,26 @@ impl SkillRatings { Ok(ratings) } + pub(crate) fn updated_ranked( + mut self, + mode: &str, + opponents: &[(Self, f32)], + ) -> Result { + let index = mode_index(mode).ok_or(SkillRatingError::InvalidMode)?; + if opponents.is_empty() { + return Ok(self); + } + let first = self.rows[index]; + let mut delta = 0.0; + for (opponent, score) in opponents { + let difference = f64::from(opponent.rows[index].rating) - f64::from(first.rating); + let expected = 1.0 / (1.0 + 10.0_f64.powf(difference / 400.0)); + delta += 32.0 * (f64::from(*score) - expected); + } + self.rows[index] = updated(first, (delta / opponents.len() as f64).round() as i32)?; + Ok(self) + } + pub(crate) fn updated_pair( mut self, mut opponent: Self, diff --git a/crates/sim/src/step.rs b/crates/sim/src/step.rs index 2651187..7e175c1 100644 --- a/crates/sim/src/step.rs +++ b/crates/sim/src/step.rs @@ -93,7 +93,7 @@ pub(crate) fn run_schedule( let request = ecs .remove_resource::() .expect("simulation request missing"); - if let Some(fault) = ecs.resource::().fault.as_ref() { + if let Some(fault) = ecs.resource::().fault.as_ref() { return Err(fault.clone()); } Ok(request @@ -723,6 +723,11 @@ fn finalize_system(ecs: &mut World) { harvest_predictable_events(&mut world, tick); world.script_gaps_mut().report(); + debug_assert!( + world.validate_entity_payloads().is_ok(), + "{:?}", + world.validate_entity_payloads() + ); } fn publish_snapshot_system(ecs: &mut World) { @@ -753,7 +758,7 @@ fn publish_snapshot_system(ecs: &mut World) { }; drop(world); let script_seats = crate::script::script_seats(ecs); - let mut runtime = ecs.resource_mut::(); + let mut runtime = ecs.resource_mut::(); let script_exit_level = std::mem::take(&mut runtime.exit_level); effects.kicks = std::mem::take(&mut runtime.kicks) .into_iter() @@ -1829,7 +1834,14 @@ fn choose_bot_class(world: &mut FrameWorld, id: ClientId, index: u8) { let bot_classes = &world.bootstrap_ref().bot_classes; let def = (!bot_classes.is_empty() && !t5) .then(|| bot_classes[index as usize % bot_classes.len()].clone()) - .filter(|def| !def.locked && validate_class_content(world, def).is_ok()); + .filter(|def| !def.locked && validate_class_content(world, def).is_ok()) + .filter(|def| { + world + .ecs_ref() + .resource::() + .temporary_client(id) + || crate::progression::validate_class(world, id, def).is_ok() + }); match def { Some(def) => crate::script::choose_class(world.ecs(), id.0, &def), None => crate::script::choose_default_class(world.ecs(), id.0, index), @@ -1855,7 +1867,10 @@ fn answer_custom_class( let Some(def) = crate::script::personal_class(world.ecs(), id.0, crate::ClassId(slot)) else { return Some(ActionOutcome::Refused); }; - if validate_class_content(world, &def).is_err() { + if validate_class_content(world, &def) + .and_then(|()| crate::progression::validate_class(world, id, &def)) + .is_err() + { return Some(ActionOutcome::Refused); } crate::script::choose_class(world.ecs(), id.0, &def); @@ -1895,7 +1910,9 @@ fn apply_select_class( return ActionOutcome::Refused; }; - if let Err(reason) = validate_class_content(world, &def) { + if let Err(reason) = validate_class_content(world, &def) + .and_then(|()| crate::progression::validate_class(world, id, &def)) + { diag::info!( Sim, "class select: rejected client={} id={} rev={} reason={}", @@ -2195,7 +2212,7 @@ pub(crate) fn script_slide( origin: [f32; 3], slide: &mut crate::script::host::mechanics::Slide, ) -> [f32; 3] { - let runtime = world.resource::(); + let runtime = world.resource::(); let linked = |mut child: u64| { for _ in 0..runtime.entities.len() { if child == object { diff --git a/crates/sim/src/weapon_content.rs b/crates/sim/src/weapon_content.rs index e842ab7..189810c 100644 --- a/crates/sim/src/weapon_content.rs +++ b/crates/sim/src/weapon_content.rs @@ -21,6 +21,7 @@ pub struct WeaponScriptSounds { } pub struct SimWeaponRow { + pub unlock_requirement: Result, pub wire_id: u32, pub scales: (f32, f32, f32), pub execution: Result, @@ -39,6 +40,9 @@ pub struct SimWeaponRow { #[derive(Debug)] pub struct SimWeaponContent { + pub(crate) rank_progression: gamemode_iw4::progression::RankProgression, + pub(crate) unlock_requirements: + Vec>, pub(crate) weapon_def_scales: Vec<(f32, f32, f32)>, pub(crate) weapon_combat: Vec, pub(crate) weapon_runnable: Vec, @@ -64,11 +68,14 @@ pub enum SimWeaponContentRefusal { MissingSentinel, NonDenseRows, UnknownAlias, + InvalidProgression, } impl SimWeaponContent { pub(crate) fn bootstrap() -> Self { Self { + rank_progression: Default::default(), + unlock_requirements: Default::default(), weapon_def_scales: Default::default(), weapon_combat: Default::default(), weapon_runnable: Default::default(), @@ -95,8 +102,13 @@ impl SimWeaponContent { aliases: impl IntoIterator, pen_table: weapon_iw4::PenetrationDepthTable, pen_table_loaded: bool, + rank_progression: gamemode_iw4::progression::RankProgression, ) -> Result, SimWeaponContentRefusal> { + if rank_progression.rank(0).is_none() { + return Err(SimWeaponContentRefusal::InvalidProgression); + } let mut result = Self { + rank_progression, pen_table, pen_table_loaded, ..Self::bootstrap() @@ -107,6 +119,7 @@ impl SimWeaponContent { if row.wire_id as usize != names.len() { return Err(SimWeaponContentRefusal::NonDenseRows); } + result.unlock_requirements.push(row.unlock_requirement); result.weapon_def_scales.push(row.scales); let (combat, refusal) = match row.execution { Ok(combat) if row.wire_id != 0 => (combat, None), diff --git a/crates/sim/src/world.rs b/crates/sim/src/world.rs index f02335e..9de7c6e 100644 --- a/crates/sim/src/world.rs +++ b/crates/sim/src/world.rs @@ -153,6 +153,7 @@ pub struct SimState { running: bool, phase: MatchPhase, + round_serial: u32, match_elapsed_ms: u32, @@ -258,6 +259,7 @@ impl Default for SimState { bootstrap: MatchBootstrap::default(), running: false, phase: MatchPhase::Warmup, + round_serial: 1, match_elapsed_ms: 0, prematch: gamemode_iw4::PrematchStep::default(), max_alive_seen: 0, @@ -543,6 +545,7 @@ impl SimState { } pub(crate) fn restart_level_phase(&mut self) { + self.round_serial = self.round_serial.wrapping_add(1); self.phase = MatchPhase::Warmup; self.match_elapsed_ms = 0; self.prematch = gamemode_iw4::PrematchStep::default(); @@ -715,6 +718,20 @@ impl SimState { Arc::clone(&self.content.weapons().weapon_script_names) } + pub(crate) fn rank_for_xp(&self, xp: i32) -> Option { + self.content.weapons().rank_progression.rank(xp) + } + + pub(crate) fn weapon_unlock_requirement( + &self, + weapon: u32, + ) -> Option<&Result> { + self.content + .weapons() + .unlock_requirements + .get(weapon as usize) + } + pub(crate) fn weapon_setup(&self, weapon: u32) -> Option<&WeaponSetup> { self.content .weapons() @@ -882,6 +899,10 @@ impl SimState { .expect("dynamic entity generation changed while its typed store was alive"); } + pub(crate) fn retained_missile_events(&self) -> &[entity_iw4::EntityState] { + &self.dying_missiles + } + pub(crate) fn expire_dying_missiles(&mut self, tick: Tick) { debug_assert_eq!( self.entity_kernel.level_time_ms(), @@ -941,23 +962,29 @@ impl SimState { &self.content.weapons().weapon_combat, &self.content.weapons().bullet_pen, &self.content.weapons().weapon_runnable, + &self.content.weapons().unlock_requirements, + &self.content.weapons().rank_progression, &self.content.weapons().weapon_transition_groups, &self.content.weapons().weapon_camouflage_slots, &self.content.weapons().equipment_runtime, &self.bootstrap, self.content.clip_brushes(), &self.entity_collision_capabilities, + self.content.map(), ); self.content_components = crate::content::content_components( &self.content.weapons().weapon_combat, &self.content.weapons().bullet_pen, &self.content.weapons().weapon_runnable, + &self.content.weapons().unlock_requirements, + &self.content.weapons().rank_progression, &self.content.weapons().weapon_transition_groups, &self.content.weapons().weapon_camouflage_slots, &self.content.weapons().equipment_runtime, &self.bootstrap, self.content.clip_brushes(), &self.entity_collision_capabilities, + self.content.map(), ); } @@ -2367,6 +2394,7 @@ impl SimState { meta: SnapshotMeta { objectives: self.objectives.clone(), phase: self.phase, + round_serial: self.round_serial, match_elapsed_ms: self.match_elapsed_ms, prematch: self.prematch, score_limit: self.bootstrap.score_limit, @@ -2711,6 +2739,17 @@ impl SimState { } pub fn install_content(&mut self, content: Arc) { + let panes = match &content.map().glass { + asset_core::GlassContent::Prepared(glass) => glass + .panes() + .iter() + .copied() + .enumerate() + .map(|(id, pane)| (id as u32, pane)) + .collect(), + _ => Vec::new(), + }; + self.world_objects.install_glass_panes(panes); self.replace_content(content); self.player_bodies.materialize_error = None; self.reset_area_entity_world(); @@ -3237,6 +3276,7 @@ pub(crate) fn clip_trace( let mut best = open(); let mut mesh_census = clipmap_iw4::MeshWalkCensus::default(); + let has_leaf_aabbs = !no_mesh && clipmap_iw4::leaves_have_coll_aabb(map.leaves); let frac = Cell::new(1.0_f32); LEAF_AABB_SCRATCH.with(|scratch| { let mut scratch = scratch.borrow_mut(); @@ -3258,7 +3298,7 @@ pub(crate) fn clip_trace( &mesh_ref, leaf, &ext, - map.leaves, + has_leaf_aabbs, &mut best, &mut mesh_census, &mut scratch, @@ -3271,7 +3311,7 @@ pub(crate) fn clip_trace( let mut ignored_winner = clipmap_iw4::ClipWorldWinner::Open; clipmap_iw4::finish_mesh_forest_fallback( &mesh_ref, - map.leaves, + has_leaf_aabbs, &ext, &mut best, &mut ignored_winner, diff --git a/crates/sim/src/world/content.rs b/crates/sim/src/world/content.rs index e5d1f13..c27feef 100644 --- a/crates/sim/src/world/content.rs +++ b/crates/sim/src/world/content.rs @@ -97,6 +97,10 @@ pub struct SimContent { } impl SimContent { + pub fn map(&self) -> &Arc { + &self.data.map + } + pub fn clip_brushes(&self) -> &[SimBrush] { &self.data.clip_brushes } @@ -113,6 +117,7 @@ impl SimContent { #[derive(Debug)] struct ContentData { + map: Arc, weapons: Arc, script_sound_aliases: Option>>, clip_brushes: Vec, @@ -143,6 +148,14 @@ pub struct SimContentBuilder { } impl SimContentBuilder { + pub fn set_map(&mut self, map: Arc) -> Result<(), String> { + if let asset_core::GlassContent::Invalid(error) = &map.glass { + return Err(error.clone()); + } + self.data.map = map; + Ok(()) + } + pub fn bootstrap() -> Self { Self::for_match(Arc::new(crate::SimWeaponContent::bootstrap())) } @@ -150,6 +163,7 @@ impl SimContentBuilder { pub fn for_match(weapons: Arc) -> Self { Self { data: ContentData { + map: Default::default(), weapons, script_sound_aliases: Default::default(), clip_brushes: Default::default(), diff --git a/crates/ui/menus/classes.json b/crates/ui/menus/classes.json index 8be8ea6..925925f 100644 --- a/crates/ui/menus/classes.json +++ b/crates/ui/menus/classes.json @@ -606,6 +606,27 @@ } ], "append_items": [ + { + "name": "class_samples", + "item_type": 1, + "style": 1, + "text_key": "USE SAMPLE", + "text_align_mode": 6, + "text_align_x": -60, + "text_literal": true, + "rect": {"x": -64, "y": 350, "w": 336, "h": 20, "horz_align": 1, "vert_align": 1}, + "background": "menu_button_selection_bar", + "font_enum": 3, + "text_scale": 0.3, + "fore_color": [1, 1, 1, 1], + "back_color": [0, 0, 0, 0], + "vis_exp": "op 57 s:636c6173735f656469746f72 op 1", + "handlers": { + "action": [{"Script": "play mouse_click; exec ui_class_samples;"}], + "focus": [{"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"}], + "leave_focus": [{"Script": "setItemColor self backcolor 0 0 0 0;"}] + } + }, { "name": "class_status", "text_exp": "op 38 s:75695f636c6173735f737461747573 op 1", @@ -4421,5 +4442,10 @@ "static_flags": 9437184 } ] + }, + { + "name": "class_samples_picker", + "base": "class_picker", + "fields": {"fullscreen": 1} } ] diff --git a/crates/ui/menus/frontend.json b/crates/ui/menus/frontend.json index 586d4e1..41eb97f 100644 --- a/crates/ui/menus/frontend.json +++ b/crates/ui/menus/frontend.json @@ -11,7 +11,7 @@ {"name": "glow", "background": "mockup_bg_glow", "rect": {"x": 0, "y": 0, "w": 640, "h": 480, "horz_align": 4, "vert_align": 4}, "fore_color": [1, 1, 1, 0.5], "static_flags": 1048576}, {"name": "create_game", "item_type": 1, "style": 1, "text_key": "CREATE GAME", "rect": {"x": -64, "y": 28, "w": 336, "h": 20, "horz_align": 1, "vert_align": 1}, "background": "menu_button_selection_bar", "fore_color": [1, 1, 1, 1], "back_color": [0, 0, 0, 0], "font_enum": 3, "text_scale": 0.375, "text_align_mode": 8, "text_align_x": 68, "text_align_y": 0, "handlers": {"action": [{"Script": "play mouse_click; exec \"ui_create_lobby\";"}], "focus": [{"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"}], "leave_focus": [{"Script": "setItemColor self backcolor 0 0 0 0;"}]}}, {"name": "find_lobbies", "item_type": 1, "style": 1, "text_key": "FIND LOBBIES", "rect": {"x": -64, "y": 48, "w": 336, "h": 20, "horz_align": 1, "vert_align": 1}, "background": "menu_button_selection_bar", "fore_color": [1, 1, 1, 1], "back_color": [0, 0, 0, 0], "font_enum": 3, "text_scale": 0.375, "text_align_mode": 8, "text_align_x": 68, "text_align_y": 0, "handlers": {"action": [{"Script": "play mouse_click; open find_lobbies;"}], "focus": [{"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"}], "leave_focus": [{"Script": "setItemColor self backcolor 0 0 0 0;"}]}}, - {"name": "classes", "item_type": 1, "style": 1, "text_key": "CLASSES", "rect": {"x": -64, "y": 84, "w": 336, "h": 20, "horz_align": 1, "vert_align": 1}, "background": "menu_button_selection_bar", "fore_color": [1, 1, 1, 1], "back_color": [0, 0, 0, 0], "font_enum": 3, "text_scale": 0.375, "text_align_mode": 8, "text_align_x": 68, "text_align_y": 0, "handlers": {"action": [{"Script": "play mouse_click; open class_setup;"}], "focus": [{"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"}], "leave_focus": [{"Script": "setItemColor self backcolor 0 0 0 0;"}]}}, + {"name": "classes", "disabled_exp": "op 35 s:75695f636c6173735f617661696c61626c65 op 1 op 12 0", "item_type": 1, "style": 1, "text_key": "", "text_literal": true, "text_exp": "op 38 s:75695f636c6173735f666561747572655f6c6162656c op 1", "rect": {"x": -64, "y": 84, "w": 336, "h": 20, "horz_align": 1, "vert_align": 1}, "background": "menu_button_selection_bar", "fore_color": [1, 1, 1, 1], "back_color": [0, 0, 0, 0], "font_enum": 3, "text_scale": 0.375, "text_align_mode": 8, "text_align_x": 68, "text_align_y": 0, "handlers": {"action": [{"Script": "play mouse_click; exec ui_class_setup;"}], "focus": [{"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"}], "leave_focus": [{"Script": "setItemColor self backcolor 0 0 0 0;"}]}}, {"name": "callsign", "item_type": 1, "style": 1, "text_key": "CALLSIGN", "rect": {"x": -64, "y": 104, "w": 336, "h": 20, "horz_align": 1, "vert_align": 1}, "background": "menu_button_selection_bar", "fore_color": [1, 1, 1, 1], "back_color": [0, 0, 0, 0], "font_enum": 3, "text_scale": 0.375, "text_align_mode": 8, "text_align_x": 68, "text_align_y": 0, "handlers": {"action": [{"Script": "play mouse_click; open callsign_setup;"}], "focus": [{"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"}], "leave_focus": [{"Script": "setItemColor self backcolor 0 0 0 0;"}]}}, {"name": "killstreaks", "item_type": 1, "style": 1, "text_key": "KILLSTREAKS", "rect": {"x": -64, "y": 124, "w": 336, "h": 20, "horz_align": 1, "vert_align": 1}, "background": "menu_button_selection_bar", "fore_color": [1, 1, 1, 1], "back_color": [0, 0, 0, 0], "font_enum": 3, "text_scale": 0.375, "text_align_mode": 8, "text_align_x": 68, "text_align_y": 0, "handlers": {"action": [{"Script": "play mouse_click; open killstreak_setup;"}], "focus": [{"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"}], "leave_focus": [{"Script": "setItemColor self backcolor 0 0 0 0;"}]}}, {"name": "options", "item_type": 1, "style": 1, "text_key": "OPTIONS", "rect": {"x": -64, "y": 144, "w": 336, "h": 20, "horz_align": 1, "vert_align": 1}, "background": "menu_button_selection_bar", "fore_color": [1, 1, 1, 1], "back_color": [0, 0, 0, 0], "font_enum": 3, "text_scale": 0.375, "text_align_mode": 8, "text_align_x": 68, "text_align_y": 0, "handlers": {"action": [{"Script": "play mouse_click; open pc_options_video;"}], "focus": [{"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"}], "leave_focus": [{"Script": "setItemColor self backcolor 0 0 0 0;"}]}}, @@ -123,14 +123,17 @@ { "index": 17, "fields": { - "disabled_exp": "0", + "text_key": "", + "text_literal": true, + "text_exp": "op 38 s:75695f636c6173735f666561747572655f6c6162656c op 1", + "disabled_exp": "op 35 s:75695f636c6173735f617661696c61626c65 op 1 op 12 0", "rect": { "y": 76 }, "handlers": { "action": [ { - "Script": "play mouse_click; open class_setup;" + "Script": "play mouse_click; exec ui_class_setup;" } ] } diff --git a/crates/ui/src/classes/equip_txn.rs b/crates/ui/src/classes/equip_txn.rs index 8b8371c..87717fe 100644 --- a/crates/ui/src/classes/equip_txn.rs +++ b/crates/ui/src/classes/equip_txn.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use frame::ScopeApp; use net::{ClientActionInbox, ClientSet, LocalPresentClient, PresentedSnapshot}; use sim::{ClassId, ClientAction, ClientLifecycle, SimEvent}; @@ -16,6 +17,8 @@ pub struct EquipTxnWatch { pub fn apply_pending_class_equip( mut pending: ResMut, store: Res, + catalog: Res, + player_data: Res, weapons: Option>, mut actions: ResMut, mut watch: ResMut, @@ -52,6 +55,15 @@ pub fn apply_pending_class_equip( ); return; }; + if let Err(reason) = catalog.validate_progression(slot, &player_data) { + diag::info!( + Ui, + "class equip: refused request_id={} — {reason}", + req.request_id + ); + reject_class_equip(&mut phase, &mut status, req.request_id, reason); + return; + } let resolved = weapons .as_ref() .ok_or_else(|| "Weapon catalog is not ready".to_owned()) @@ -62,6 +74,11 @@ pub fn apply_pending_class_equip( let loadout = match resolved { Ok(loadout) => loadout, Err(reason) => { + diag::info!( + Ui, + "class equip: refused request_id={} — {reason}", + req.request_id + ); reject_class_equip(&mut phase, &mut status, req.request_id, reason); return; } @@ -151,7 +168,7 @@ pub fn sync_class_change_allowed( } pub fn resolve_class_equip_transaction( - mut store: ResMut, + mut equipped: ResMut, mut phase: ResMut, mut overlay: ResMut, mut status: ResMut, @@ -183,7 +200,7 @@ pub fn resolve_class_equip_transaction( SimEvent::ClassAccepted { request_id: rid, .. } if rid == request_id => { - if accept_class_equip(&mut store, &mut phase, &mut overlay, request_id) { + if accept_class_equip(&mut equipped, &mut phase, &mut overlay, request_id) { diag::info!( Ui, "class select: reliable Accept request_id={request_id} (overlay closed)" @@ -199,20 +216,20 @@ pub fn resolve_class_equip_transaction( pub(crate) fn register_equip_systems(app: &mut App) { app.add_systems( - Update, - reset_equip_transaction - .after(frame::SessionSwapApplied) - .before(apply_pending_class_equip), + OnExit(frame::MatchScope::Live), + release_equip.in_set(frame::ScopeSet::Release), ) - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .init_resource::() .add_systems( Update, ( consume_class_select_handoff, - publish_signon_class_status, + publish_class_availability, + publish_signon_class_status.in_set(frame::InMatch), sync_class_change_allowed, - apply_pending_class_equip, + apply_pending_class_equip.in_set(frame::InMatch), ) .chain() .in_set(ClientSet::Present), @@ -220,11 +237,57 @@ pub(crate) fn register_equip_systems(app: &mut App) { .add_systems( Update, resolve_class_equip_transaction + .in_set(frame::InMatch) .after(apply_pending_class_equip) .in_set(frame::ClassEquipResolved), ); } +fn publish_class_availability( + store: Res, + catalog: Res, + player_data: Res, + mut dvars: ResMut, +) { + if !store.is_changed() + && !catalog.is_changed() + && !player_data.is_changed() + && dvars.get("ui_class_unavailable_0").is_some() + { + return; + } + let feature_lock = catalog.progression_lock("cac", &player_data); + dvars.set( + "ui_class_feature_lock", + feature_lock.clone().unwrap_or_default(), + ); + for index in 0..sim::match_state::PERSONAL_CLASS_SLOTS { + let reason = feature_lock + .clone() + .or_else(|| { + (!player_data.custom_class_available(index)) + .then(|| "Custom class slot is locked".to_owned()) + }) + .or_else(|| { + store.slots.get(index).map_or_else( + || Some("Class is unavailable".to_owned()), + |slot| { + slot.lock_reason.clone().or_else(|| { + catalog + .validate_class(slot) + .and_then(|()| catalog.validate_progression(slot, &player_data)) + .err() + }) + }, + ) + }); + dvars.set( + &format!("ui_class_unavailable_{index}"), + reason.unwrap_or_default(), + ); + } +} + fn consume_class_select_handoff( mut handoff: ResMut, mut catalog: ResMut, @@ -295,21 +358,6 @@ fn publish_signon_class_status( } } -fn reset_equip_transaction( - mut store: ResMut, - mut torn: MessageReader, - mut watch: ResMut, - mut pending: ResMut, - mut phase: ResMut, - mut status: ResMut, -) { - if torn.read().next().is_none() { - return; - } - torn.clear(); - store.equipped = None; - *watch = EquipTxnWatch::default(); - pending.0 = None; - *phase = ClassSelectPhase::default(); - status.0 = None; +fn release_equip(mut handoff: ResMut) { + *handoff = frame::ClassSelectHandoff::default(); } diff --git a/crates/ui/src/classes/select.rs b/crates/ui/src/classes/select.rs index 40c1a45..e43eb4e 100644 --- a/crates/ui/src/classes/select.rs +++ b/crates/ui/src/classes/select.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use frame::ScopeApp; use frame::{AppScreen, ClientSet, RuntimeRole}; use net::SignonState; @@ -165,7 +166,7 @@ pub fn commit_class_equip( } pub fn accept_class_equip( - store: &mut SessionClassStore, + equipped: &mut crate::classes::store::EquippedClass, phase: &mut ClassSelectPhase, overlay: &mut ClassSelectOverlayOpen, request_id: u32, @@ -175,7 +176,7 @@ pub fn accept_class_equip( request_id: pending, class_index, } if pending == request_id => { - store.equipped = Some(class_index); + equipped.0 = Some(class_index); *phase = ClassSelectPhase::Interactive; overlay.0 = false; true @@ -210,9 +211,9 @@ impl Plugin for ClassSelectPlugin { app.init_resource::() .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) + .scoped::(frame::MatchScope::Live) .init_resource::() .init_resource::() .init_resource::() diff --git a/crates/ui/src/classes/setup.rs b/crates/ui/src/classes/setup.rs index 234fba2..a47d34e 100644 --- a/crates/ui/src/classes/setup.rs +++ b/crates/ui/src/classes/setup.rs @@ -141,6 +141,53 @@ impl Default for ClassLoadoutCatalog { } impl ClassLoadoutCatalog { + pub fn progression_lock(&self, value: &str, data: &sim::LocalPlayerData) -> Option { + if value.is_empty() || value == "specialty_null" { + return None; + } + let Some(registry) = self.resolver.0.as_deref() else { + return Some("Content is loading".into()); + }; + let (namespace, name) = asset_game::FamilyKey::parse(value) + .map(|family| (family.namespace, family.base)) + .unwrap_or_else(|| { + ( + asset_core::AssetNamespace::Iw4, + value.trim_end_matches("_mp").to_owned(), + ) + }); + let requirement = match registry.item_unlock_requirement(namespace, &name) { + Ok(requirement) => requirement, + Err(reason) => return Some(format!("{value}: {reason}")), + }; + let Some(progression) = data.progression.as_ref() else { + return Some("Progression is loading".into()); + }; + if progression.rank < requirement.rank { + return Some(format!("Unlocks at level {}", requirement.rank + 1)); + } + (!progression.unlocked(&requirement)).then(|| "Complete the required challenge".into()) + } + + pub fn validate_progression( + &self, + slot: &ClassSlotState, + data: &sim::LocalPlayerData, + ) -> Result<(), String> { + for row in ClassEditRow::ALL { + if let Some(reason) = self.progression_lock(slot.row_value(row), data) { + return Err(reason); + } + } + if slot.perk1 == "specialty_bling" + && slot.secondary_attachments.len() > 1 + && let Some(reason) = self.progression_lock("specialty_secondarybling", data) + { + return Err(reason); + } + Ok(()) + } + pub fn from_editor_catalog(registry: std::sync::Arc) -> Self { let families = registry.weapon_families(); let offered: std::collections::HashSet<_> = diff --git a/crates/ui/src/classes/store.rs b/crates/ui/src/classes/store.rs index c2dc69a..7ee210c 100644 --- a/crates/ui/src/classes/store.rs +++ b/crates/ui/src/classes/store.rs @@ -8,7 +8,6 @@ use sim::match_state::PERSONAL_CLASS_SLOTS; pub struct SessionClassStore { pub slots: Vec, - pub equipped: Option, pub selected: usize, } @@ -115,15 +114,10 @@ impl SessionClassStore { } Self { slots: slots.iter().map(ClassSlotState::from_host_slot).collect(), - equipped: None, selected: 0, } } - pub fn equipped_slot(&self) -> Option<&ClassSlotState> { - self.equipped.and_then(|i| self.slots.get(i)) - } - pub fn apply_coverage_locks(&mut self, reasons: impl IntoIterator>) { for (slot, reason) in self.slots.iter_mut().zip(reasons) { slot.lock_reason = reason; @@ -300,6 +294,11 @@ pub(crate) fn load_class_store( diag::info!(Ui, "classes: {} read from {}", slots.len(), path.display()); file.written = Some(encode_slots(&slots)); store.slots = slots; + let mut defaults = generate().slots; + let start = store.slots.len(); + store + .slots + .extend(defaults.drain(start.min(defaults.len())..)); for slot in &mut store.slots { slot.lock_reason = catalog.validate_class(slot).err(); } @@ -404,3 +403,6 @@ pub(crate) fn save_class_store( } } } + +#[derive(Resource, Default)] +pub struct EquippedClass(pub Option); diff --git a/crates/ui/src/launch_report.rs b/crates/ui/src/launch_report.rs index a4f61ba..ed1e8b6 100644 --- a/crates/ui/src/launch_report.rs +++ b/crates/ui/src/launch_report.rs @@ -8,6 +8,7 @@ pub fn publish_gap_hud( identity: Option>, report: Option>, class_store: Res, + equipped: Option>, mut hud: ResMut, ) { let Some(identity) = identity else { @@ -30,7 +31,10 @@ pub fn publish_gap_hud( sim::FFA.time_limit_ms ), ]; - if let Some(slot) = class_store.equipped_slot() { + if let Some(slot) = equipped + .and_then(|equipped| equipped.0) + .and_then(|index| class_store.slots.get(index)) + { body.push(format!("equipped class: {}", slot.name)); } else { body.push("equipped class: (choose-class gate)".into()); diff --git a/crates/ui/src/menu_load.rs b/crates/ui/src/menu_load.rs index 40857f6..78fb619 100644 --- a/crates/ui/src/menu_load.rs +++ b/crates/ui/src/menu_load.rs @@ -14,7 +14,7 @@ use crate::loading::{ LoadingCamera, LoadingRoot, LoadingScreen, OverlayUiCamera, dismiss_loading_overlay, }; use crate::menu::MenuMapList; -use frame::{AppScreen, LaunchIdentity, LaunchReport, MapLoadApproved, ReturnedToMenu}; +use frame::{AppScreen, LaunchIdentity, LaunchReport, MapLoadApproved}; fn launch_report( zone: String, @@ -159,7 +159,7 @@ pub(crate) fn begin_load_from_session( pub(crate) fn restore_menu_on_return( mut commands: Commands, - mut returned: MessageReader, + mut returned: MessageReader>, mut class_overlay: ResMut, mut maps: ResMut, identity: Option>, @@ -167,7 +167,10 @@ pub(crate) fn restore_menu_on_return( chrome: Query, With)>>, overlay_cams: Query>, ) { - if returned.read().count() == 0 { + if !returned + .read() + .any(|event| event.exited.is_some() && event.entered == Some(frame::MatchScope::Absent)) + { return; } class_overlay.0 = false; @@ -241,7 +244,7 @@ fn install_menu_strings_prepare( } pub(crate) fn register_menu_load_systems(app: &mut App) { - app.add_message::().add_systems( + app.add_systems( Update, ( begin_load_from_session, diff --git a/crates/updater/Cargo.toml b/crates/updater/Cargo.toml index 6dc1a54..f271590 100644 --- a/crates/updater/Cargo.toml +++ b/crates/updater/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true description = "Community configuration and HTTPS self-update before game startup." [dependencies] +diag = { path = "../diag" } rustls = { version = "0.23.31", default-features = false, features = ["ring", "std"] } rustls-pemfile = "2.2.0" serde.workspace = true diff --git a/crates/updater/src/lib.rs b/crates/updater/src/lib.rs index f238552..79b8235 100644 --- a/crates/updater/src/lib.rs +++ b/crates/updater/src/lib.rs @@ -258,114 +258,177 @@ pub fn startup() -> Result>> { return Ok(Some(args)); }; let community = read_community(&path)?; + SELECTED + .set(community.clone()) + .map_err(|_| "community already selected")?; if !cfg!(windows) { - SELECTED - .set(community) - .map_err(|_| "community already selected")?; return Ok(Some(args)); } - let lock = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(root.join("iw4l.update.lock")) + update_log(&format!("checking updates for {}", community.name)); + match check_update(&exe, root, &community, &args, restarted) { + Ok(true) => Ok(None), + Ok(false) => Ok(Some(args)), + Err(error) => { + let message = + format!("update failed: {error}; starting game with the installed version"); + diag::announce_stdout(&format!("[iw4l] {message}")); + diag::warn!(Launch, "{message}"); + Ok(Some(args)) + } + } +} + +fn update_log(message: &str) { + diag::announce_stdout(&format!("[iw4l] {message}")); + diag::info!(Launch, "{message}"); +} + +fn check_update( + exe: &Path, + root: &Path, + community: &Community, + args: &[OsString], + restarted: bool, +) -> Result { + let mut stage = "acquire update lock"; + let result = (|| { + let lock = open_update_lock(root)?; + let deadline = Instant::now() + Duration::from_secs(10); + loop { + if lock.try_lock_exclusive().is_ok() { + break; + } + if !restarted || Instant::now() >= deadline { + return Err("another iw4l process is checking or applying an update".into()); + } + std::thread::sleep(Duration::from_millis(20)); + } + stage = "clean up previous update helper"; + cleanup_helper(root)?; + stage = "resolve update host and configure TLS"; + let (client, url) = client(community)?; + stage = "fetch update manifest"; + let manifest = + Manifest::parse(&get(&client, url.clone(), MAX_MANIFEST, "manifest", None)?)?; + update_log(&format!( + "release {}: {} compressed bytes, {} executable bytes", + manifest.release, manifest.file.compressed_size, manifest.file.size + )); + stage = "verify installed executable"; + update_log("checking installed executable size and SHA-256"); + if fs::metadata(exe).map_err(|e| e.to_string())?.len() == manifest.file.size + && file_sha256(exe)? == manifest.file.sha256.to_ascii_lowercase() + { + let _ = fs::remove_file(root.join("iw4l.previous.exe")); + update_log("installed executable is up to date; starting game"); + return Ok(false); + } + stage = "download release"; + update_log(&format!("downloading release {}", manifest.release)); + let blob_url = url.join(&manifest.file.path).map_err(|e| e.to_string())?; + let blob = get( + &client, + blob_url, + manifest.file.compressed_size, + "release", + Some(manifest.file.compressed_size), + )?; + stage = "decompress release"; + update_log("decompressing release"); + let mut decoder = + zstd::stream::read::Decoder::new(blob.as_slice()).map_err(|e| e.to_string())?; + decoder.window_log_max(29).map_err(|e| e.to_string())?; + let decoded = read_bounded(decoder, manifest.file.size)?; + stage = "verify downloaded executable"; + update_log("verifying downloaded executable size and SHA-256"); + if decoded.len() as u64 != manifest.file.size { + return Err(format!( + "executable size mismatch: expected {} bytes, received {}", + manifest.file.size, + decoded.len() + )); + } + let actual_hash = hash(&decoded); + if actual_hash != manifest.file.sha256.to_ascii_lowercase() { + return Err(format!( + "executable SHA-256 mismatch: expected {}, received {actual_hash}", + manifest.file.sha256 + )); + } + stage = "stage verified executable"; + update_log("writing verified executable to iw4l.update.exe"); + let staged = root.join("iw4l.update.exe"); + write_synced(&staged, &decoded)?; + fs::set_permissions( + &staged, + fs::metadata(exe).map_err(|e| e.to_string())?.permissions(), + ) .map_err(|e| e.to_string())?; - let deadline = Instant::now() + Duration::from_secs(10); - loop { - if lock.try_lock_exclusive().is_ok() { - break; - } - if !restarted || Instant::now() >= deadline { - return Err("another iw4l process is checking or applying an update".into()); - } - std::thread::sleep(Duration::from_millis(20)); - } - cleanup_helper(root)?; - let (client, url) = client(&community)?; - let manifest = Manifest::parse(&get(&client, url.clone(), MAX_MANIFEST)?)?; - if fs::metadata(&exe).map_err(|e| e.to_string())?.len() == manifest.file.size - && file_sha256(&exe)? == manifest.file.sha256.to_ascii_lowercase() - { - let _ = fs::remove_file(root.join("iw4l.previous.exe")); - SELECTED - .set(community) - .map_err(|_| "community already selected")?; - return Ok(Some(args)); - } - println!("[iw4l] downloading release {}", manifest.release); - let blob_url = url.join(&manifest.file.path).map_err(|e| e.to_string())?; - let blob = get(&client, blob_url, manifest.file.compressed_size)?; - if blob.len() as u64 != manifest.file.compressed_size { - return Err("compressed update size mismatch".into()); - } - let mut decoder = - zstd::stream::read::Decoder::new(blob.as_slice()).map_err(|e| e.to_string())?; - decoder.window_log_max(29).map_err(|e| e.to_string())?; - let decoded = read_bounded(decoder, manifest.file.size)?; - if decoded.len() as u64 != manifest.file.size - || hash(&decoded) != manifest.file.sha256.to_ascii_lowercase() - { - return Err("update executable size or SHA-256 mismatch".into()); - } - let staged = root.join("iw4l.update.exe"); - write_synced(&staged, &decoded)?; - fs::set_permissions( - &staged, - fs::metadata(&exe).map_err(|e| e.to_string())?.permissions(), - ) - .map_err(|e| e.to_string())?; - let temp = std::env::temp_dir().join(format!( - "iw4l-update-{}-{}", - std::process::id(), - &manifest.file.sha256[..16] - )); - fs::create_dir(&temp).map_err(|e| format!("create helper directory: {e}"))?; - let helper = temp.join("iw4l-update-helper.exe"); - fs::copy(&exe, &helper).map_err(|e| e.to_string())?; - let job = Job { - pid: std::process::id(), - target: exe.clone(), - source: staged, - sha256: manifest.file.sha256, - size: manifest.file.size, - args: args - .iter() - .map(|a| { - a.to_str() - .map(str::to_owned) - .ok_or("non-UTF8 launch argument") - }) - .collect::>()?, - cwd: std::env::current_dir().map_err(|e| e.to_string())?, - }; - let job_path = temp.join("job.json"); - write_synced( - &job_path, - &serde_json::to_vec(&job).map_err(|e| e.to_string())?, - )?; - write_synced( - &root.join("iw4l.update-helper"), - temp.to_str().ok_or("non-UTF8 helper directory")?.as_bytes(), - )?; - let mut child = Command::new(helper) - .arg("--iw4l-replace") - .arg(&job_path) - .stdin(std::process::Stdio::null()) - .spawn() - .map_err(|e| e.to_string())?; - let deadline = Instant::now() + Duration::from_secs(10); - while !temp.join("ready").is_file() { - if child.try_wait().map_err(|e| e.to_string())?.is_some() { - return Err("update helper exited before handoff".into()); - } - if Instant::now() >= deadline { + stage = "prepare replacement helper"; + update_log("preparing replacement helper"); + let temp = std::env::temp_dir().join(format!( + "iw4l-update-{}-{}", + std::process::id(), + &manifest.file.sha256[..16] + )); + fs::create_dir(&temp).map_err(|e| format!("create helper directory: {e}"))?; + let helper = temp.join("iw4l-update-helper.exe"); + fs::copy(exe, &helper).map_err(|e| e.to_string())?; + let job = Job { + pid: std::process::id(), + target: exe.to_path_buf(), + source: staged, + sha256: manifest.file.sha256, + size: manifest.file.size, + args: args + .iter() + .map(|a| { + a.to_str() + .map(str::to_owned) + .ok_or("non-UTF8 launch argument") + }) + .collect::>()?, + cwd: std::env::current_dir().map_err(|e| e.to_string())?, + }; + let job_path = temp.join("job.json"); + write_synced( + &job_path, + &serde_json::to_vec(&job).map_err(|e| e.to_string())?, + )?; + write_synced( + &root.join("iw4l.update-helper"), + temp.to_str().ok_or("non-UTF8 helper directory")?.as_bytes(), + )?; + stage = "start replacement helper"; + update_log("starting replacement helper and waiting for handoff"); + let mut child = Command::new(helper) + .arg("--iw4l-replace") + .arg(&job_path) + .stdin(std::process::Stdio::null()) + .spawn() + .map_err(|e| e.to_string())?; + let handoff = (|| { + let deadline = Instant::now() + Duration::from_secs(10); + while !temp.join("ready").is_file() { + if let Some(status) = child.try_wait().map_err(|e| e.to_string())? { + return Err(format!("update helper exited before handoff: {status}")); + } + if Instant::now() >= deadline { + return Err("update helper handoff timeout".into()); + } + std::thread::sleep(Duration::from_millis(20)); + } + Ok(()) + })(); + if let Err(error) = handoff { let _ = child.kill(); - return Err("update helper handoff timeout".into()); + let _ = child.wait(); + return Err(error); } - std::thread::sleep(Duration::from_millis(20)); - } - Ok(None) + update_log("replacement helper ready; handing off to install and restart"); + Ok(true) + })(); + result.map_err(|error| format!("{stage}: {error}")) } fn cleanup_helper(root: &Path) -> Result<()> { @@ -418,13 +481,30 @@ fn client(community: &Community) -> Result<(reqwest::blocking::Client, reqwest:: let port = url .port_or_known_default() .ok_or("update URL has no port")?; - let addresses: Vec<_> = (url.host_str().ok_or("missing update host")?, port) - .to_socket_addrs() - .map_err(|e| format!("update address: {e}"))? - .collect(); + update_log(&format!( + "resolving update host {}:{port} (DNS timeout 8s)", + url.host_str().ok_or("missing update host")? + )); + let host = url.host_str().ok_or("missing update host")?.to_owned(); + let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let lookup_host = host.clone(); + std::thread::spawn(move || { + let addresses = (lookup_host.as_str(), port) + .to_socket_addrs() + .map(|addresses| addresses.collect::>()); + let _ = sender.send(addresses); + }); + let addresses = receiver + .recv_timeout(Duration::from_secs(8)) + .map_err(|e| format!("DNS lookup {host}:{port} (8s timeout): {e}"))? + .map_err(|e| format!("DNS lookup {host}:{port}: {e}"))?; if addresses.is_empty() { return Err("update host resolved no addresses".into()); } + update_log(&format!( + "resolved update addresses: {addresses:?}; TLS server name: {}", + community.master.server_name + )); let roots = trust_roots(&community.updates.ca_pem)?; let tls = rustls::ClientConfig::builder_with_provider(Arc::new( rustls::crypto::ring::default_provider(), @@ -463,19 +543,98 @@ pub fn trust_roots(pem: &str) -> Result { Ok(roots) } -fn get(client: &reqwest::blocking::Client, url: reqwest::Url, cap: u64) -> Result> { - let response = client - .get(url) +fn error_chain(error: &(dyn std::error::Error + 'static)) -> String { + let mut message = error.to_string(); + let mut source = error.source(); + while let Some(cause) = source { + message.push_str(&format!(": {cause}")); + source = cause.source(); + } + message +} + +fn get( + client: &reqwest::blocking::Client, + url: reqwest::Url, + cap: u64, + label: &str, + expected: Option, +) -> Result> { + let started = Instant::now(); + update_log(&format!( + "requesting {label}: {url} (connect timeout 8s, request timeout 180s)" + )); + let mut response = client + .get(url.clone()) .header(reqwest::header::ACCEPT_ENCODING, "identity") .send() - .map_err(|e| e.to_string())?; + .map_err(|e| format!("GET {url}: {}", error_chain(&e)))?; + update_log(&format!( + "{label}: HTTP {} after {:.1}s, content length {:?}", + response.status(), + started.elapsed().as_secs_f64(), + response.content_length() + )); if response.status() != reqwest::StatusCode::OK { - return Err(format!("update HTTP status {}", response.status())); + return Err(format!("GET {url}: HTTP {}", response.status())); } if response.content_length().is_some_and(|size| size > cap) { - return Err("update response exceeds limit".into()); + return Err(format!("GET {url}: response exceeds {cap} byte limit")); } - read_bounded(response, cap) + let total = expected.or(response.content_length()); + let mut bytes = Vec::new(); + let mut buffer = [0; 64 * 1024]; + let mut last_progress = Instant::now(); + update_log(&format!("{label}: download started, 0 bytes received")); + loop { + let remaining = (cap + 1 - bytes.len() as u64).min(buffer.len() as u64) as usize; + let count = response.read(&mut buffer[..remaining]).map_err(|e| { + format!( + "GET {url}: read failed after {} bytes in {:.1}s: {}", + bytes.len(), + started.elapsed().as_secs_f64(), + error_chain(&e) + ) + })?; + if count == 0 { + break; + } + bytes.extend_from_slice(&buffer[..count]); + if bytes.len() as u64 > cap { + return Err(format!("GET {url}: response exceeds {cap} byte limit")); + } + if last_progress.elapsed() >= Duration::from_secs(1) { + log_progress(label, bytes.len() as u64, total, started.elapsed()); + last_progress = Instant::now(); + } + } + if let Some(expected) = total + && bytes.len() as u64 != expected + { + return Err(format!( + "GET {url}: incomplete download: received {} of {expected} bytes in {:.1}s", + bytes.len(), + started.elapsed().as_secs_f64() + )); + } + log_progress(label, bytes.len() as u64, total, started.elapsed()); + update_log(&format!("{label}: download complete")); + Ok(bytes) +} + +fn log_progress(label: &str, received: u64, total: Option, elapsed: Duration) { + let size = match total.filter(|&total| total > 0) { + Some(total) => format!( + "{received}/{total} bytes ({:.1}%)", + received as f64 * 100.0 / total as f64 + ), + None => format!("{received} bytes"), + }; + update_log(&format!( + "{label}: {size}, {:.1}s elapsed, {:.1} KiB/s", + elapsed.as_secs_f64(), + received as f64 / 1024.0 / elapsed.as_secs_f64().max(0.001) + )); } fn read_bounded(reader: impl Read, cap: u64) -> Result> { @@ -506,6 +665,18 @@ fn write_synced(path: &Path, bytes: &[u8]) -> Result<()> { file.sync_all().map_err(|e| e.to_string()) } +fn open_update_lock(root: &Path) -> Result { + let artifacts = root.join("iw4l-artifacts"); + fs::create_dir_all(&artifacts).map_err(|e| e.to_string())?; + OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(artifacts.join("iw4l.update.lock")) + .map_err(|e| e.to_string()) +} + #[derive(Serialize, Deserialize)] struct Job { pid: u32, @@ -532,13 +703,7 @@ fn replace(path: &Path) -> Result<()> { return Err("invalid replacement source".into()); } wait_parent(job.pid, &temp.join("ready"))?; - let lock = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(root.join("iw4l.update.lock")) - .map_err(|e| e.to_string())?; + let lock = open_update_lock(root)?; lock.try_lock_exclusive() .map_err(|_| "another process interrupted replacement")?; if fs::metadata(&job.source).map_err(|e| e.to_string())?.len() != job.size diff --git a/crates/weapon_iw4/src/sprint.rs b/crates/weapon_iw4/src/sprint.rs index 9c8b531..5c015a2 100644 --- a/crates/weapon_iw4/src/sprint.rs +++ b/crates/weapon_iw4/src/sprint.rs @@ -50,7 +50,7 @@ pub(crate) fn weapon_check_hands_for_sprint( } } else if !sprinting && matches!(ws, WeaponState::SprintIn | WeaponState::SprintLoop) { for hand in hands { - begin_sprint_out(hand, facts); + begin_sprint_out(hand, facts, cmd.perks1); } } } @@ -106,12 +106,17 @@ fn sprint_loop(hand: &mut WeaponHandState) { ); } -fn begin_sprint_out(hand: &mut WeaponHandState, facts: &WeaponCombatFacts) { +fn begin_sprint_out(hand: &mut WeaponHandState, facts: &WeaponCombatFacts, perks1: u32) { let time = if facts.sprint_drop_time_ms > 0 { facts.sprint_drop_time_ms } else { 1 }; + let time = if perks1 & playerstate_iw4::PERK1_FASTSPRINTRECOVERY != 0 { + ((time as f32) * playerstate_iw4::SPRINT_RECOVERY_MULTIPLIER) as i32 + } else { + time + }; hand.weaponstate = WeaponState::SprintOut as i32; hand.weapon_time = time; hand.weapon_delay = 0; diff --git a/crates/weapon_iw4/src/tick.rs b/crates/weapon_iw4/src/tick.rs index 0640158..1bceab8 100644 --- a/crates/weapon_iw4/src/tick.rs +++ b/crates/weapon_iw4/src/tick.rs @@ -711,6 +711,7 @@ pub struct WeaponCmd { pub offhand: crate::offhand::OffhandCmd, pub perks0: u32, + pub perks1: u32, pub perk_weap_reload_multiplier: f32, } @@ -747,6 +748,7 @@ impl Default for WeaponCmd { switch_first_raise_time_ms: 0, offhand: crate::offhand::OffhandCmd::default(), perks0: 0, + perks1: 0, perk_weap_reload_multiplier: PERK_WEAP_RELOAD_MULTIPLIER_DEFAULT, } } diff --git a/docs/ENTITIES.md b/docs/ENTITIES.md index 4a3f4d5..c71737c 100644 --- a/docs/ENTITIES.md +++ b/docs/ENTITIES.md @@ -38,10 +38,8 @@ and leaves transport to `net`. * `sim/` — `gentity spawn world snapshot damage bullet* missile item corpse match_state score rules world_objects identities` (`ScriptModelId` is the one identity of a script model; a raw `u32` is not a second one); -* `frame` — `AppScreen`, `HasWorld`, `RuntimeRole`, `LaunchIdentity`; no - plugins, no systems; -* `session` — standing the match up and tearing it down (`MatchInstalled` / - `MatchTornDown`); `net` — wire, deltas, prediction; `bots` — host-only +* `frame` — `AppScreen`, `RuntimeRole`, `LaunchIdentity` and `ScopePlugin`: match/round states, scoped resource registration and entity retirement; +* `session` — standing the match up and tearing it down through `ScopeControl`; `net` — wire, deltas, prediction; `bots` — host-only controllers that observe through a sensor adapter, walk a ClipMap-baked graph and enter the same `TickInput`. diff --git a/docs/GSC-RUNTIME.md b/docs/GSC-RUNTIME.md index 4ed8a27..297bdd6 100644 --- a/docs/GSC-RUNTIME.md +++ b/docs/GSC-RUNTIME.md @@ -139,7 +139,7 @@ Natives are grouped by what they touch; the core three: level settings, and weapon facts from the captured weapon table. Argument reads live in `script/host/args.rs`, array helpers in `script/host/arrays.rs`, -and table lookup in `script/host/tables.rs`. All three use the one `Runtime` +and table lookup in `script/host/tables.rs`. All three use `RoundScript` with explicit access to `MatchScript` (`arrays`, `next_object`, `tables`, `rng`). The compiler is `sim::script::compiler`: `Token`/`lex` and the call, expression, assignment, and statement methods sit in child modules, and `Parser` stays in the parent. @@ -183,7 +183,7 @@ emitting an event. Use Sounds the scripts play (`playLocalSound`, `playSoundToPlayer`, `playSoundToTeam`) reach the client. Effects, rumble and earthquakes validate their receiver and are -kept in `Runtime.presented` or dropped. In-game menus (team, class, escape, leave-game, +kept in `RoundScript.presented` or dropped. In-game menus (team, class, escape, leave-game, scoreboard header) and the IW4L frontend run from menuDefs through the same ordered GPU menu pass. `crates/ui/menus/frontend.json` defines IW4L navigation and native menu styling; frontend service commands connect it to session and master APIs. diff --git a/docs/MAP-LOAD.md b/docs/MAP-LOAD.md index 730a1b9..27d7111 100644 --- a/docs/MAP-LOAD.md +++ b/docs/MAP-LOAD.md @@ -9,7 +9,7 @@ console/ui `map` → session::lifecycle SessionSwapRequest, tear the previous occupancy down, then MapLoadApproved{request_id} → assets::match_load start_match_load onto load_pool() (MatchLoadBusy), poll_match_load → PreparedMatchReady - → session::match_apply apply_prepared_match → scene + SimWorld + MatchInstalled + → session::match_apply prepare install → Staging → OnEnter(Live) → scene + SimWorld ``` Cancellation is `MatchLoadAbort(request_id)`: the walk returns @@ -18,6 +18,27 @@ Failure is `MapLoadFailed`, from discovery *and* install; a request ends once. Startup and later load failures return to the main menu with a map error dialog; OK or Escape dismisses it. The log keeps the full error. +The main world has three match states: `Absent`, `Loading`, and `Live`. +`ScopeControl` resolves requests once in `Last`, with teardown taking priority over +install and install over begin-load. Forced transitions also retire a replaced +`Loading` scope. Load tasks and sound-bank composition belong to `Loading`; +prepared catalogs, simulation, animation, scene plans and replay buffers belong +to `Live`. Fallible preparation completes before requesting `Live`. + +`OnEnter` installs resources in registration order, derives publications, then +announces the installed identity. `WorldGeneration` is a nonzero scoped resource; +`WorldStamp` is its copied value in diagnostics and render frames. On exit, +release systems and entity removal hooks run while resources still exist. The +transition schedule then removes registered resources in reverse order and hands +them to `Retiring`. Match systems use `InMatch` on their main/fixed schedules; +render extraction reads optional main-world resources and clears missing frames. + +`RoundPhase` exists only under `Live`. A changed server round serial forces a +fresh `Playing` scope even when the phase stays `Playing`. Simulation separately +owns `MatchScript` and `RoundScript`; only `RoundScript::new(&MatchScript)` creates +round state. Frontend menu locals and menu stacks have app owners separate from +their scoped in-match equivalents. + ## The walk: `assets::session_load::load_prepared_match` The single entry point; the composition root sees no `fastfile_*` and no diff --git a/docs/PRESENTATION.md b/docs/PRESENTATION.md index a9bff52..53c0e11 100644 --- a/docs/PRESENTATION.md +++ b/docs/PRESENTATION.md @@ -30,11 +30,11 @@ legal. Late delivery after expiry may create a fresh product. `fx_dump` reports scope/time, product budget refusals, receipt size/capacity and sweep costs. `asset_material::UiImagePublication` owns material mappings, preview policy, -zone payloads and archive indices. Preparation returns private products; -shell/match installation selects the active owner. A late shell completion -cannot replace an installed match. HUD handle/RGBA caches and asynchronous -class/loading previews adopt publication identity. Menu overlays replace their -own maps and caches when the menu resource changes. +zone payloads and archive indices. Shell/match installation selects privately +prepared products. A late shell completion cannot replace an installed match. +HUD caches and class/loading previews adopt publication identity. Shell previews +use this publication; gameplay archive image plans begin with the first match +and remain shared. Menu overlays replace maps and caches when menus change. Installation revisions cover relevant file metadata, following game symlinks. Unix revisions also include inode/change time. A new load checks the revision; diff --git a/docs/RUN.md b/docs/RUN.md index dda104e..24ae80c 100644 --- a/docs/RUN.md +++ b/docs/RUN.md @@ -115,5 +115,21 @@ before level entry scripts start. Player-data methods read and write the bound s external scripts block persistent-data writes. IW4 remote admission exchanges a signed profile before gameplay; host updates save locally before acknowledgement. Installed multiplayer validation and schema migration remain incomplete. + +At the main menu, `unlock all` saves the maximum shared level and completes the +native challenges, including all Pro perk requirements. `unlock reset` saves +level 1, clears prestige, and restores initial challenge progress. Both preserve +account identity, custom classes and other statistics. Disconnect before using +these commands; active matches, loading and replay refuse progression edits. +The console offers `all` and `reset` as argument completions. Unlocking progression +does not supply missing weapon assets or enable unsupported content. + Older account files upgrade while retaining stats; conflicting disk revisions refuse save instead of overwriting another writer. + +On a local match host with cheats, `round_restart` runs the script round restart +while retaining prepared map content and round-persistent state. `map_restart` +creates a new match on the current map. + +For console scripts that edit progression as the menu starts, use +`wait progression; unlock all` to wait for native account content. diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md index 5d2ce8d..fa96e5c 100644 --- a/docs/WINDOWS.md +++ b/docs/WINDOWS.md @@ -34,8 +34,13 @@ restores the previous executable. There is no permanent second executable. The temporary files are `iw4l.update.exe`, `iw4l.previous.exe` and a helper in `%TEMP%`; the next verified startup removes the backup and helper. The lock file -`iw4l.update.lock` serializes update checks; `iw4l.update-helper` records cleanup. -An update error stops startup and reports the failure; a configured community requires a reachable update origin. +`iw4l-artifacts/iw4l.update.lock` serializes update checks; the artifacts directory +is created as needed beside the executable. `iw4l.update-helper` records cleanup. +Update checks log DNS/TLS settings, HTTP results, download bytes/percentage/speed, +verification and handoff steps to the console and `iw4l-artifacts/logs/`. +If checking or downloading fails, the reason is logged and the installed game +starts with the selected community. Online play still needs a reachable master +and a compatible version. DNS/connect waits are limited to 8s; each HTTP request to 180s. Without a community descriptor, local development can launch without checking. A descriptor pins a public CA and the master's TLS name for both HTTPS and QUIC. diff --git a/xtask/src/publish.rs b/xtask/src/publish.rs index 608ea10..effa5d4 100644 --- a/xtask/src/publish.rs +++ b/xtask/src/publish.rs @@ -15,7 +15,7 @@ use serde_json::Value; use crate::dotenv::Env; use crate::release::{file_sha256, release_dir, sha256_hex}; use crate::server::{Descriptor, Remote, root_dir}; -use crate::shell::{Res, Ssh, capture, require_tools, run}; +use crate::shell::{Res, Ssh, Step, capture, require_tools, run}; /// How long the restarted master gets to answer `status` with the protocol the /// release was built against, before its predecessor is put back. @@ -150,13 +150,35 @@ fn preflight(servers: &[Descriptor]) -> Res<()> { require_tools(&["curl", "rsync", "ssh"])?; for server in servers { let ssh = server.ssh()?; + let step = Step::start( + "preflight.ssh", + &format!("server={} connecting to {}", server.name(), ssh.target()), + ); ssh.run("true") .map_err(|error| format!("ssh {} is not usable: {error}", ssh.target()))?; - Remote::find(&ssh, server.port()?)?; + step.done(&format!("connected to {}", ssh.target())); + discover_service(&ssh, server.port()?)?; } Ok(()) } +fn discover_service(ssh: &Ssh, port: u16) -> Res { + let step = Step::start( + "discover.service", + &format!("host={} port={port}", ssh.host()), + ); + let remote = Remote::find(ssh, port)?; + step.done(&format!( + "found service={} host={} port={} bin={} updates={}", + remote.unit, + ssh.host(), + remote.port, + remote.bin, + remote.updates, + )); + Ok(remote) +} + pub fn run_cli(root: &Path, args: &[String]) -> Res<()> { let dir = release_dir(root)?; let servers = Descriptor::select(Descriptor::all_in(&dir.join("client"))?, args)?; @@ -225,10 +247,14 @@ fn publish(dir: &Path, server: &Descriptor) -> Res<()> { let release_id = str_field(&deployment, "id", &descriptor_path)?; let protocol = u64_field(&deployment, "protocol", &descriptor_path)?; let ssh = &server.ssh()?; - let remote = Remote::find(ssh, server.port()?)?; + let remote = discover_service(ssh, server.port()?)?; println!("publish: {} unit={}", server.summary(), remote.unit); let rows = rows_of(&deployment, &remote, &descriptor_path)?; + let step = Step::start( + "verify.local", + &format!("release={release_id} files={}", rows.len()), + ); for row in &rows { let local = dir.join(&row.local); if !local.is_file() { @@ -247,8 +273,13 @@ fn publish(dir: &Path, server: &Descriptor) -> Res<()> { if u64::from(manifest.protocol) != protocol { return Err("client/master protocol mismatch inside the release".to_string()); } + step.done(&format!("sha256 verified protocol={protocol}")); let lib = &remote.lib; + let step = Step::start( + "verify.ca", + &format!("host={} path={}", ssh.host(), remote.ca()), + ); let installed_ca = ssh .capture(&format!("sha256sum '{}' | cut -d' ' -f1", remote.ca()))? .trim() @@ -261,24 +292,36 @@ fn publish(dir: &Path, server: &Descriptor) -> Res<()> { ssh.host(), )); } + step.done("installed CA matches community descriptor"); let master_local = dir.join("server/iw4l-master"); let master_sha = file_sha256(&master_local)?; let staging = format!("{lib}/staging/{release_id}"); let lock_dir = format!("{lib}/publish.lock"); + println!( + "publish.lock: acquiring host={} path={lock_dir}", + ssh.host() + ); if !ssh.try_run(&format!("mkdir '{lock_dir}'"))? { return Err(format!("publish: {} already being published", remote.unit)); } let _lock = Lock { ssh, dir: lock_dir }; + println!("publish.lock: acquired"); ssh.run(&format!( "install -d -m 0755 '{staging}' '{lib}/manifests' '{lib}/masters/{master_sha}' '{updates}'", updates = remote.updates, ))?; + println!("publish.staging: ready host={} path={staging}", ssh.host()); let inventory_started = Instant::now(); let finals: Vec = rows.iter().map(|row| row.remote.clone()).collect(); + let step = Step::start( + "inventory.remote", + &format!("host={} files={}", ssh.host(), finals.len()), + ); let inventory = remote_inventory(ssh, &finals)?; + step.done("remote sha256 inventory collected"); let mut need = Vec::new(); let (mut upload_bytes, mut skipped) = (0_u64, 0_usize); @@ -304,6 +347,12 @@ fn publish(dir: &Path, server: &Descriptor) -> Res<()> { if !need.is_empty() { for row in &need { let remote = format!("{staging}/{}", row.local); + println!( + "upload.file: {} -> {}:{remote} bytes={}", + row.local, + ssh.host(), + row.size, + ); let parent = remote.rsplit_once('/').map_or("", |(head, _)| head); ssh.run(&format!("install -d -m 0755 '{parent}'"))?; ssh.rsync( @@ -316,12 +365,14 @@ fn publish(dir: &Path, server: &Descriptor) -> Res<()> { .iter() .map(|row| format!("{staging}/{}", row.local)) .collect(); + let step = Step::start("verify.staged", &format!("files={}", staged.len())); let landed = remote_inventory(ssh, &staged)?; for (row, path) in need.iter().zip(&staged) { if lookup(&landed, path) != row.sha256 { return Err(format!("staged sha256 mismatch: {path}")); } } + step.done("uploaded sha256 hashes match release"); } println!( "upload: done elapsed={}s", @@ -339,7 +390,15 @@ fn publish(dir: &Path, server: &Descriptor) -> Res<()> { }) .collect(); if !promote.is_empty() { + let step = Step::start( + "promote.files", + &format!( + "files={}", + need.iter().filter(|row| !row.is_manifest()).count() + ), + ); ssh.feed(REMOTE_PROMOTE, &promote)?; + step.done("staged files moved into place"); } let master = MasterSwitch { @@ -363,6 +422,7 @@ fn publish(dir: &Path, server: &Descriptor) -> Res<()> { let previous = activate_manifest(ssh, &remote, dir, &staging, &release_id)?; let ca_pem = CaFile::write(server)?; let updates = dir.join("server/updates"); + println!("verify.served: start release={release_id}"); if let Err(error) = verify_served(server, &ca_pem.0, &updates, &manifest.file.path) { let restored = match &previous { Some(previous) => restore_manifest(ssh, &remote, previous).map_or_else( @@ -385,7 +445,10 @@ fn publish(dir: &Path, server: &Descriptor) -> Res<()> { port = remote.port, url = server.community.updates.url, ); - ssh.run(&format!("rm -rf '{staging}'")) + let step = Step::start("publish.cleanup", &format!("path={staging}")); + ssh.run(&format!("rm -rf '{staging}'"))?; + step.done("staging removed"); + Ok(()) } struct MasterSwitch<'a> { @@ -573,10 +636,12 @@ fn verify_served( let result = (|| { for (name, timeout) in [("manifest.toml", "10"), (blob, "300")] { let got = scratch.join(name); + println!("verify.served: fetching {name} from {}", community.host()); fetch(community, ca_pem, name, timeout, &got)?; if file_sha256(&got)? != file_sha256(&local_updates.join(name))? { return Err(format!("served {name} does not match the prepared release")); } + println!("verify.served: sha256 verified {name}"); } Ok(()) })(); diff --git a/xtask/src/publish_check.rs b/xtask/src/publish_check.rs index 1d137e2..e1cfdb7 100644 --- a/xtask/src/publish_check.rs +++ b/xtask/src/publish_check.rs @@ -453,12 +453,79 @@ fn looks_binary(bytes: &[u8]) -> bool { bytes.iter().take(8000).any(|b| *b == 0) } +fn scoped_contracts(root: &Path, paths: &[PathBuf]) -> Res> { + let mut sources = Vec::new(); + let mut types = std::collections::BTreeSet::new(); + for path in paths.iter().filter(|path| { + path.extension().is_some_and(|ext| ext == "rs") && path.starts_with("crates") + }) { + let text = std::fs::read_to_string(root.join(path)) + .map_err(|error| format!("{}: {error}", path.display()))?; + for spelling in [".scoped::<", ".staged::<"] { + for tail in text.split(spelling).skip(1) { + if let Some((name, _)) = tail.split_once('>') { + types.insert(name.rsplit("::").next().unwrap_or(name).trim().to_owned()); + } + } + } + sources.push((path, text)); + } + let mut findings = Vec::new(); + for (path, text) in sources { + if path == Path::new("crates/frame/src/scope.rs") + || path == Path::new("crates/net/src/role_matrix.rs") + { + continue; + } + for (index, line) in text.lines().enumerate() { + let compact = line.split_whitespace().collect::(); + let legacy = [ + "MatchTornDown", + "ReturnedToMenu", + "HasWorld", + "frame::lifetime", + ] + .iter() + .any(|name| compact.contains(name)); + let bypass = types.iter().any(|name| { + compact.contains(&format!(".init_resource::<{name}>")) + || compact.contains(&format!(".insert_resource({name}{{")) + || compact.contains(&format!(".insert_resource({name}::")) + || compact.contains(&format!("If")) + || compact.contains(&format!("If")) + }); + let state_writer = path != Path::new("crates/session/src/scope_control.rs") + && [ + "NextState", + "NextState", + "NextState", + "NextState", + ] + .iter() + .any(|name| compact.contains(name)); + if legacy || bypass || state_writer { + findings.push(format!( + "{}:{}: scoped lifetime bypass", + path.display(), + index + 1 + )); + } + } + } + Ok(findings) +} + pub fn run_cli(root: &Path) -> Res<()> { let mut leaks = Vec::new(); let mut offsets = Vec::new(); let mut tests = Vec::new(); + let paths: Vec<_> = tracked(root)? + .into_iter() + .filter(|path| root.join(path).exists()) + .collect(); + let mut contracts = scoped_contracts(root, &paths)?; let mut scanned = 0usize; - for path in tracked(root)? { + for path in paths { let rel = path.to_string_lossy().replace('\\', "/"); // The leak scan runs on every tracked path, exemptions included: a file // is exempt from being read for offsets, never from being here at all. @@ -479,6 +546,16 @@ pub fn run_cli(root: &Path) -> Res<()> { } // Not every text file is valid UTF-8; what is in one still counts. let text = String::from_utf8_lossy(&bytes); + if rel.starts_with("crates/render_anim/src/") { + for (line, source) in text.lines().enumerate() { + if source.contains(".init_resource::<") { + contracts.push(format!( + "{rel}:{}: animation resource must declare its lifetime", + line + 1 + )); + } + } + } if let Some((line, what)) = stray_test(&rel, &text) { tests.push(Finding { path: path.clone(), @@ -531,6 +608,9 @@ pub fn run_cli(root: &Path) -> Res<()> { finding.text ); } + if !contracts.is_empty() { + return Err(contracts.join("\n")); + } if !leaks.is_empty() { return Err(format!( "{} file(s) that are not the product are staged for publication; \ diff --git a/xtask/src/server.rs b/xtask/src/server.rs index fe275e5..aceb411 100644 --- a/xtask/src/server.rs +++ b/xtask/src/server.rs @@ -147,6 +147,7 @@ impl Remote { pub fn find(ssh: &Ssh, port: u16) -> Res { let found = ssh.capture(&format!( "for f in /etc/systemd/system/*.service; do + [ -f \"$f\" ] || continue line=$(grep -m1 '^ExecStart=.* serve --bind [^ ]*:{port} ' \"$f\" || true) [ -z \"$line\" ] || printf '%s\\t%s\\n' \"$(basename \"$f\")\" \"${{line#ExecStart=}}\" done"