use std::collections::BTreeMap;
use std::path::Path;
use ini::Ini;
use super::Section;
#[derive(Debug, PartialEq, Eq)]
pub enum LookupError {
SectionHasNoScores,
SectionNotFound,
ParseFailed(String),
}
pub fn read_sections(vpreg_path: &Path, game_name: &str) -> Result<Vec<Section>, LookupError> {
let ini =
Ini::load_from_file(vpreg_path).map_err(|e| LookupError::ParseFailed(e.to_string()))?;
extract_sections(&ini, game_name)
}
const LEGACY_EM_ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_<";
fn extract_sections(ini: &Ini, game_name: &str) -> Result<Vec<Section>, LookupError> {
let section = ini
.section(Some(game_name))
.ok_or(LookupError::SectionNotFound)?;
let mut scores: BTreeMap<u32, &str> = BTreeMap::new();
let mut names: BTreeMap<u32, &str> = BTreeMap::new();
for (key, value) in section.iter() {
let Some(rest) = key.strip_prefix("HighScore") else {
continue;
};
if let Some(n_str) = rest.strip_prefix("Name") {
if let Ok(n) = n_str.parse::<u32>() {
names.insert(n, value);
}
continue;
}
if let Some(n_str) = rest.strip_suffix("Name") {
if let Ok(n) = n_str.parse::<u32>() {
names.insert(n, value);
}
} else if let Ok(n) = rest.parse::<u32>() {
scores.insert(n, value);
}
}
if scores.is_empty() {
if let Some(legacy) = try_legacy_em(section, game_name) {
return Ok(vec![legacy]);
}
return Err(LookupError::SectionHasNoScores);
}
let rows: Vec<Vec<String>> = scores
.into_iter()
.map(|(n, score)| {
let initials = names.get(&n).copied().unwrap_or("").trim().to_string();
vec![
format!("#{n}"),
initials,
score.trim().to_string(),
String::new(),
]
})
.collect();
let ranked = rows.len() > 1;
let header = if ranked {
"HIGH SCORES".to_string()
} else {
game_name.to_uppercase()
};
Ok(vec![Section {
header,
rows,
ranked,
}])
}
fn try_legacy_em(section: &ini::Properties, game_name: &str) -> Option<Section> {
let hiscore: u64 = ["hiscore", "HighScore"]
.iter()
.find_map(|k| get_ci(section, k))
.and_then(|v| v.trim().parse::<u64>().ok())?;
if hiscore == 0 {
return None;
}
let initials: String = ["hsa1", "hsa2", "hsa3"]
.iter()
.filter_map(|k| get_ci(section, k))
.filter_map(|v| v.trim().parse::<usize>().ok())
.filter_map(decode_legacy_em_initial)
.collect();
Some(Section {
header: game_name.to_uppercase(),
rows: vec![vec![
"HIGH SCORE".to_string(),
initials,
hiscore.to_string(),
String::new(),
]],
ranked: false,
})
}
fn get_ci<'a>(section: &'a ini::Properties, key: &str) -> Option<&'a str> {
section
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.map(|(_, v)| v)
}
fn decode_legacy_em_initial(idx_1based: usize) -> Option<char> {
if idx_1based == 0 {
return None;
}
LEGACY_EM_ALPHABET.get(idx_1based - 1).map(|&b| b as char)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn parse(content: &str) -> Ini {
Ini::load_from_str(content).expect("ini parse")
}
#[test]
fn orders_by_rank_number_not_ini_order() {
let ini = parse(
r"
[gameofthrones]
HighScore2=500000000
HighScore2Name=BBB
HighScore10=7000000
HighScore10Name=JJJ
HighScore1=750000000
HighScore1Name=AAA
",
);
let sections = extract_sections(&ini, "gameofthrones").expect("section");
let labels: Vec<&str> = sections[0].rows.iter().map(|r| r[0].as_str()).collect();
assert_eq!(labels, vec!["#1", "#2", "#10"]);
}
#[test]
fn ignores_unrelated_keys_in_the_section() {
let ini = parse(
r"
[somegame]
SETDIPS=0
HighScore1=42
HighScore1Name=FOO
MasterVol=99
Credits=3
TotalGamesPlayed=7
",
);
let sections = extract_sections(&ini, "somegame").expect("section");
assert_eq!(sections[0].rows[0], vec!["#1", "FOO", "42", ""]);
assert_eq!(sections[0].rows.len(), 1);
}
#[test]
fn returns_section_not_found_when_game_name_missing() {
let ini = parse(
r"
[OtherGame]
HighScore1=10
",
);
let err = extract_sections(&ini, "TheMatrix").expect_err("should miss");
assert_eq!(err, LookupError::SectionNotFound);
}
#[test]
fn returns_no_scores_when_section_has_only_settings() {
let ini = parse(
r"
[hh]
SETDIPS=0
",
);
let err = extract_sections(&ini, "hh").expect_err("should be empty");
assert_eq!(err, LookupError::SectionHasNoScores);
}
#[test]
fn legacy_em_missing_hsa_yields_empty_initials() {
let ini = parse(
r"
[some_em_table]
credit=0
hiscore=5000
score1=
",
);
let sections = extract_sections(&ini, "some_em_table").expect("section");
assert_eq!(sections[0].rows[0], vec!["HIGH SCORE", "", "5000", ""]);
}
#[test]
fn legacy_em_zero_hiscore_falls_through() {
let ini = parse(
r"
[some_em_table]
credit=0
hiscore=0
hsa1=0
hsa2=0
hsa3=0
",
);
let err = extract_sections(&ini, "some_em_table").expect_err("no real score");
assert_eq!(err, LookupError::SectionHasNoScores);
}
#[test]
fn legacy_em_decodes_extended_alphabet_chars() {
let ini = parse(
r"
[em_extended]
hiscore=42
hsa1=27
hsa2=37
hsa3=38
",
);
let sections = extract_sections(&ini, "em_extended").expect("section");
assert_eq!(sections[0].rows[0][1], "0_<");
}
#[test]
fn legacy_em_drops_out_of_range_hsa_index() {
let ini = parse(
r"
[em_oor]
hiscore=42
hsa1=4
hsa2=99
hsa3=7
",
);
let sections = extract_sections(&ini, "em_oor").expect("section");
assert_eq!(sections[0].rows[0][1], "DG");
}
}