nightshade 0.43.0

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::world::World;

pub type SystemFn = fn(&mut World);

#[derive(Clone)]
pub struct FrameScheduleEntry {
    pub name: &'static str,
    pub system: SystemFn,
}

#[derive(Clone, Default)]
pub struct FrameSchedule {
    pub entries: Vec<FrameScheduleEntry>,
}

#[derive(Clone, Default)]
pub struct Schedules {
    pub frame: FrameSchedule,
    pub retained_ui: FrameSchedule,
}

pub fn schedule_push(schedule: &mut FrameSchedule, name: &'static str, system: SystemFn) {
    schedule.entries.push(FrameScheduleEntry { name, system });
}

pub fn schedule_insert_before(
    schedule: &mut FrameSchedule,
    target: &str,
    name: &'static str,
    system: SystemFn,
) {
    let position = schedule
        .entries
        .iter()
        .position(|entry| entry.name == target)
        .unwrap_or_else(|| panic!("schedule_insert_before: no system named \"{target}\""));
    schedule
        .entries
        .insert(position, FrameScheduleEntry { name, system });
}

pub fn schedule_insert_after(
    schedule: &mut FrameSchedule,
    target: &str,
    name: &'static str,
    system: SystemFn,
) {
    let position = schedule
        .entries
        .iter()
        .position(|entry| entry.name == target)
        .unwrap_or_else(|| panic!("schedule_insert_after: no system named \"{target}\""))
        + 1;
    schedule
        .entries
        .insert(position, FrameScheduleEntry { name, system });
}

pub fn schedule_remove(schedule: &mut FrameSchedule, name: &str) {
    schedule.entries.retain(|entry| entry.name != name);
}

pub fn schedule_contains(schedule: &FrameSchedule, name: &str) -> bool {
    schedule.entries.iter().any(|entry| entry.name == name)
}

pub fn schedule_run(schedule: &FrameSchedule, world: &mut World) {
    let _span = tracing::info_span!("systems").entered();
    for entry in &schedule.entries {
        (entry.system)(world);
    }
}

pub mod system_names {
    pub const INITIALIZE_AUDIO: &str = "initialize_audio";
    pub const BUILD_AUDIO_BUSES: &str = "build_audio_buses";
    pub const UPDATE_AUDIO: &str = "update_audio";
    #[cfg(target_arch = "wasm32")]
    pub const SYNC_WASM_CANVAS_SIZE: &str = "sync_wasm_canvas_size";
    pub const ENSURE_BOUNDING_VOLUMES: &str = "ensure_bounding_volumes";
    pub const RUN_PHYSICS: &str = "run_physics";
    pub const UPDATE_ANIMATION_PLAYERS: &str = "update_animation_players";
    pub const APPLY_ANIMATIONS: &str = "apply_animations";
    pub const UPDATE_TWEENS: &str = "update_tweens";
    pub const TRANSFORM_SYSTEMS: &str = "transform_systems";
    pub const ASSIGN_SHADOW_ATLAS: &str = "assign_shadow_atlas";
    pub const UPDATE_INSTANCED_MESH_CACHES: &str = "update_instanced_mesh_caches";
    pub const RETAINED_UI: &str = "retained_ui";
    #[cfg(feature = "gizmos")]
    pub const GIZMO_OVERLAY: &str = "gizmo_overlay";
    #[cfg(feature = "gizmos")]
    pub const NAV_GIZMO_OVERLAY: &str = "nav_gizmo_overlay";
    pub const APPLY_IME_ALLOWED: &str = "apply_ime_allowed";
    pub const RESET_MOUSE: &str = "reset_mouse";
    pub const RESET_KEYBOARD: &str = "reset_keyboard";
    pub const RESET_TOUCH: &str = "reset_touch";
    pub const PROCESS_COMMANDS: &str = "process_commands";
    pub const CLEANUP_UNUSED_RESOURCES: &str = "cleanup_unused_resources";
    pub const RUN_NAVMESH: &str = "run_navmesh";
    pub const POLL_FILE_WATCHER: &str = "poll_file_watcher";
    pub const POLL_ASSET_WATCHER: &str = "poll_asset_watcher";
    pub const UPDATE_PARTICLE_EMITTERS: &str = "update_particle_emitters";
    pub const SYNC_CLOTH_MESHES: &str = "sync_cloth_meshes";
}

pub mod retained_ui_system_names {
    pub const INPUT_SYNC: &str = "ui_retained_input_sync";
    pub const GAMEPAD_NAVIGATION: &str = "ui_gamepad_navigation";
    pub const LAYOUT_PICKING: &str = "ui_layout_picking";
    pub const WIDGET_INTERACTION: &str = "ui_widget_interaction";
    pub const EVENT_BUBBLE: &str = "ui_event_bubble";
    pub const EMIT_CLICK_EVENTS: &str = "ui_emit_click_events";
    pub const LAYOUT_STATE_UPDATE: &str = "ui_layout_state_update";
    pub const DOCKED_PANEL_LAYOUT: &str = "ui_docked_panel_layout";
    pub const RESPONSIVE_APPLY: &str = "ui_responsive_apply";
    pub const LAYOUT_COMPUTE: &str = "ui_layout_compute";
    pub const THEME_TRANSITION_TICK: &str = "ui_theme_transition_tick";
    pub const THEME_APPLY: &str = "ui_theme_apply";
    pub const LAYOUT_COLOR_BLEND: &str = "ui_layout_color_blend";
    pub const LAYOUT_TRANSFORM_BLEND: &str = "ui_layout_transform_blend";
    pub const LAYOUT_RENDER_SYNC: &str = "ui_layout_render_sync";
}

pub fn build_default_frame_schedule() -> FrameSchedule {
    let entries: &[(&str, SystemFn)] = &[
        #[cfg(feature = "audio")]
        (
            system_names::INITIALIZE_AUDIO,
            crate::ecs::audio::systems::initialize_audio_system,
        ),
        #[cfg(feature = "audio")]
        (
            system_names::BUILD_AUDIO_BUSES,
            crate::ecs::audio::systems::build_audio_buses_system,
        ),
        #[cfg(feature = "audio")]
        (
            system_names::UPDATE_AUDIO,
            crate::ecs::audio::systems::update_audio_system,
        ),
        #[cfg(target_arch = "wasm32")]
        (
            system_names::SYNC_WASM_CANVAS_SIZE,
            crate::ecs::camera::systems::sync_wasm_canvas_size,
        ),
        (
            system_names::ENSURE_BOUNDING_VOLUMES,
            crate::ecs::bounding_volume::systems::ensure_bounding_volumes,
        ),
        #[cfg(feature = "physics")]
        (
            system_names::RUN_PHYSICS,
            crate::ecs::physics::systems::run_physics_systems,
        ),
        (
            system_names::UPDATE_ANIMATION_PLAYERS,
            crate::ecs::animation::systems::update_animation_players,
        ),
        (
            system_names::APPLY_ANIMATIONS,
            crate::ecs::animation::systems::apply_animations,
        ),
        (
            system_names::UPDATE_TWEENS,
            crate::ecs::tween::update_tweens_system,
        ),
        #[cfg(feature = "navmesh")]
        (
            system_names::RUN_NAVMESH,
            crate::ecs::navmesh::systems::run_navmesh_systems,
        ),
        (
            system_names::UPDATE_PARTICLE_EMITTERS,
            crate::ecs::particles::systems::update_particle_emitters,
        ),
        (
            system_names::SYNC_CLOTH_MESHES,
            crate::ecs::cloth::systems::sync_cloth_meshes_system,
        ),
        (
            system_names::TRANSFORM_SYSTEMS,
            crate::ecs::transform::systems::run_systems,
        ),
        (
            system_names::ASSIGN_SHADOW_ATLAS,
            crate::render::wgpu::passes::shadow_depth::atlas::assign_spotlight_shadow_atlas_system,
        ),
        (
            system_names::UPDATE_INSTANCED_MESH_CACHES,
            crate::ecs::mesh::systems::update_instanced_mesh_caches_system,
        ),
        (system_names::RETAINED_UI, run_retained_ui_schedule),
        #[cfg(feature = "gizmos")]
        (
            system_names::GIZMO_OVERLAY,
            crate::ecs::gizmos::gizmo_overlay_system,
        ),
        #[cfg(feature = "gizmos")]
        (
            system_names::NAV_GIZMO_OVERLAY,
            crate::ecs::gizmos::nav_gizmo_overlay_system,
        ),
        (
            system_names::APPLY_IME_ALLOWED,
            crate::ecs::input::systems::apply_ime_allowed_system,
        ),
        (
            system_names::RESET_MOUSE,
            crate::ecs::input::systems::reset_mouse_system,
        ),
        (
            system_names::RESET_KEYBOARD,
            crate::ecs::input::systems::reset_keyboard_system,
        ),
        (
            system_names::RESET_TOUCH,
            crate::ecs::input::systems::reset_touch_system,
        ),
        #[cfg(all(feature = "file_watcher", not(target_arch = "wasm32")))]
        (
            system_names::POLL_FILE_WATCHER,
            crate::ecs::file_watcher::poll_file_watcher_system,
        ),
        #[cfg(all(
            feature = "assets",
            feature = "file_watcher",
            not(target_arch = "wasm32")
        ))]
        (
            system_names::POLL_ASSET_WATCHER,
            crate::ecs::asset_watcher::poll_asset_watcher_system,
        ),
        (
            system_names::PROCESS_COMMANDS,
            crate::ecs::world::process_commands_system,
        ),
        (
            system_names::CLEANUP_UNUSED_RESOURCES,
            crate::ecs::world::cleanup_unused_resources_system,
        ),
    ];

    let mut schedule = FrameSchedule::default();
    for &(name, system) in entries {
        schedule_push(&mut schedule, name, system);
    }
    schedule
}

pub fn build_default_retained_ui_schedule() -> FrameSchedule {
    // Gamepad navigation has to run after picking. Picking resets every
    // entity's `interaction.clicked` flag at the top of the frame, so if
    // gamepad nav set `clicked = true` for the focused entity before
    // picking, the activation would be wiped before any widget or click
    // emitter could see it. Running it after picking lets the gamepad's
    // South-press flag survive into widget interaction and click event
    // emission.
    let entries: &[(&str, SystemFn)] = &[
        (
            retained_ui_system_names::INPUT_SYNC,
            crate::ecs::ui::widget_systems::ui_retained_input_sync_system,
        ),
        (
            retained_ui_system_names::LAYOUT_PICKING,
            crate::ecs::ui::picking::ui_layout_picking_system,
        ),
        #[cfg(feature = "gamepad")]
        (
            retained_ui_system_names::GAMEPAD_NAVIGATION,
            crate::ecs::ui::gamepad_navigation::ui_gamepad_navigation_system,
        ),
        (
            retained_ui_system_names::WIDGET_INTERACTION,
            crate::ecs::ui::widget_systems::ui_widget_interaction_system,
        ),
        (
            retained_ui_system_names::EVENT_BUBBLE,
            crate::ecs::ui::widget_systems::ui_event_bubble_system,
        ),
        (
            retained_ui_system_names::EMIT_CLICK_EVENTS,
            crate::ecs::ui::widget_systems::ui_emit_click_events_system,
        ),
        (
            retained_ui_system_names::LAYOUT_STATE_UPDATE,
            crate::ecs::ui::systems::ui_layout_state_update_system,
        ),
        (
            retained_ui_system_names::DOCKED_PANEL_LAYOUT,
            crate::ecs::ui::systems::ui_docked_panel_layout_system,
        ),
        (
            retained_ui_system_names::RESPONSIVE_APPLY,
            crate::ecs::ui::systems::ui_responsive_apply_system,
        ),
        (
            retained_ui_system_names::LAYOUT_COMPUTE,
            crate::ecs::ui::systems::ui_layout_compute_system,
        ),
        (
            retained_ui_system_names::THEME_TRANSITION_TICK,
            crate::ecs::ui::systems::ui_theme_transition_tick_system,
        ),
        (
            retained_ui_system_names::THEME_APPLY,
            crate::ecs::ui::systems::ui_theme_apply_system,
        ),
        (
            retained_ui_system_names::LAYOUT_COLOR_BLEND,
            crate::ecs::ui::systems::ui_layout_color_blend_system,
        ),
        (
            retained_ui_system_names::LAYOUT_TRANSFORM_BLEND,
            crate::ecs::ui::systems::ui_layout_transform_blend_system,
        ),
        (
            retained_ui_system_names::LAYOUT_RENDER_SYNC,
            crate::ecs::ui::render_sync::ui_layout_render_sync_system,
        ),
    ];

    let mut schedule = FrameSchedule::default();
    for &(name, system) in entries {
        schedule_push(&mut schedule, name, system);
    }
    schedule
}

pub fn run_retained_ui_schedule(world: &mut World) {
    let schedule = std::mem::take(&mut world.resources.schedules.retained_ui);
    schedule_run(&schedule, world);
    world.resources.schedules.retained_ui = schedule;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn frame_schedule_contains_one_retained_ui_entry() {
        let schedule = build_default_frame_schedule();
        let count = schedule
            .entries
            .iter()
            .filter(|entry| entry.name == system_names::RETAINED_UI)
            .count();
        assert_eq!(
            count, 1,
            "frame schedule must collapse retained UI into a single entry"
        );
    }

    #[test]
    fn retained_ui_schedule_orders_pipeline_correctly() {
        let schedule = build_default_retained_ui_schedule();
        let names: Vec<&str> = schedule.entries.iter().map(|entry| entry.name).collect();

        let expected_prefix = [
            retained_ui_system_names::INPUT_SYNC,
            retained_ui_system_names::LAYOUT_PICKING,
            #[cfg(feature = "gamepad")]
            retained_ui_system_names::GAMEPAD_NAVIGATION,
            retained_ui_system_names::WIDGET_INTERACTION,
            retained_ui_system_names::EVENT_BUBBLE,
            retained_ui_system_names::EMIT_CLICK_EVENTS,
            retained_ui_system_names::LAYOUT_STATE_UPDATE,
            retained_ui_system_names::DOCKED_PANEL_LAYOUT,
            retained_ui_system_names::RESPONSIVE_APPLY,
            retained_ui_system_names::LAYOUT_COMPUTE,
            retained_ui_system_names::THEME_TRANSITION_TICK,
            retained_ui_system_names::THEME_APPLY,
            retained_ui_system_names::LAYOUT_COLOR_BLEND,
            retained_ui_system_names::LAYOUT_TRANSFORM_BLEND,
            retained_ui_system_names::LAYOUT_RENDER_SYNC,
        ];
        assert_eq!(names, expected_prefix);
    }
}