fix(config): import original INI semantics

Load case-insensitive Language, Speed, and singular Sound values from an adjacent or embedded TDKPIN.INI before portable JSON exists, preserving Win16 defaults and clamps. Separate runtime playback from stored settings so F12 remains a silent, non-persistent toggle exactly like the original.

Test Plan:
- cargo test --all-targets
- cargo clippy --all-targets --all-features -- -D warnings
- rumdl check CHANGELOG.md RECONSTRUCTION.md README.md
- git diff --check
This commit is contained in:
2026-08-23 19:12:31 +02:00
parent fae322bb91
commit 0e7208e03a
5 changed files with 94 additions and 28 deletions
+11 -23
View File
@@ -108,6 +108,7 @@ pub struct App {
assets: Assets,
persistence: Persistence,
saved: SavedData,
sounds_enabled: bool,
render_target: RenderTarget,
screen: Screen,
return_screen: Screen,
@@ -116,8 +117,6 @@ pub struct App {
loading_started: f64,
loading_until: f64,
last_help_click: f64,
message: String,
message_until: f64,
name: String,
pending_score: u32,
attract: AttractAnimation,
@@ -128,6 +127,7 @@ impl App {
let assets = Assets::load().await;
let persistence = Persistence::new();
let saved = persistence.load();
let sounds_enabled = saved.settings.sounds;
let render_target = render_target(640, 460);
render_target.texture.set_filter(FilterMode::Nearest);
let loading_started = get_time();
@@ -135,6 +135,7 @@ impl App {
assets,
persistence,
saved,
sounds_enabled,
render_target,
screen: Screen::Loading,
return_screen: Screen::Attract,
@@ -143,8 +144,6 @@ impl App {
loading_started,
loading_until: loading_started + 1.1,
last_help_click: -1.0,
message: String::new(),
message_until: 0.0,
name: String::new(),
pending_score: 0,
attract: AttractAnimation::default(),
@@ -224,15 +223,7 @@ impl App {
fn handle_global_input(&mut self) {
if is_key_pressed(KeyCode::F12) {
self.saved.settings.sounds = !self.saved.settings.sounds;
if self.saved.settings.sounds {
"SOUND ON"
} else {
"SOUND OFF"
}
.clone_into(&mut self.message);
self.message_until = get_time() + 1.2;
self.save();
self.sounds_enabled = !self.sounds_enabled;
}
if is_key_pressed(KeyCode::F1) && self.screen != Screen::NameEntry {
if self.screen == Screen::Help {
@@ -274,7 +265,7 @@ impl App {
if is_key_pressed(KeyCode::KpAdd) || is_key_pressed(KeyCode::Equal) {
self.game = Some(Game::new(1));
self.screen = Screen::Playing;
self.assets.play(2001, self.saved.settings.sounds);
self.assets.play(2001, self.sounds_enabled);
}
}
@@ -285,7 +276,7 @@ impl App {
.as_mut()
.is_some_and(Game::add_player_before_launch)
{
self.assets.play(2001, self.saved.settings.sounds);
self.assets.play(2001, self.sounds_enabled);
}
let left_flipper = is_key_down(KeyCode::LeftControl)
|| is_key_down(KeyCode::A)
@@ -340,7 +331,7 @@ impl App {
return;
}
if let Some(resource) = event.sound_resource() {
self.assets.play(resource, self.saved.settings.sounds);
self.assets.play(resource, self.sounds_enabled);
}
}
@@ -390,7 +381,8 @@ impl App {
self.saved.settings.speed = u8::try_from(speed.clamp(1, 5)).unwrap_or(3);
}
1 => {
self.saved.settings.sounds = !self.saved.settings.sounds;
self.sounds_enabled = !self.sounds_enabled;
self.saved.settings.sounds = self.sounds_enabled;
}
_ => {
let current = Language::ALL
@@ -405,7 +397,7 @@ impl App {
self.saved.settings.language = Language::ALL[next];
}
}
self.assets.play(2012, self.saved.settings.sounds);
self.assets.play(2012, self.sounds_enabled);
self.save();
}
@@ -484,10 +476,6 @@ impl App {
Screen::HighScores => self.draw_high_scores(),
Screen::NameEntry => self.draw_name_entry(),
}
if get_time() < self.message_until {
draw_panel(232.0, 205.0, 176.0, 48.0);
draw_centered(&self.message, 229.0, 24, BLACK);
}
}
fn draw_attract(&self) {
@@ -1080,7 +1068,7 @@ impl App {
format!("GRAPHICS DETAIL / SPEED: {}", self.saved.settings.speed),
format!(
"SOUND: {}",
if self.saved.settings.sounds {
if self.sounds_enabled {
"ON"
} else {
"OFF"
+75 -2
View File
@@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
use std::{fs, io, path::PathBuf};
const ORIGINAL_HIGHSCORES: &[u8] = include_bytes!("../assets/original/HISCORES.DAT");
const ORIGINAL_INI: &str = include_str!("../../original/TDKPIN.INI");
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum Language {
@@ -67,7 +68,7 @@ pub struct SavedData {
impl Default for SavedData {
fn default() -> Self {
Self {
settings: Settings::default(),
settings: parse_original_settings(ORIGINAL_INI),
high_scores: parse_original_high_scores(ORIGINAL_HIGHSCORES),
}
}
@@ -90,7 +91,11 @@ impl Persistence {
fs::read_to_string(&self.path)
.ok()
.and_then(|text| serde_json::from_str(&text).ok())
.unwrap_or_default()
.unwrap_or_else(|| SavedData {
settings: load_adjacent_original_settings()
.unwrap_or_else(|| parse_original_settings(ORIGINAL_INI)),
high_scores: parse_original_high_scores(ORIGINAL_HIGHSCORES),
})
}
pub fn save(&self, data: &SavedData) -> io::Result<()> {
@@ -112,6 +117,51 @@ impl Persistence {
}
}
fn load_adjacent_original_settings() -> Option<Settings> {
let executable = std::env::current_exe().ok()?;
let exact_name = executable.with_extension("INI");
let original_name = executable.parent()?.join("TDKPIN.INI");
[exact_name, original_name]
.into_iter()
.find_map(|path| fs::read_to_string(path).ok())
.map(|text| parse_original_settings(&text))
}
pub fn parse_original_settings(text: &str) -> Settings {
let mut settings = Settings::default();
let mut in_settings = false;
for line in text.lines() {
let line = line.trim();
if line.starts_with('[') && line.ends_with(']') {
in_settings = line[1..line.len() - 1].eq_ignore_ascii_case("Settings");
continue;
}
if !in_settings || line.is_empty() || line.starts_with(';') {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
let Ok(value) = value.trim().parse::<i32>() else {
continue;
};
if key.trim().eq_ignore_ascii_case("Language") {
settings.language = match value {
2 => Language::German,
3 => Language::French,
4 => Language::Italian,
5 => Language::Spanish,
_ => Language::English,
};
} else if key.trim().eq_ignore_ascii_case("Speed") {
settings.speed = u8::try_from(value).ok().filter(|speed| (1..=5).contains(speed)).unwrap_or(3);
} else if key.trim().eq_ignore_ascii_case("Sound") {
settings.sounds = value != 0;
}
}
settings
}
pub fn parse_original_high_scores(bytes: &[u8]) -> Vec<HighScore> {
const HEADER: usize = 16;
const COUNT: usize = 10;
@@ -194,6 +244,29 @@ mod tests {
);
}
#[test]
fn imports_the_distributed_original_ini_with_win16_key_names() {
let settings = parse_original_settings(ORIGINAL_INI);
assert_eq!(settings.speed, 3);
assert_eq!(settings.language, Language::German);
assert!(settings.sounds, "the original reads singular Sound, not Sounds");
}
#[test]
fn original_ini_values_are_case_insensitive_and_clamped() {
let settings = parse_original_settings(
"[settings]\nlanguage=5\nSPEED=9\nSound=0\nSounds=1\n",
);
assert_eq!(settings.speed, 3);
assert_eq!(settings.language, Language::Spanish);
assert!(!settings.sounds);
let defaults = parse_original_settings("[Settings]\nLanguage=-1\nSpeed=0\n");
assert_eq!(defaults.speed, 3);
assert_eq!(defaults.language, Language::English);
assert!(defaults.sounds);
}
#[test]
fn rejects_a_table_without_the_original_signature() {
let mut corrupt = ORIGINAL_HIGHSCORES.to_vec();