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);
}
fn cursor(&self) -> (f32, f32) {
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),
}
}
fn vars(&self, (x, y): (f32, f32), down: bool, click: bool, date: &DateTime) -> Md2Vars {
let flag = |b: bool| if b { 1.0 } else { 0.0 };
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(date.total_seconds()),
],
}
}
pub fn vars_now(&self, time: f64) -> Md2Vars {
let date = self.date.unwrap_or_else(|| DateTime::from_frame_time(time));
self.vars(self.cursor(), self.mouse.left_down, false, &date)
}
pub fn begin_frame(
&mut self,
audio: &AudioFrame,
fps: f32,
time: f64,
(attack, decay): (f32, f32),
shader_fft: bool,
) -> Md2Frame {
let (x, y) = self.cursor();
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 vars = self.vars((x, y), down, click, &date);
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_render::BEATDROP_FFT_BINS, SHADER_FFT_BINS);
assert_eq!(lactea_render::BEATDROP_WAVE_SAMPLES, SHADER_WAVE_SAMPLES);
}
#[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)]
);
}
#[test]
fn the_wave_rows_are_the_first_480_samples_over_128() {
let mut audio = AudioFrame::default();
for k in 0..audio.wave[0].len() {
audio.wave[0][k] = k as f32;
audio.wave[1][k] = -(k as f32) / 2.0;
}
let mut i = Md2Inputs::new();
let f = i.begin_frame(&audio, 60.0, 0.0, (0.5, 0.7), true);
let t = f.texture.expect("the shader FFT is on");
for k in 0..SHADER_WAVE_SAMPLES {
assert_eq!(t.wave[0][k], k as f32 / 128.0, "left {k}");
assert_eq!(t.wave[1][k], -(k as f32) / 2.0 / 128.0, "right {k}");
}
let texels = t.texels();
assert_eq!(texels.len(), 2 * (SHADER_FFT_BINS + SHADER_WAVE_SAMPLES));
let wave_at = 2 * SHADER_FFT_BINS;
assert_eq!(texels[wave_at + 479], 479.0 / 128.0);
assert_eq!(
texels[wave_at + SHADER_WAVE_SAMPLES + 479],
-479.0 / 2.0 / 128.0
);
assert!(
i.begin_frame(&audio, 60.0, 0.0, (0.5, 0.7), false)
.texture
.is_none()
);
}
#[test]
fn init_code_sees_the_current_inputs_and_leaves_the_click_edge_alone() {
let mut i = Md2Inputs::new();
i.set_mouse(MouseState {
position: Some([0.25, 0.25]),
left_down: true,
});
i.set_ctrl_keys(CtrlKeys {
up: true,
..CtrlKeys::default()
});
i.set_date_time(Some(DateTime {
year: 2026,
month: 10,
day: 13,
weekday: 2,
hour: 1,
minute: 2,
second: 3,
millisecond: 500,
}));
let v = i.vars_now(0.0);
assert_eq!(v.mouse, [0.25, 0.75, 1.0, 0.0]);
assert_eq!(v.ctrl, [0.0, 0.0, 0.0, 1.0]);
assert_eq!(v.date[0], 2026.0);
assert_eq!(v.date[7], 500.0);
assert_eq!(v.date[8], f64::from(3723.5f32));
let f = i.begin_frame(&AudioFrame::default(), 60.0, 0.0, (0.5, 0.7), false);
assert_eq!(f.vars.mouse[3], 1.0, "the peek did not eat the press");
assert_eq!(f.vars.date, v.date);
}
}