use crate::md3_inputs::MouseState;
use lactea_audio::{AudioFrame, BeatDropShaderFft, SHADER_FFT_BINS, SHADER_WAVE_SAMPLES};
use lactea_render::BeatDropTexture;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DateTime {
pub year: u16,
pub month: u8,
pub day: u8,
pub weekday: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub millisecond: u16,
}
impl DateTime {
pub fn is_valid(&self) -> bool {
(1..=12).contains(&self.month)
&& (1..=31).contains(&self.day)
&& (1..=7).contains(&self.weekday)
&& self.hour < 24
&& self.minute < 60
&& self.second < 60
&& self.millisecond < 1000
}
fn total_seconds(&self) -> f32 {
f32::from(self.hour) * 3600.0
+ f32::from(self.minute) * 60.0
+ f32::from(self.second)
+ f32::from(self.millisecond) * 0.001
}
fn from_frame_time(seconds: f64) -> Self {
let day = seconds.rem_euclid(86_400.0);
let whole = day.floor();
let n = whole as u32;
DateTime {
hour: (n / 3600) as u8,
minute: (n / 60 % 60) as u8,
second: (n % 60) as u8,
millisecond: (((day - whole) * 1000.0) as u16).min(999),
..DateTime::default()
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CtrlKeys {
pub left: bool,
pub right: bool,
pub down: bool,
pub up: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Md2Vars {
pub mouse: [f64; 4],
pub ctrl: [f64; 4],
pub date: [f64; 9],
}
pub(crate) struct Md2Frame {
pub vars: Md2Vars,
pub mouse: [f32; 4],
pub time_date: [[f32; 4]; 2],
pub texture: Option<Arc<BeatDropTexture>>,
}
pub(crate) struct Md2Inputs {
mouse: MouseState,
ctrl: CtrlKeys,
date: Option<DateTime>,
prev_down: bool,
last_click: [f32; 2],
fft: Option<Box<BeatDropShaderFft>>,
}
impl Md2Inputs {
pub fn new() -> Self {
Self {
mouse: MouseState::default(),
ctrl: CtrlKeys::default(),
date: None,
prev_down: false,
last_click: [0.0, 0.0],
fft: None,
}
}
pub fn set_mouse(&mut self, m: MouseState) {
self.mouse = m;
}
pub fn set_ctrl_keys(&mut self, k: CtrlKeys) {
self.ctrl = k;
}
pub fn set_date_time(&mut self, d: Option<DateTime>) {
self.date = d;
}
pub fn push_pcm(&mut self, interleaved: &[f32], channels: usize) {
self.fft
.get_or_insert_with(|| Box::new(BeatDropShaderFft::new()))
.push_pcm_f32(interleaved, channels);
}
pub fn begin_frame(
&mut self,
audio: &AudioFrame,
fps: f32,
time: f64,
(attack, decay): (f32, f32),
shader_fft: bool,
) -> Md2Frame {
let (x, y) = match self.mouse.position {
Some([u, v]) => (((2.0 * u - 1.0) / 2.0) + 0.5, ((1.0 - 2.0 * v) / 2.0) + 0.5),
None => (-1.0, -1.0),
};
let down = self.mouse.left_down;
let click = down && !self.prev_down;
self.prev_down = down;
if click {
self.last_click = [x, -y + 1.0];
}
let [lx, ly] = self.last_click;
let mouse = [
x,
if y != -1.0 { -y + 1.0 } else { -1.0 },
if down { lx } else { -lx },
if click { ly } else { -ly },
];
let date = self.date.unwrap_or_else(|| DateTime::from_frame_time(time));
let total = date.total_seconds();
let flag = |b: bool| if b { 1.0 } else { 0.0 };
let vars = Md2Vars {
mouse: [f64::from(x), f64::from(y), flag(down), flag(click)],
ctrl: [
flag(self.ctrl.left),
flag(self.ctrl.right),
flag(self.ctrl.down),
flag(self.ctrl.up),
],
date: [
f64::from(date.year),
f64::from(date.month),
f64::from(date.day),
f64::from(date.weekday),
f64::from(date.hour),
f64::from(date.minute),
f64::from(date.second),
f64::from(date.millisecond),
f64::from(total),
],
};
let time_date = [
[
f32::from(date.hour),
f32::from(date.minute),
f32::from(date.second),
total,
],
[
f32::from(date.year),
f32::from(date.month),
f32::from(date.day),
f32::from(date.weekday),
],
];
let texture = shader_fft.then(|| {
let fft = self
.fft
.get_or_insert_with(|| Box::new(BeatDropShaderFft::new()));
fft.update(fps, attack, decay);
let mut wave = [[0.0f32; SHADER_WAVE_SAMPLES]; 2];
for (row, src) in wave.iter_mut().zip(&audio.wave) {
for (d, s) in row.iter_mut().zip(src) {
*d = s / 128.0;
}
}
let mut tex = BeatDropTexture {
spectrum: [0.0; SHADER_FFT_BINS],
peak: [0.0; SHADER_FFT_BINS],
wave,
};
tex.spectrum.copy_from_slice(fft.smoothed());
tex.peak.copy_from_slice(fft.peak());
Arc::new(tex)
});
Md2Frame {
vars,
mouse,
time_date,
texture,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn frame(i: &mut Md2Inputs, pos: Option<[f32; 2]>, down: bool) -> Md2Frame {
i.set_mouse(MouseState {
position: pos,
left_down: down,
});
i.begin_frame(&AudioFrame::default(), 60.0, 0.0, (0.5, 0.7), false)
}
#[test]
fn the_beatdrop_texture_layout_agrees_across_crates() {
assert_eq!(lactea_hlsl::BEATDROP_FFT_BINS, SHADER_FFT_BINS);
assert_eq!(lactea_render::BEATDROP_FFT_BINS, SHADER_FFT_BINS);
assert_eq!(lactea_hlsl::BEATDROP_WAVE_SAMPLES, SHADER_WAVE_SAMPLES);
assert_eq!(lactea_render::BEATDROP_WAVE_SAMPLES, SHADER_WAVE_SAMPLES);
assert_eq!(lactea_render::BEATDROP_ROWS, 4);
}
#[test]
fn mouse_vars_and_c14() {
let mut i = Md2Inputs::new();
let f = frame(&mut i, None, false);
assert_eq!(f.vars.mouse, [-1.0, -1.0, 0.0, 0.0]);
assert_eq!(f.mouse, [-1.0, -1.0, 0.0, 0.0]);
let f = frame(&mut i, Some([0.25, 0.25]), true);
assert_eq!(f.vars.mouse, [0.25, 0.75, 1.0, 1.0]);
assert_eq!(f.mouse, [0.25, 0.25, 0.25, 0.25]);
let f = frame(&mut i, Some([0.5, 0.5]), true);
assert_eq!(
f.vars.mouse,
[0.5, 0.5, 1.0, 0.0],
"no second click while held"
);
assert_eq!(
f.mouse,
[0.5, 0.5, 0.25, -0.25],
"held: +lastX; no click: -lastY"
);
let f = frame(&mut i, Some([0.5, 0.5]), false);
assert_eq!(f.vars.mouse[2], 0.0);
assert_eq!(f.mouse[2], -0.25, "released: -lastX");
}
#[test]
fn clock_follows_the_host_or_the_frame_time() {
let mut i = Md2Inputs::new();
let f = i.begin_frame(&AudioFrame::default(), 60.0, 3723.5, (0.5, 0.7), false);
assert_eq!(f.time_date[0], [1.0, 2.0, 3.0, 3723.5]);
assert_eq!(f.time_date[1], [0.0; 4]);
i.set_date_time(Some(DateTime {
year: 2026,
month: 10,
day: 13,
weekday: 2,
hour: 23,
minute: 59,
second: 58,
millisecond: 250,
}));
let f = i.begin_frame(&AudioFrame::default(), 60.0, 0.0, (0.5, 0.7), false);
assert_eq!(f.time_date[1], [2026.0, 10.0, 13.0, 2.0]);
assert_eq!(
f.vars.date[4..],
[23.0, 59.0, 58.0, 250.0, f64::from(86_398.25f32)]
);
}
}