#![allow(clippy::collapsible_else_if)]
#![allow(clippy::collapsible_if)]
#![allow(clippy::needless_update)]
#![allow(clippy::single_match)]
#![deny(rust_2018_idioms)]
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(clippy::cast_lossless)]
#![warn(clippy::doc_markdown)]
#![warn(clippy::exhaustive_enums)]
#![warn(clippy::exhaustive_structs)]
#![warn(clippy::modulo_arithmetic)]
#![warn(clippy::return_self_not_must_use)]
#![warn(clippy::undocumented_unsafe_blocks)]
#![warn(clippy::uninlined_format_args)]
#![warn(clippy::unnecessary_self_imports)]
#![warn(clippy::wrong_self_convention)]
#![warn(explicit_outlives_requirements)]
#![warn(missing_debug_implementations)]
#![warn(noop_method_call)]
#![warn(trivial_numeric_casts)]
#![warn(unused_extern_crates)]
#![warn(unused_lifetimes)]
#![cfg_attr(test,
allow(clippy::float_cmp), // deterministic tests
allow(clippy::redundant_clone), // prefer regularity over efficiency
)]
use std::time::{Duration, Instant};
use all_is_cubes::universe::Universe;
use clap::{CommandFactory as _, Parser as _};
use futures::executor::block_on;
use indicatif::{ProgressBar, ProgressStyle};
use rand::{thread_rng, Rng};
use all_is_cubes::camera::{GraphicsOptions, Viewport};
use all_is_cubes::cgmath::{Vector2, Zero as _};
use all_is_cubes::listen::ListenableCell;
use all_is_cubes::space::{LightUpdatesInfo, Space};
use all_is_cubes::util::YieldProgress;
use all_is_cubes_ui::apps::Session;
mod aic_winit;
use aic_winit::winit_main_loop;
mod command_options;
use command_options::GraphicsType;
mod config_files;
mod glue;
mod record;
use record::record_main;
mod audio;
mod session;
mod terminal;
use crate::aic_winit::{create_winit_rt_desktop_session, create_winit_wgpu_desktop_session};
use crate::command_options::{
determine_record_format, parse_universe_source, AicDesktopArgs, DisplaySizeArg, UniverseSource,
};
use crate::record::{create_recording_session, RecordFormat};
use crate::session::DesktopSession;
use crate::terminal::{
create_terminal_session, terminal_main_loop, terminal_print_once, TerminalOptions,
};
static TITLE: &str = "All is Cubes";
fn title_and_version() -> String {
format!("{TITLE} v{v}", v = clap::crate_version!())
}
fn main() -> Result<(), anyhow::Error> {
let options = AicDesktopArgs::parse();
let AicDesktopArgs {
graphics: graphics_type,
display_size: DisplaySizeArg(display_size),
fullscreen,
template,
precompute_light,
input_file,
output_file,
duration,
verbose,
no_config_files,
} = options.clone();
let input_source = parse_universe_source(input_file, template);
if graphics_type != GraphicsType::Terminal || verbose {
use simplelog::LevelFilter::{Debug, Off, Trace};
simplelog::TermLogger::init(
match verbose {
false => Debug,
true => Trace,
},
simplelog::ConfigBuilder::new()
.set_target_level(Off)
.set_location_level(Off)
.add_filter_ignore_str("wgpu") .add_filter_ignore_str("naga") .add_filter_ignore_str("winit") .build(),
simplelog::TerminalMode::Stderr,
simplelog::ColorChoice::Auto,
)?;
}
let graphics_options = if no_config_files {
GraphicsOptions::default()
} else {
config_files::load_config().expect("Error loading configuration files")
};
let viewport_cell = ListenableCell::new(Viewport::with_scale(
1.0,
display_size.unwrap_or_else(Vector2::zero),
));
let start_session_time = Instant::now();
let mut session = block_on(Session::builder().ui(viewport_cell.as_source()).build());
session.graphics_options_mut().set(graphics_options);
let session_done_time = Instant::now();
log::debug!(
"Initialized session ({:.3} s)",
session_done_time
.duration_since(start_session_time)
.as_secs_f32()
);
let precompute_light = precompute_light
|| (graphics_type == GraphicsType::Record
&& output_file
.as_ref()
.and_then(|f| determine_record_format(f).ok())
!= Some(RecordFormat::Gltf));
let universe = create_universe(input_source, precompute_light)?;
session.set_universe(universe);
let inner_params = InnerMainParams {
before_loop_time: Instant::now(),
headless: options.is_headless(),
};
match graphics_type {
GraphicsType::Window => {
let event_loop = winit::event_loop::EventLoop::new();
let dsession = block_on(create_winit_wgpu_desktop_session(
session,
aic_winit::create_window(
&event_loop,
&title_and_version(),
display_size,
fullscreen,
)?,
viewport_cell,
))?;
inner_main(
inner_params,
move |dsession| winit_main_loop(event_loop, dsession),
dsession,
)
}
GraphicsType::WindowRt => {
let event_loop = winit::event_loop::EventLoop::new();
let dsession = create_winit_rt_desktop_session(
session,
aic_winit::create_window(
&event_loop,
&title_and_version(),
display_size,
fullscreen,
)?,
viewport_cell,
)?;
inner_main(
inner_params,
move |dsession| winit_main_loop(event_loop, dsession),
dsession,
)
}
GraphicsType::Terminal => {
let dsession =
create_terminal_session(session, TerminalOptions::default(), viewport_cell)?;
inner_main(inner_params, terminal_main_loop, dsession)
}
GraphicsType::Record => {
let record_options = options
.record_options()
.map_err(|e| e.format(&mut AicDesktopArgs::command()))?;
let (dsession, sr) = create_recording_session(session, &record_options, viewport_cell)?;
inner_main(
inner_params,
|dsession| record_main(dsession, record_options, sr),
dsession,
)
}
GraphicsType::Print => {
let dsession =
create_terminal_session(session, TerminalOptions::default(), viewport_cell)?;
inner_main(
inner_params,
|dsession| {
terminal_print_once(
dsession,
display_size
.unwrap_or_else(|| Vector2::new(80, 24))
.map(|component| component.min(u16::MAX.into()) as u16),
)
},
dsession,
)
}
GraphicsType::Headless => inner_main(
inner_params,
|dsession| headless_main_loop(dsession, duration),
DesktopSession::new((), (), session, viewport_cell),
),
}
}
fn inner_main<Ren, Win>(
params: InnerMainParams,
looper: impl FnOnce(DesktopSession<Ren, Win>) -> Result<(), anyhow::Error>,
mut dsession: DesktopSession<Ren, Win>,
) -> Result<(), anyhow::Error> {
if !params.headless {
match audio::init_sound(&dsession.session) {
Ok(audio_out) => dsession.audio = Some(audio_out),
Err(e) => log::error!(
"Failed to initialize audio. Will proceed without.\n{e:#}",
),
};
}
log::debug!(
"Initialized desktop-session ({:.3} s); entering event loop",
Instant::now()
.duration_since(params.before_loop_time)
.as_secs_f64()
);
looper(dsession)
}
struct InnerMainParams {
before_loop_time: Instant,
headless: bool,
}
fn create_universe(
input_source: UniverseSource,
precompute_light: bool,
) -> Result<Universe, anyhow::Error> {
let start_time = Instant::now();
let universe_progress_bar = ProgressBar::new(100)
.with_style(
common_progress_style()
.template("{prefix:8} [{elapsed}] {wide_bar} {pos:>6}% {msg:36}")
.unwrap(),
)
.with_prefix("Building");
universe_progress_bar.set_position(0);
let yield_progress = {
let universe_progress_bar = universe_progress_bar.clone();
YieldProgress::new(
|| std::future::ready(()),
move |fraction, label| {
universe_progress_bar.set_position((fraction * 100.0) as u64);
universe_progress_bar.set_message(String::from(label));
},
)
};
let universe = block_on(async {
match input_source.clone() {
UniverseSource::Template(template) => template
.build(yield_progress, thread_rng().gen())
.await
.map_err(anyhow::Error::from),
UniverseSource::File(path) => {
all_is_cubes_port::load_universe_from_file(yield_progress, &*path).await
}
}
})?;
universe_progress_bar.finish();
let universe_done_time = Instant::now();
log::debug!(
"Initialized game state with {:?} ({:.3} s)",
input_source,
universe_done_time.duration_since(start_time).as_secs_f32()
);
if precompute_light {
if let Some(c) = universe.get_default_character() {
c.read()
.unwrap()
.space
.try_modify(evaluate_light_with_progress)
.unwrap();
}
}
Ok(universe)
}
fn headless_main_loop(
mut dsession: DesktopSession<(), ()>,
duration: Option<f64>,
) -> Result<(), anyhow::Error> {
log::info!("Simulating a universe nobody's looking at...");
let duration = duration.map(Duration::from_secs_f64);
let t0 = Instant::now();
loop {
dsession.advance_time_and_maybe_step();
if duration
.map(|d| Instant::now().duration_since(t0) > d)
.unwrap_or(false)
{
break;
} else if let Some(t) = dsession.session.frame_clock.next_step_or_draw_time() {
std::thread::sleep(t - Instant::now());
}
}
Ok(())
}
fn evaluate_light_with_progress(space: &mut Space) {
let light_progress = ProgressBar::new(100)
.with_style(common_progress_style())
.with_prefix("Lighting");
space.evaluate_light(1, lighting_progress_adapter(&light_progress));
light_progress.finish();
}
fn lighting_progress_adapter(progress: &ProgressBar) -> impl FnMut(LightUpdatesInfo) + '_ {
let mut worst = 1;
move |info| {
worst = worst.max(info.queue_count);
progress.set_length(worst as u64);
progress.set_position((worst - info.queue_count) as u64);
}
}
fn common_progress_style() -> ProgressStyle {
ProgressStyle::default_bar()
.template("{prefix:8} [{elapsed}] {wide_bar} {pos:>6}/{len:6} {msg:30}")
.unwrap()
}
fn choose_graphical_window_size(maximum_size: Option<Vector2<u32>>) -> Vector2<u32> {
match maximum_size {
Some(maximum_size) => {
maximum_size * 7 / 10
}
None => Vector2::new(800, 600),
}
}