use crate::config::VpxConfig;
const VR_ENABLED: i32 = 0;
const VR_AUTODETECT: i32 = 1;
const VR_DISABLED: i32 = 2;
pub const STEREO_FIRST: i32 = 1;
pub const STEREO_LAST: i32 = 10;
#[derive(Clone, Debug, PartialEq)]
pub struct ThreeD {
pub vr: bool,
pub vr_always: bool,
pub vr_refresh: i32,
pub vr_desktop: i32,
pub vr_shrink: bool,
pub vr_backglass: bool,
pub vr_audio_backglass: bool,
pub vr_audio_playfield: bool,
pub stereo: i32,
pub stereo_custom: Option<i32>,
pub stereo_eye_mm: f32,
pub stereo_brightness: f32,
pub stereo_saturation: f32,
}
impl Default for ThreeD {
fn default() -> Self {
Self {
vr: false,
vr_always: false,
vr_refresh: 0,
vr_desktop: 1,
vr_shrink: false,
vr_backglass: false,
vr_audio_backglass: true,
vr_audio_playfield: true,
stereo: 0,
stereo_custom: None,
stereo_eye_mm: 63.0,
stereo_brightness: 1.0,
stereo_saturation: 1.0,
}
}
}
impl ThreeD {
pub fn load(config: &VpxConfig) -> Self {
let d = Self::default();
let flag =
|key: &str, default: bool| config.get_i32("PlayerVR", key).map_or(default, |v| v != 0);
let ask = config
.get_i32("PlayerVR", "AskToTurnOn")
.unwrap_or(VR_DISABLED);
let mode = config.get_i32("Player", "Stereo3D").unwrap_or(0);
let offered = (STEREO_FIRST..=STEREO_LAST).contains(&mode);
let stereo = if offered { mode } else { 0 };
let stereo_custom = (mode > STEREO_LAST).then_some(mode);
Self {
vr: ask == VR_ENABLED || ask == VR_AUTODETECT,
vr_always: ask == VR_ENABLED,
vr_refresh: config
.get_i32("PlayerVR", "DisplayRefreshRate")
.unwrap_or(d.vr_refresh)
.clamp(0, 4),
vr_desktop: config
.get_i32("PlayerVR", "DesktopDisplay")
.unwrap_or(d.vr_desktop)
.clamp(0, 5),
vr_shrink: flag("ShrinkDesktopDisplay", d.vr_shrink),
vr_backglass: flag("AddBackglass", d.vr_backglass),
vr_audio_backglass: flag("SpatialAudioBackglass", d.vr_audio_backglass),
vr_audio_playfield: flag("SpatialAudioPlayfield", d.vr_audio_playfield),
stereo,
stereo_custom,
stereo_eye_mm: config
.get_f32("Player", "Stereo3DEyeSeparation")
.unwrap_or(d.stereo_eye_mm)
.clamp(5.0, 200.0),
stereo_brightness: config
.get_f32("Player", "Stereo3DBrightness")
.unwrap_or(d.stereo_brightness)
.clamp(0.0, 2.0),
stereo_saturation: config
.get_f32("Player", "Stereo3DSaturation")
.unwrap_or(d.stereo_saturation)
.clamp(0.0, 2.0),
}
}
pub fn save(&self, config: &mut VpxConfig) {
let b = |v: bool| i32::from(v);
let ask = match (self.vr, self.vr_always) {
(false, _) => VR_DISABLED,
(true, true) => VR_ENABLED,
(true, false) => VR_AUTODETECT,
};
config.set_i32("PlayerVR", "AskToTurnOn", ask);
config.set_i32(
"PlayerVR",
"DisplayRefreshRate",
self.vr_refresh.clamp(0, 4),
);
config.set_i32("PlayerVR", "DesktopDisplay", self.vr_desktop.clamp(0, 5));
config.set_i32("PlayerVR", "ShrinkDesktopDisplay", b(self.vr_shrink));
config.set_i32("PlayerVR", "AddBackglass", b(self.vr_backglass));
config.set_i32(
"PlayerVR",
"SpatialAudioBackglass",
b(self.vr_audio_backglass),
);
config.set_i32(
"PlayerVR",
"SpatialAudioPlayfield",
b(self.vr_audio_playfield),
);
if self.stereo > 0 || self.stereo_custom.is_none() {
config.set_i32("Player", "Stereo3D", self.stereo.clamp(0, STEREO_LAST));
config.set_i32("Player", "Stereo3DEnabled", b(self.stereo > 0));
}
config.set_f32(
"Player",
"Stereo3DEyeSeparation",
self.stereo_eye_mm.clamp(5.0, 200.0),
);
config.set_f32(
"Player",
"Stereo3DBrightness",
self.stereo_brightness.clamp(0.0, 2.0),
);
config.set_f32(
"Player",
"Stereo3DSaturation",
self.stereo_saturation.clamp(0.0, 2.0),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn config() -> VpxConfig {
let dir = std::env::temp_dir().join(format!("pinready-3d-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("VPinballX.ini");
std::fs::write(&path, "[Player]\n").unwrap();
VpxConfig::load(Some(&path)).unwrap()
}
#[test]
fn an_empty_ini_has_vr_and_stereo_off() {
let t = ThreeD::load(&config());
assert_eq!(t, ThreeD::default());
assert!(!t.vr);
assert_eq!(t.stereo, 0);
}
#[test]
fn what_is_saved_is_read_back() {
let mut c = config();
let t = ThreeD {
vr: true,
vr_always: true,
vr_refresh: 3,
vr_desktop: 5,
vr_shrink: true,
vr_backglass: true,
vr_audio_backglass: false,
vr_audio_playfield: false,
stereo: 5,
stereo_custom: None,
stereo_eye_mm: 60.0,
stereo_brightness: 1.25,
stereo_saturation: 0.75,
};
t.save(&mut c);
assert_eq!(ThreeD::load(&c), t);
}
#[test]
fn vr_modes_map_to_vpxs_three_values() {
for (vr, always, want) in [(false, false, 2), (true, true, 0), (true, false, 1)] {
let mut c = config();
ThreeD {
vr,
vr_always: always,
..ThreeD::default()
}
.save(&mut c);
assert_eq!(c.get_i32("PlayerVR", "AskToTurnOn"), Some(want));
}
}
#[test]
fn disabled_vr_keeps_its_other_choices() {
let mut c = config();
let t = ThreeD {
vr: false,
vr_refresh: 2,
..ThreeD::default()
};
t.save(&mut c);
assert_eq!(ThreeD::load(&c).vr_refresh, 2);
}
#[test]
fn a_custom_anaglyph_slot_reads_as_off_and_survives_a_save() {
let mut c = config();
c.set_i32("Player", "Stereo3D", 12);
let t = ThreeD::load(&c);
assert_eq!((t.stereo, t.stereo_custom), (0, Some(12)));
t.save(&mut c);
assert_eq!(c.get_i32("Player", "Stereo3D"), Some(12));
ThreeD { stereo: 3, ..t }.save(&mut c);
assert_eq!(c.get_i32("Player", "Stereo3D"), Some(3));
}
#[test]
fn choosing_a_stereo_mode_turns_the_stereo_switch_on() {
let mut c = config();
ThreeD {
stereo: 4,
..ThreeD::default()
}
.save(&mut c);
assert_eq!(c.get_i32("Player", "Stereo3DEnabled"), Some(1));
ThreeD::default().save(&mut c);
assert_eq!(c.get_i32("Player", "Stereo3DEnabled"), Some(0));
}
}