use std::fmt;
use std::path::PathBuf;
use std::rc::Rc;
use oxframe_2d::{SceneGraph, View};
use oxframe_core::Executor;
use oxframe_protocol::{MarkerInfo, Meta, MetaError, SceneInfo};
use oxframe_render::{Image, RenderOptions, Renderer};
use crate::{Project, SceneDef};
#[derive(Debug)]
pub enum EngineError {
Meta {
path: PathBuf,
error: MetaError,
},
Io {
path: PathBuf,
error: std::io::Error,
},
NoScenes,
}
impl fmt::Display for EngineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Meta { path, error } => write!(f, "{}: {error}", path.display()),
Self::Io { path, error } => write!(f, "{}: {error}", path.display()),
Self::NoScenes => write!(f, "the project has no scenes"),
}
}
}
impl std::error::Error for EngineError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Meta { error, .. } => Some(error),
Self::Io { error, .. } => Some(error),
Self::NoScenes => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetaSection {
Markers,
Eases,
Tunables,
}
impl MetaSection {
fn singular(self) -> &'static str {
match self {
Self::Markers => "marker",
Self::Eases => "ease",
Self::Tunables => "tunable",
}
}
}
struct SceneRun {
graph: Rc<SceneGraph>,
exec: Executor,
theme: oxframe_2d::Theme,
}
impl SceneRun {
fn new(def: &SceneDef, project: &Project, meta: Meta) -> Self {
let graph = SceneGraph::new(project.size.0, project.size.1);
let fut = (def.factory)(View::current());
Self {
graph,
exec: Executor::new(
f64::from(project.fps),
meta,
scene_seed(project.seed, def.name),
fut,
),
theme: project.theme.clone(),
}
}
fn step(&mut self) -> bool {
let theme = self.theme.clone();
oxframe_2d::enter_theme(&theme, || {
let Self { graph, exec, .. } = self;
graph.enter(|| exec.step())
})
}
fn last_frame(&self) -> Option<u32> {
self.exec.frames_done().checked_sub(1)
}
}
struct SceneState {
def: SceneDef,
meta: Meta,
meta_path: PathBuf,
start: u32,
frames: u32,
run: Option<SceneRun>,
}
pub struct Engine {
project: Project,
scenes: Vec<SceneState>,
}
impl Engine {
pub fn new(project: Project) -> Result<Self, EngineError> {
if project.scenes.is_empty() {
return Err(EngineError::NoScenes);
}
let mut scenes = Vec::with_capacity(project.scenes.len());
for def in &project.scenes {
let meta_path = match &project.meta_dir {
Some(dir) => dir.join(format!("{}.meta.json", def.name)),
None => def.meta_path(),
};
let meta = match &project.inline_meta {
Some(m) => m.get(def.name).cloned().unwrap_or_default(),
None => load_meta(&meta_path)?,
};
scenes.push(SceneState {
def: *def,
meta,
meta_path,
start: 0,
frames: 0,
run: None,
});
}
if let Some(file) = &project.audio
&& let Some(first) = scenes.first_mut()
&& first.meta.audio.as_ref().is_none_or(|a| &a.file != file)
{
let offset = first.meta.audio.as_ref().map_or(0.0, |a| a.offset);
first.meta.audio = Some(oxframe_protocol::AudioMeta {
file: file.clone(),
offset,
extra: Default::default(),
});
if project.write_meta && project.inline_meta.is_none() {
save_meta(&first.meta_path, &first.meta)?;
}
}
let mut engine = Self { project, scenes };
for i in 0..engine.scenes.len() {
engine.measure(i)?;
}
engine.layout();
Ok(engine)
}
pub fn project(&self) -> &Project {
&self.project
}
fn measure(&mut self, i: usize) -> Result<(), EngineError> {
let max = (self.project.max_scene_seconds * f64::from(self.project.fps)).ceil() as u32;
let scene = &mut self.scenes[i];
let mut run = SceneRun::new(&scene.def, &self.project, scene.meta.clone());
while !run.step() && run.exec.frames_done() < max.max(1) {}
scene.frames = run.exec.frames_done();
if run.exec.take_meta_changed() {
scene.meta = run.exec.meta();
if self.project.write_meta && self.project.inline_meta.is_none() {
save_meta(&scene.meta_path, &scene.meta)?;
}
}
scene.run = Some(run);
Ok(())
}
fn layout(&mut self) {
let mut start = 0;
for s in &mut self.scenes {
s.start = start;
start += s.frames;
}
}
pub fn fps(&self) -> u32 {
self.project.fps
}
pub fn size(&self) -> (u32, u32) {
self.project.size
}
pub fn total_frames(&self) -> u32 {
self.scenes.last().map_or(0, |s| s.start + s.frames)
}
pub fn scenes(&self) -> Vec<SceneInfo> {
self.scenes
.iter()
.map(|s| SceneInfo {
name: s.def.name.to_owned(),
start: s.start,
frames: s.frames,
})
.collect()
}
pub fn markers(&self) -> Vec<MarkerInfo> {
self.scenes
.iter()
.flat_map(|s| {
s.meta.markers.iter().map(|(name, &time)| MarkerInfo {
scene: s.def.name.to_owned(),
name: name.clone(),
time,
})
})
.collect()
}
pub fn audio(&self) -> Option<(String, f64)> {
let a = self.scenes.first()?.meta.audio.as_ref()?;
Some((a.file.clone(), a.offset))
}
pub fn metas(&self) -> Vec<(String, Meta)> {
self.scenes
.iter()
.map(|s| (s.def.name.to_owned(), s.meta.clone()))
.collect()
}
pub fn meta(&self, scene: &str) -> Option<&Meta> {
self.scenes
.iter()
.find(|s| s.def.name == scene)
.map(|s| &s.meta)
}
pub fn locate(&self, frame: u32) -> (usize, u32) {
let total = self.total_frames();
let frame = frame.min(total.saturating_sub(1));
let i = self
.scenes
.iter()
.rposition(|s| s.start <= frame)
.unwrap_or(0);
(i, frame - self.scenes[i].start)
}
pub fn seek(&mut self, frame: u32) -> &SceneGraph {
let (i, local) = self.locate(frame);
let project = &self.project;
let scene = &mut self.scenes[i];
let reusable = scene
.run
.as_ref()
.is_some_and(|r| r.last_frame().is_some_and(|last| last <= local));
if !reusable {
scene.run = Some(SceneRun::new(&scene.def, project, scene.meta.clone()));
}
let run = scene.run.as_mut().expect("run was just ensured");
while run.last_frame().is_none_or(|last| last < local) {
if run.step() {
break;
}
}
for (j, s) in self.scenes.iter_mut().enumerate() {
if j != i {
s.run = None;
}
}
&self.scenes[i].run.as_ref().expect("run exists").graph
}
pub fn edit_meta(
&mut self,
section: MetaSection,
name: &str,
scene: Option<&str>,
apply: impl FnOnce(&mut Meta),
) -> Result<(String, Meta), String> {
let i = match scene {
Some(scene) => self
.scenes
.iter()
.position(|s| s.def.name == scene)
.ok_or_else(|| format!("no scene named `{scene}`"))?,
None => self
.scenes
.iter()
.position(|s| match section {
MetaSection::Markers => s.meta.markers.contains_key(name),
MetaSection::Eases => s.meta.eases.contains_key(name),
MetaSection::Tunables => s.meta.tunables.contains_key(name),
})
.ok_or_else(|| format!("no {} named `{name}` in any scene", section.singular()))?,
};
let scene = &mut self.scenes[i];
let mut meta = scene.meta.clone();
apply(&mut meta);
meta.validate().map_err(|e| e.to_string())?;
scene.meta = meta;
if self.project.write_meta && self.project.inline_meta.is_none() {
save_meta(&scene.meta_path, &scene.meta).map_err(|e| e.to_string())?;
}
scene.run = None;
self.measure(i).map_err(|e| e.to_string())?;
self.layout();
let s = &self.scenes[i];
Ok((s.def.name.to_owned(), s.meta.clone()))
}
pub fn pixels_to_scene(&self, x: f64, y: f64) -> oxframe_core::Point {
let (w, h) = self.project.size;
oxframe_core::Point::new(x - f64::from(w) / 2.0, y - f64::from(h) / 2.0)
}
pub fn render(&mut self, frame: u32, renderer: &mut dyn Renderer, size: (u32, u32)) -> Image {
let background = self.project.background;
let graph = self.seek(frame);
renderer.render(
graph,
&RenderOptions {
width: size.0,
height: size.1,
background,
},
)
}
}
fn scene_seed(seed: u64, name: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in name.bytes() {
h = (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3);
}
seed ^ h
}
fn load_meta(path: &PathBuf) -> Result<Meta, EngineError> {
match std::fs::read_to_string(path) {
Ok(text) => Meta::parse(&text).map_err(|error| EngineError::Meta {
path: path.clone(),
error,
}),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Meta::default()),
Err(error) => Err(EngineError::Io {
path: path.clone(),
error,
}),
}
}
fn save_meta(path: &PathBuf, meta: &Meta) -> Result<(), EngineError> {
let text = meta.to_pretty_string();
if std::fs::read_to_string(path).is_ok_and(|old| old == text) {
return Ok(());
}
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, &text)
.and_then(|()| std::fs::rename(&tmp, path))
.map_err(|error| EngineError::Io {
path: path.clone(),
error,
})
}