Blacksite/crates/game/src/lib.rs
Rbanh 0798aa5d57 Build renderer and material component foundations
Add dedicated skinned rendering, pose restoration, shared Material and Material Instance slots, registry-driven components, Surface/Solari integration, transactional schema upgrades, navigation authoring, documentation, and evaluation evidence.
2026-07-12 00:24:06 -04:00

264 lines
9.0 KiB
Rust

//! Runtime game crate: FPS player, rendering, and starter world.
#![allow(clippy::type_complexity)]
#![cfg_attr(
feature = "hot-reload",
allow(
clippy::too_many_arguments,
reason = "hot-lib-reloader wrappers preserve Bevy system signatures and macro spans"
)
)]
pub mod animation;
pub mod audio;
pub mod editor_ext;
pub mod launch;
pub mod navigation;
mod scene_bootstrap;
mod schema_world_loader;
pub use scene_bootstrap::ProjectDefaultLevelRoot;
pub mod player {
//! Player types re-exported from the stable sim crate.
pub use sim::{CameraSensitivity, PlayerCamera};
}
pub mod rendering {
//! Rendering helpers re-exported from the hot-reloadable implementation.
pub use game_hot::{
apply_camera_render_profile, apply_project_camera_fx, camera_auto_exposure_active,
clear_viewport_camera_stack, effective_gi_path_for_camera, effective_viewport_gi_path,
effective_viewport_render_stack, has_local_shadow_lights, hdr_enabled_profile,
patch_camera_effects, resolve_viewport_camera_owner, strip_project_camera_fx,
sync_optional_rendering_fx, sync_project_atmosphere, sync_viewport_camera_stack,
world_has_local_shadow_lights, FullscreenEffectsPlugin, ProjectAtmosphere,
SolariRaytracingSceneStats, SolariRenderingPlugin, ViewportCameraOwner, ViewportFxSnapshot,
ViewportStackApply, HDR_ENV_VAR,
};
}
use bevy::prelude::*;
use settings::ProjectSettingsPlugin;
use sim::SimPlugin;
pub use sim::{GameInputEnabled, GameInputFocused, SimEnabled};
/// When set, the in-process editor owns [`apply_project_camera_fx`] on a single camera.
#[derive(Resource, Debug, Clone, Copy, Default)]
pub struct GameRenderBootstrap {
pub defer_player_camera_fx: bool,
}
/// Scene-startup controls for a host that embeds [`GamePlugin`].
#[derive(Resource, Debug, Clone, Copy, Default)]
pub struct GameSceneBootstrap {
/// The host owns initial scene materialization instead of the standalone runtime.
pub defer_default_level: bool,
}
#[cfg(not(feature = "hot-reload"))]
mod systems {
pub use game_hot::{
apply_ambient_light, apply_player_intent, grab_cursor, move_and_slide,
setup_project_camera_effects, spawn_level, spawn_player, spawn_sun,
sync_player_physics_position, sync_project_sun_visibility,
sync_scene_directional_shadow_cascades, tag_player_camera, toggle_cursor_grab,
update_camera_pitch, update_eye_height, update_local_player_intent,
};
}
#[cfg(feature = "hot-reload")]
mod systems {
pub use crate::hot::{
apply_ambient_light, apply_player_intent, grab_cursor, move_and_slide,
setup_project_camera_effects, spawn_level, spawn_player, spawn_sun,
sync_player_physics_position, sync_project_sun_visibility,
sync_scene_directional_shadow_cascades, tag_player_camera, toggle_cursor_grab,
update_camera_pitch, update_eye_height, update_local_player_intent,
};
}
#[cfg(feature = "hot-reload")]
#[hot_lib_reloader::hot_module(dylib = "game_hot", lib_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/debug"))]
mod hot {
use avian3d::prelude::*;
use bevy::input::mouse::AccumulatedMouseMotion;
use bevy::light::atmosphere::ScatteringMedium;
use bevy::light::CascadeShadowConfig;
use bevy::prelude::*;
use bevy::time::Fixed;
use bevy::window::{CursorOptions, PrimaryWindow};
use game_hot::{ProjectAtmosphere, SolariRaytracingSceneStats};
use protocol::PlayerInputIntent;
use settings::{
ActiveCameraRenderProfile, ProjectRenderCamera, ProjectSettings, RenderingCapabilities,
SimTuning,
};
use shared::{InspectorOrder, LevelObject, LightDesc, ProjectSun};
use sim::{
CameraSensitivity, Crouching, GameInputEnabled, GameInputFocused, Grounded, JumpState,
Player, PlayerCamera, PlayerVelocity, PlayerYawSensitivity,
};
hot_functions_from_file!("crates/game_hot/src/sim_systems.rs"); // move_and_slide, sync_player_physics_position, apply_player_intent
hot_functions_from_file!("crates/game_hot/src/player/spawn.rs");
hot_functions_from_file!("crates/game_hot/src/player/input.rs");
hot_functions_from_file!("crates/game_hot/src/player/camera.rs");
hot_functions_from_file!("crates/game_hot/src/rendering/camera_fx.rs");
hot_functions_from_file!("crates/game_hot/src/world/mod.rs");
hot_functions_from_file!("crates/game_hot/src/world/level.rs");
hot_functions_from_file!("crates/game_hot/src/world/lighting.rs");
#[lib_change_subscription]
pub fn subscribe() -> hot_lib_reloader::LibReloadObserver {}
}
#[cfg(feature = "hot-reload")]
pub fn hot_reload_subscribe() -> hot_lib_reloader::LibReloadObserver {
hot::subscribe()
}
fn sim_enabled(enabled: Res<SimEnabled>) -> bool {
enabled.0
}
fn game_input_enabled(enabled: Res<GameInputEnabled>) -> bool {
enabled.0
}
fn register_level_bootstrap(app: &mut App, defer_default_level: bool) {
if defer_default_level {
// SceneIo owns editor startup and retains the generated arena as its
// failure fallback until the authored document is materialized.
app.add_systems(Startup, systems::spawn_level);
} else {
app.add_systems(Startup, scene_bootstrap::spawn_project_default_level);
}
}
fn register_game_systems(
app: &mut App,
render_bootstrap: GameRenderBootstrap,
scene_bootstrap: GameSceneBootstrap,
) {
app.add_systems(
FixedUpdate,
(
systems::apply_player_intent,
systems::move_and_slide,
systems::sync_player_physics_position,
)
.chain()
.run_if(sim_enabled),
)
.add_systems(Startup, systems::spawn_player)
.add_systems(
Startup,
systems::grab_cursor
.after(systems::spawn_player)
.run_if(game_input_enabled),
)
.add_systems(
Update,
(
systems::toggle_cursor_grab,
systems::update_local_player_intent,
systems::update_camera_pitch,
systems::update_eye_height,
)
.chain()
.run_if(game_input_enabled),
)
.add_systems(
Startup,
(systems::apply_ambient_light, systems::spawn_sun).chain(),
);
register_level_bootstrap(app, scene_bootstrap.defer_default_level);
app.add_systems(
Update,
(
systems::sync_project_sun_visibility,
systems::sync_scene_directional_shadow_cascades,
),
);
if !render_bootstrap.defer_player_camera_fx {
app.add_systems(
PostStartup,
(
systems::tag_player_camera,
systems::setup_project_camera_effects,
)
.chain(),
);
}
}
/// Runtime game systems and content, without Bevy's platform/window plugins.
pub struct GamePlugin;
impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
let render_bootstrap = app
.world()
.get_resource::<GameRenderBootstrap>()
.copied()
.unwrap_or_default();
let scene_bootstrap = app
.world()
.get_resource::<GameSceneBootstrap>()
.copied()
.unwrap_or_default();
app.register_type::<GameInputEnabled>()
.init_resource::<GameInputEnabled>()
.init_resource::<GameInputFocused>()
.add_plugins(schema_world_loader::SchemaWorldAssetPlugin)
.add_plugins((ProjectSettingsPlugin, SimPlugin, bevy_ufbx::FbxPlugin))
.add_plugins((audio::GameAudioPlugin, animation::GameAnimationPlugin))
.add_plugins(game_hot::SolariRenderingPlugin)
.add_plugins(game_hot::AutoExposureRenderingPlugin)
.add_plugins(game_hot::FullscreenEffectsPlugin)
.insert_resource(ClearColor(Color::srgb(0.0, 0.0, 0.0)));
register_game_systems(app, render_bootstrap, scene_bootstrap);
}
}
/// Called by the editor after swapping the hot dylib.
pub fn on_hot_reload(world: &mut World) {
game_hot::on_hot_reload(world);
}
#[cfg(test)]
mod startup_tests {
use super::*;
use bevy::asset::AssetPlugin;
use bevy::world_serialization::WorldSerializationPlugin;
#[test]
fn deferred_bootstrap_leaves_default_level_loading_to_the_host() {
let mut app = App::new();
app.add_plugins((
MinimalPlugins,
AssetPlugin::default(),
WorldSerializationPlugin,
));
app.insert_resource(settings::ProjectSettings::default());
register_level_bootstrap(&mut app, true);
app.update();
let world = app.world_mut();
let mut runtime_roots =
world.query_filtered::<Entity, With<scene_bootstrap::ProjectDefaultLevelRoot>>();
assert_eq!(runtime_roots.iter(world).count(), 0);
let mut fallback = world.query_filtered::<Entity, With<shared::LevelObject>>();
assert!(
fallback.iter(world).next().is_some(),
"the editor fallback arena remains available until SceneIo loads"
);
}
}