dreamwell-runtime 1.0.0

Dreamwell Runtime — cross-platform GPU-accelerated game client
Documentation
// Dreamwell Runtime v1.0.0 — cross-platform GPU-accelerated game client.
//
// Consumes dreamwell-engine for game logic, dreamwell-gpu for rendering,
// dreamwell-fabric for frame orchestration.
//
// Two authority modes:
//   Local  — standalone, test, offline, editor preview
//   Remote — SpacetimeDB maincloud (feature-gated behind `multiplayer`)
//
// Same runtime contracts, two backends.

pub mod app;
pub mod game_state;
pub mod input;
pub mod renderer;
pub mod scene;
pub mod time;
pub mod window;

pub mod play;

// ── v1.0.0 API modules ─────────────────────────────────────────────────
pub mod authority;
pub mod mirror;
pub mod prelude;
pub mod presentation;
pub mod runtime_ui;
pub mod sync;

/// Runtime configuration.
pub struct RuntimeConfig {
    /// Window title.
    pub title: String,
    /// Initial window width.
    pub width: u32,
    /// Initial window height.
    pub height: u32,
    /// Enable vertical sync.
    pub vsync: bool,
    /// MSAA sample count (1 = no MSAA, 4 = recommended).
    pub msaa_samples: u32,
    /// Authority mode: Local (offline) or Remote (multiplayer).
    pub authority_mode: AuthorityMode,
    /// Root directory for content assets.
    pub asset_root: std::path::PathBuf,
    /// Waymark pack JSON file path to load on startup.
    pub pack_file: Option<String>,
    /// Tapestry lock file path for play mode.
    pub lock_file: Option<String>,
    /// SpacetimeDB host URL (enables remote authority when set).
    pub db_host: Option<String>,
    /// SpacetimeDB database name.
    pub db_name: String,
}

/// Authority mode selection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthorityMode {
    /// Local authority — standalone, test, offline, editor preview.
    Local,
    /// Remote authority — SpacetimeDB maincloud.
    Remote,
}

impl Default for RuntimeConfig {
    fn default() -> Self {
        Self {
            title: "Dreamwell".into(),
            width: 1920,
            height: 1080,
            vsync: true,
            msaa_samples: 1,
            authority_mode: AuthorityMode::Local,
            asset_root: "assets".into(),
            pack_file: None,
            lock_file: None,
            db_host: None,
            db_name: "dreamwell".into(),
        }
    }
}

/// Runtime phase model — explicit frame phases for profiling and test hooks.
///
/// Each phase has a defined purpose and mutation scope:
/// - PollInput: collect window/input events (read-only scene)
/// - PollAuthority: non-blocking check for authority events
/// - StageAuthorityEvents: buffer events for atomic application
/// - ApplyAuthorityEvents: apply staged events at tick boundary
/// - UpdateMirror: update client mirror from applied events
/// - UpdatePresentation: rebuild render-facing interpolated state
/// - AssembleFrame: prepare observer context + render packet
/// - Render: submit DreamFabric frame
/// - DrainSync: drain sync packet and enqueue outgoing work
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimePhase {
    PollInput,
    PollAuthority,
    StageAuthorityEvents,
    ApplyAuthorityEvents,
    UpdateMirror,
    UpdatePresentation,
    AssembleFrame,
    Render,
    DrainSync,
}

impl RuntimePhase {
    /// All phases in execution order.
    pub const ALL: &'static [RuntimePhase] = &[
        Self::PollInput,
        Self::PollAuthority,
        Self::StageAuthorityEvents,
        Self::ApplyAuthorityEvents,
        Self::UpdateMirror,
        Self::UpdatePresentation,
        Self::AssembleFrame,
        Self::Render,
        Self::DrainSync,
    ];

    /// Phase name for profiler labels and logging.
    pub fn label(self) -> &'static str {
        match self {
            Self::PollInput => "poll_input",
            Self::PollAuthority => "poll_authority",
            Self::StageAuthorityEvents => "stage_authority",
            Self::ApplyAuthorityEvents => "apply_authority",
            Self::UpdateMirror => "update_mirror",
            Self::UpdatePresentation => "update_presentation",
            Self::AssembleFrame => "assemble_frame",
            Self::Render => "render",
            Self::DrainSync => "drain_sync",
        }
    }
}

/// The Dreamwell runtime. Call `run()` to start the event loop.
pub struct Runtime {
    config: RuntimeConfig,
}

impl Runtime {
    pub fn new(config: RuntimeConfig) -> Result<Self, Box<dyn std::error::Error>> {
        Ok(Self { config })
    }

    /// Run the runtime. This blocks on the winit event loop.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn run(self) -> Result<(), Box<dyn std::error::Error>> {
        // On Windows, the event loop may run on a non-main thread (8MB stack thread
        // spawned by main.rs). Use `with_any_thread(true)` to allow this.
        #[cfg(target_os = "windows")]
        let event_loop = {
            use winit::platform::windows::EventLoopBuilderExtWindows;
            winit::event_loop::EventLoop::builder().with_any_thread(true).build()?
        };
        #[cfg(not(target_os = "windows"))]
        let event_loop = winit::event_loop::EventLoop::new()?;

        let mut runtime_app = app::RuntimeApp::new(RuntimeConfig {
            title: self.config.title.clone(),
            width: self.config.width,
            height: self.config.height,
            authority_mode: self.config.authority_mode,
            pack_file: self.config.pack_file.clone(),
            lock_file: self.config.lock_file.clone(),
            db_host: self.config.db_host.clone(),
            db_name: self.config.db_name.clone(),
            ..Default::default()
        })
        .map_err(|e| format!("{e:?}"))?;

        // Mount pack file if provided via CLI args.
        if let Some(ref pack_path) = self.config.pack_file {
            match std::fs::read_to_string(pack_path) {
                Ok(json) => {
                    if let Err(e) = runtime_app.mount_pack(&json) {
                        log::warn!("Failed to mount pack {}: {}", pack_path, e);
                    }
                }
                Err(e) => log::warn!("Failed to read pack file {}: {}", pack_path, e),
            }
        }

        event_loop.run_app(&mut runtime_app)?;
        Ok(())
    }
}

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

    #[test]
    fn runtime_config_default() {
        let c = RuntimeConfig::default();
        assert_eq!(c.title, "Dreamwell");
        assert_eq!(c.width, 1280);
        assert_eq!(c.height, 720);
        assert!(c.vsync);
        assert_eq!(c.msaa_samples, 4);
        assert_eq!(c.authority_mode, AuthorityMode::Local);
        assert!(c.pack_file.is_none());
        assert!(c.db_host.is_none());
        assert_eq!(c.db_name, "dreamwell");
    }

    #[test]
    fn runtime_phase_count() {
        assert_eq!(RuntimePhase::ALL.len(), 9);
    }

    #[test]
    fn runtime_phase_labels() {
        assert_eq!(RuntimePhase::PollInput.label(), "poll_input");
        assert_eq!(RuntimePhase::Render.label(), "render");
        assert_eq!(RuntimePhase::DrainSync.label(), "drain_sync");
    }

    #[test]
    fn authority_mode_default_local() {
        assert_eq!(RuntimeConfig::default().authority_mode, AuthorityMode::Local);
    }
}