slamshuffle/src/mfvi.rs

197 lines
6.1 KiB
Rust
Raw Normal View History

2024-02-04 12:09:24 -05:00
use std::collections::{BTreeMap, HashSet};
#[derive(Clone, Copy)]
2024-02-04 13:43:56 -05:00
/// Flag indicating whether special effects variants should be used.
2024-02-04 12:09:24 -05:00
pub enum SfxMode {
Off,
On,
}
2024-01-31 15:49:41 -05:00
struct Translation<'a> {
find: &'a str,
replacement: &'a str,
}
2024-02-04 12:09:24 -05:00
#[derive(Clone)]
2024-02-04 13:43:56 -05:00
/// Holds the set of delimiters that should be removed from the MML to select a variant.
pub struct Variant {
keep_delim: char,
remove_delims: HashSet<char>,
2024-02-04 12:09:24 -05:00
}
impl Variant {
2024-02-04 13:43:56 -05:00
/// Get the set of character delimiters to remove from MML for this variant.
pub fn ignores(&self) -> &HashSet<char> {
2024-02-04 12:09:24 -05:00
&self.remove_delims
}
}
2024-02-04 13:43:56 -05:00
/// The list of all variants defined in an MML file.
2024-02-04 12:09:24 -05:00
pub struct VariantList<'v> {
all_delims: HashSet<char>,
variants: BTreeMap<&'v str, Variant>,
2024-02-04 12:09:24 -05:00
}
impl<'v> VariantList<'v> {
fn new() -> Self {
VariantList {
all_delims: HashSet::new(),
variants: BTreeMap::new(),
}
}
fn finish(mut self) -> Self {
for (_, v) in self.variants.iter_mut() {
v.remove_delims = self.all_delims.clone();
v.remove_delims.remove(&v.keep_delim);
2024-02-04 12:09:24 -05:00
}
if self.variants.is_empty() {
self.variants.insert(
"_default_",
Variant {
// Not that we expect to find NUL bytes, but effectively we will remove all
// delimiters with this variant.
keep_delim: '\0',
2024-02-04 12:09:24 -05:00
remove_delims: self.all_delims.clone(),
},
);
}
self
}
2024-02-04 13:43:56 -05:00
/// Get a specific variant by name, which exists in the MML file.
2024-02-04 12:09:24 -05:00
pub fn get(&self, name: &str) -> Option<&Variant> {
self.variants.get(name)
}
}
2024-02-04 13:43:56 -05:00
/// Processes "#REPLACE" macros in MML.
///
/// Macro syntax:
/// #REPLACE c1 c2
///
/// "c1" and "c2" are meant to be single characters, but `replace_chars` finds any two strings of
/// non-whitespace. These strings are substituted in-place, so the longer string is truncated to
/// the length of the shorter.
2024-01-31 15:49:41 -05:00
pub fn replace_chars(input: &str) -> String {
let trs: Vec<Translation<'_>> = input
.lines()
.filter_map(|line| {
2024-02-04 08:53:10 -05:00
line.strip_prefix("#REPLACE")
2024-02-04 13:43:56 -05:00
.and_then(|l| l.trim().rsplit_once(char::is_whitespace))
2024-01-31 15:49:41 -05:00
.and_then(|(first, second)| {
let length = std::cmp::min(first.len(), second.len());
Some(Translation {
find: &first[0..length],
replacement: &second[0..length],
})
})
})
.collect();
let mut new = input.to_string();
for tr in trs {
new = new.replace(tr.find, tr.replacement);
}
new
}
2024-02-04 12:09:24 -05:00
2024-02-04 13:43:56 -05:00
/// Processes "#SFXV" and "#VARIANT" macros in MML.
///
/// Macro syntax:
/// #SFXV o i
/// #VARIANT c name
///
/// "o", "i", and "c" are single characters that act as markup delimiters around the variant codes.
/// For #SFXV, if special effects mode is off, the "o" variants are used, otherwise "i". The "name"
/// for one #VARIANT is optional, and its absence indicates the default variant. If all variants in
/// the input are named, the first variant will be the default.
2024-02-04 12:09:24 -05:00
pub fn get_variants<'a>(input: &'a str, sfx_mode: SfxMode) -> VariantList<'a> {
input
.lines()
.fold(VariantList::new(), |mut state, line| {
if let Some(l) = line.strip_prefix("#SFXV") {
2024-02-04 13:43:56 -05:00
if let Some((first, second)) = l.trim().rsplit_once(char::is_whitespace) {
2024-02-04 12:09:24 -05:00
use SfxMode::*;
match sfx_mode {
Off => state.all_delims.insert(first_char(first)),
On => state.all_delims.insert(first_char(second)),
2024-02-04 12:09:24 -05:00
};
}
} else if let Some(l) = line.strip_prefix("#VARIANT") {
let (first, second) = {
let rest = l.trim();
if let Some((f, s)) = rest.rsplit_once(' ') {
(first_char(f), s)
2024-02-04 12:09:24 -05:00
} else {
(first_char(rest), "_default_")
2024-02-04 12:09:24 -05:00
}
};
state.all_delims.insert(first);
if state.variants.len() == 0 && second != "_default_" {
state.variants.insert(
"_default_",
Variant {
keep_delim: first,
remove_delims: HashSet::new(),
},
);
}
state.variants.insert(
second,
Variant {
keep_delim: first,
remove_delims: HashSet::new(),
},
);
}
state
})
.finish()
}
fn first_char(s: &str) -> char {
s.chars().next().unwrap_or('\0')
}
pub fn select_variant<'i>(input: &'i str, var: &'i Variant) -> String {
let mut output = input.to_string();
// Workspace for converting char to &str in the loop.
let mut buf = [0; 4];
// Set of chars to find in `input`.
let charlist: Vec<char> = var
.remove_delims
.iter()
.copied()
.chain([var.keep_delim])
.collect();
let mut needles = input.rmatch_indices(&charlist[..]).peekable();
while let Some((idx, c)) = needles.next() {
match c {
// Remove the char if it is the `keep_delim`.
d if var.keep_delim.encode_utf8(&mut buf) == d => {
// `replace_range` doesn't make a full copy of the string the way `remove` does.
output.replace_range(idx..=idx, "");
}
// Anything else will be in `remove_delim`, so find a matching pair and remove
// them and everything between them.
d => match needles.peek() {
Some(&(idx_1, d2)) if d == d2 => {
needles.next();
output.replace_range(idx_1..=idx, "");
}
_ => {}
},
}
}
output
}