use crate::app::runloop;
use crate::app::startup_error::StartupError;
use crate::app::state::App;
use crate::result::CnResult;
use concinnity_host::store::paths::StateTree;
use std::path::Path;
use tracing_subscriber::EnvFilter;
fn default_log_directive() -> &'static str {
if cfg!(debug_assertions) {
"info"
} else {
"warn"
}
}
fn log_filter() -> EnvFilter {
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_log_directive()))
}
pub fn init_logging() {
use tracing_subscriber::Layer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let fmt = tracing_subscriber::fmt::layer().with_filter(log_filter());
let _ = tracing_subscriber::registry()
.with(fmt)
.with(crate::crash::RingLayer)
.try_init();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PipelineMode {
#[default]
Pipelined,
Serial,
}
#[derive(Debug, Default)]
pub struct RunOptions {
pub mode: PipelineMode,
pub schedule: crate::ecs::ScheduleMode,
pub screenshot: Option<String>,
pub max_frames: Option<u64>,
}
pub fn run(tree: &StateTree, options: RunOptions) -> std::io::Result<()> {
init_logging();
let mut app = App::new().in_tree(tree.clone());
let primary = app.primary_blob();
if let Err(e) = app.load_blob() {
report_startup_error(match primary {
Some(blob) => StartupError::from_blob_failure(blob, e),
None => StartupError::NoStateRoot,
});
return Ok(());
}
start_runtime(app, options).map_err(start_failure)
}
fn start_failure(e: CnResult) -> std::io::Error {
std::io::Error::other(format!("failed to start app: {e}"))
}
fn report_startup_error(error: StartupError) {
tracing::error!("{}", error.log_line());
if !crate::error_screen::show("Concinnity", &error.user_message()) {
eprintln!("{}", error.log_line());
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlobSource<'a> {
Directory(&'a Path),
File(&'a Path),
}
impl BlobSource<'_> {
fn primary(&self) -> std::path::PathBuf {
match self {
BlobSource::Directory(dir) => dir.join("0"),
BlobSource::File(file) => file.to_path_buf(),
}
}
fn check_span(&self, max_blob_index: u32) -> Option<StartupError> {
match self {
BlobSource::File(file) if max_blob_index > 0 => {
Some(StartupError::OverflowUnsupported {
blob: file.to_path_buf(),
needed: max_blob_index,
})
}
_ => None,
}
}
}
pub fn run_from(tree: &StateTree, blob: BlobSource<'_>) -> std::io::Result<()> {
init_logging();
let primary = blob.primary();
let mut app = App::new().in_tree(tree.clone());
let failure = match app.load_blob_from(&primary) {
Ok(max_blob_index) => blob.check_span(max_blob_index),
Err(e) => Some(StartupError::from_blob_failure(primary, e)),
};
if let Some(error) = failure {
report_startup_error(error.clone());
return Err(std::io::Error::new(error.io_kind(), error.log_line()));
}
start_runtime(app, RunOptions::default()).map_err(start_failure)
}
pub(crate) fn start_runtime(mut app: App, options: RunOptions) -> Result<(), CnResult> {
init_logging();
tracing::info!("Running app...");
runloop::install_ctrlc_handler(&app);
let renders = crate::ecs::renders(app.world());
if let Some(max) = options.max_frames {
for config in app
.world_mut()
.query_mut::<crate::components::GraphicsConfig>()
{
config.max_frames = Some(max);
}
}
if options.screenshot.is_some() {
crate::app::dev_flags::set_capture(true);
}
app.world_mut().insert_resource(options.schedule);
#[cfg(target_os = "macos")]
if renders {
runloop::activate_app_macos();
}
if let Err(e) = app.start() {
tracing::error!("failed to start app: {e}");
return Err(e);
}
match options.mode {
PipelineMode::Pipelined if renders => {
crate::app::pipeline::run_pipelined(app, options.screenshot.as_deref());
}
_ => {
runloop::run_loop(&mut app, cfg!(target_os = "macos") && renders, |_| {});
capture_exit_screenshot(&mut app, options.screenshot.as_deref());
}
}
Ok(())
}
fn capture_exit_screenshot(app: &mut App, path: Option<&str>) {
let Some(path) = path else { return };
let Some(mut backend) = crate::ecs::take_render_backend(app.world_mut()) else {
tracing::warn!("screenshot skipped: no live backend at exit");
return;
};
match backend.screenshot(path) {
Ok(saved) => tracing::info!("screenshot saved: {}", saved),
Err(e) => tracing::warn!("screenshot failed: {}", e),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_directive_matches_build_profile() {
let expected = if cfg!(debug_assertions) {
"info"
} else {
"warn"
};
assert_eq!(default_log_directive(), expected);
}
#[test]
fn default_directive_is_a_valid_filter() {
EnvFilter::new(default_log_directive());
}
#[test]
fn each_blob_source_names_the_same_primary_file() {
let dir = Path::new("/apps/MyGame/data");
assert_eq!(
BlobSource::Directory(dir).primary(),
dir.join("0"),
"a directory holds blob 0"
);
let file = Path::new("/apps/MyGame/data");
assert_eq!(
BlobSource::File(file).primary(),
file.to_path_buf(),
"a single file is blob 0"
);
}
#[test]
fn a_single_file_source_refuses_a_world_that_overflows() {
let file = Path::new("/apps/MyGame/data");
assert_eq!(BlobSource::File(file).check_span(0), None);
assert_eq!(
BlobSource::File(file).check_span(2),
Some(StartupError::OverflowUnsupported {
blob: file.to_path_buf(),
needed: 2,
})
);
let dir = Path::new("/apps/MyGame/data");
assert_eq!(BlobSource::Directory(dir).check_span(0), None);
assert_eq!(BlobSource::Directory(dir).check_span(7), None);
}
#[test]
fn a_refused_start_returns_instead_of_exiting_the_process() {
let mut app = App::new();
app.start().expect("the first start succeeds");
assert_eq!(
app.run_with(RunOptions::default()),
Err(CnResult::InvalidState),
"a second start is refused"
);
}
}