use regex::Regex;
use std::collections::HashSet;
use std::fmt::Display;
use std::fs::File;
use std::io;
use std::io::{Read, Write};
use std::path::Path;
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum LineEndingsResult {
NoChanges,
Unified,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum PatchType {
DropTarget,
StandupTarget,
}
impl Display for PatchType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PatchType::DropTarget => write!(f, "DTArray fix"),
PatchType::StandupTarget => write!(f, "STArray fix"),
}
}
}
pub fn patch_vbs_file(vbs_path: &Path) -> io::Result<HashSet<PatchType>> {
let mut file = File::open(vbs_path)?;
let mut text = String::new();
file.read_to_string(&mut text)?;
let (patched_text, applied) = patch_script(text);
let mut file = File::create(vbs_path)?;
file.write_all(patched_text.as_bytes())?;
Ok(applied)
}
pub fn unify_line_endings_vbs_file(vbs_path: &Path) -> io::Result<LineEndingsResult> {
let mut file = File::open(vbs_path)?;
let mut text = String::new();
file.read_to_string(&mut text)?;
let patched_text = unify_line_endings(&text);
let mut file = File::create(vbs_path)?;
file.write_all(patched_text.as_bytes())?;
if text != patched_text {
Ok(LineEndingsResult::Unified)
} else {
Ok(LineEndingsResult::NoChanges)
}
}
pub fn patch_script(script: String) -> (String, HashSet<PatchType>) {
let mut applied_patches = HashSet::new();
let mut patched_script = script;
if patched_script.contains("DTArray(i)(0)") {
applied_patches.insert(PatchType::DropTarget);
patched_script = patch_drop_target_array(patched_script);
}
if patched_script.contains("STArray(i)(0)") {
applied_patches.insert(PatchType::StandupTarget);
patched_script = patch_standup_target_array(patched_script);
}
(patched_script, applied_patches)
}
fn unify_line_endings(script: &str) -> String {
script
.replace("\r\n", "\n")
.replace('\r', "\n")
.replace('\n', "\r\n")
}
fn patch_standup_target_array(script: String) -> String {
let re = Regex::new(r"(ST[a-zA-Z0-9]*\s*=\s*)Array\((.*?\s*,\s*[0-9]+\s*)\)").unwrap();
let mut patched_script = re
.replace_all(&script, |caps: ®ex::Captures| {
let ind = caps.get(1).unwrap().as_str();
let ind2 = caps.get(2).unwrap().as_str();
format!("Set {ind}(new StandupTarget)({ind2})")
})
.to_string();
let st_class = include_str!("assets/standup_target_class.vbs");
let marker = "'Define a variable for each stand-up target";
patched_script = introduce_class(patched_script, marker, "new StandupTarget", st_class);
patched_script = patched_script.replace("STArray(i)(0)", "STArray(i).primary");
patched_script = patched_script.replace("STArray(i)(1)", "STArray(i).prim");
patched_script = patched_script.replace("STArray(i)(2)", "STArray(i).sw");
patched_script = patched_script.replace("STArray(i)(3)", "STArray(i).animate");
patched_script
}
fn patch_drop_target_array(script: String) -> String {
let re =
Regex::new(r"(DT[a-zA-Z0-9]*\s*=\s*)Array\((.*?\s*,\s*[0-9]+\s*)(,\s*(false|true))?\)")
.unwrap();
let mut patched_script = re
.replace_all(&script, |caps: ®ex::Captures| {
let ind = caps.get(1).unwrap().as_str();
let ind2 = caps.get(2).unwrap().as_str();
let ind3 = caps.get(3);
let false_true = match ind3 {
Some(c) => c.as_str().to_string(),
None => ", false".to_string(),
};
format!("Set {ind}(new DropTarget)({ind2}{false_true})")
})
.to_string();
let dt_class = include_str!("assets/drop_target_class.vbs");
let marker = "'Define a variable for each drop target";
patched_script = introduce_class(patched_script, marker, "new DropTarget", dt_class);
patched_script = patched_script.replace("DTArray(i)(0)", "DTArray(i).primary");
patched_script = patched_script.replace("DTArray(i)(1)", "DTArray(i).secondary");
patched_script = patched_script.replace("DTArray(i)(2)", "DTArray(i).prim");
patched_script = patched_script.replace("DTArray(i)(3)", "DTArray(i).sw");
patched_script = patched_script.replace("DTArray(i)(4)", "DTArray(i).animate");
patched_script = patched_script.replace("DTArray(i)(5)", "DTArray(i).isDropped");
patched_script = patched_script.replace("DTArray(ind)(5)", "DTArray(ind).isDropped");
patched_script
}
fn introduce_class(script: String, marker: &str, fallback_marker: &str, class_def: &str) -> String {
if script.match_indices(marker).count() == 1 {
script.replace(marker, format!("{class_def}\r\n{marker}").as_str())
} else {
let regex = format!(r"\r\n(.*?)({fallback_marker})");
let re = Regex::new(regex.as_ref()).unwrap();
if re.is_match(&script) {
re.replace(&script, |caps: ®ex::Captures| {
let first = caps.get(1).unwrap().as_str();
let second = caps.get(2).unwrap().as_str();
format!("\r\n{class_def}\r\n{first}{second}")
})
.to_string()
} else {
format!("{script}\r\n{class_def}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_unify_line_endings() {
let script = "first\nsecond\r\nthird\rfourth";
let expected = "first\r\nsecond\r\nthird\r\nfourth";
let result = unify_line_endings(script);
assert_eq!(expected, result);
}
#[test]
fn test_introduce_class_at_marker() {
let script = r#"
hello
this is the line
this is the other line
end"#;
let marker = "this is the line";
let fallback_marker = "other";
let class_def = "Class Foo\r\nEnd Class\r\n";
let expected = r#"
hello
Class Foo
End Class
this is the line
this is the other line
end"#;
let script = script.replace('\n', "\r\n");
let expected = expected.replace('\n', "\r\n");
let result = introduce_class(script.to_string(), marker, fallback_marker, class_def);
assert_eq!(expected, result);
}
#[test]
fn test_introduce_class_at_fallback() {
let script = r#"
hello
this is the line
this is the other line
end"#;
let marker = "missing";
let fallback_marker = "other";
let class_def = "Class Foo\r\nEnd Class\r\n";
let expected = r#"
hello
this is the line
Class Foo
End Class
this is the other line
end"#;
let script = script.replace('\n', "\r\n");
let expected = expected.replace('\n', "\r\n");
let result = introduce_class(script.to_string(), marker, fallback_marker, class_def);
assert_eq!(expected, result);
}
#[test]
fn test_introduce_class_at_end() {
let script = r#"
hello
end"#;
let marker = "missing";
let fallback_marker = "also missing";
let class_def = "Class Foo\r\nEnd Class\r\n";
let expected = r#"
hello
end
Class Foo
End Class
"#;
let script = script.replace('\n', "\r\n");
let expected = expected.replace('\n', "\r\n");
let result = introduce_class(script.to_string(), marker, fallback_marker, class_def);
assert_eq!(expected, result);
}
#[test]
fn test_vbs_patch() {
let script = r#"
'Define a variable for each drop target
Dim DT9, DT47, DTA1v, DTJKv
DTBk9=Array(sw9, sw9a, sw9p, 9, 0, true)
DT47 = Array(sw47, sw47a, sw47p, 47, 0)
DTA1v = Array(DTA1, DTA1a, DTA1p, 1, 0)
DTJKv = Array(DTJK, DTJKa, DTJKp, 3, 0)
Dim DTArray
DTArray = Array(DTBk9,DT47,DTA1v,DTJKv)
Sub DoDTAnim()
Dim i
For i=0 to Ubound(DTArray)
DTArray(i)(4) = DTAnimate(DTArray(i)(0),DTArray(i)(1),DTArray(i)(2),DTArray(i)(3),DTArray(i)(4))
Next
End Sub
'Define a variable for each stand-up target
Dim ST41, ST42
ST41= Array(sw41, Target_Rect_Fat_011_BM_Lit_Room, 41, 0)
ST42 = Array(sw42, Target_Rect_Fat_010_BM_Lit_Room, 42, 0)
Dim STArray
STArray = Array(ST41,ST42)
Sub DoSTAnim()
Dim i
For i=0 to Ubound(STArray)
STArray(i)(3) = STAnimate(STArray(i)(0),STArray(i)(1),STArray(i)(2),STArray(i)(3))
Next
End Sub
"#;
let script = script.replace('\n', "\r\n");
let expected = r#"
Class DropTarget
Private m_primary, m_secondary, m_prim, m_sw, m_animate, m_isDropped
Public Property Get Primary(): Set Primary = m_primary: End Property
Public Property Let Primary(input): Set m_primary = input: End Property
Public Property Get Secondary(): Set Secondary = m_secondary: End Property
Public Property Let Secondary(input): Set m_secondary = input: End Property
Public Property Get Prim(): Set Prim = m_prim: End Property
Public Property Let Prim(input): Set m_prim = input: End Property
Public Property Get Sw(): Sw = m_sw: End Property
Public Property Let Sw(input): m_sw = input: End Property
Public Property Get Animate(): Animate = m_animate: End Property
Public Property Let Animate(input): m_animate = input: End Property
Public Property Get IsDropped(): IsDropped = m_isDropped: End Property
Public Property Let IsDropped(input): m_isDropped = input: End Property
Public default Function init(primary, secondary, prim, sw, animate, isDropped)
Set m_primary = primary
Set m_secondary = secondary
Set m_prim = prim
m_sw = sw
m_animate = animate
m_isDropped = isDropped
Set Init = Me
End Function
End Class
'Define a variable for each drop target
Dim DT9, DT47, DTA1v, DTJKv
Set DTBk9=(new DropTarget)(sw9, sw9a, sw9p, 9, 0, true)
Set DT47 = (new DropTarget)(sw47, sw47a, sw47p, 47, 0, false)
Set DTA1v = (new DropTarget)(DTA1, DTA1a, DTA1p, 1, 0, false)
Set DTJKv = (new DropTarget)(DTJK, DTJKa, DTJKp, 3, 0, false)
Dim DTArray
DTArray = Array(DTBk9,DT47,DTA1v,DTJKv)
Sub DoDTAnim()
Dim i
For i=0 to Ubound(DTArray)
DTArray(i).animate = DTAnimate(DTArray(i).primary,DTArray(i).secondary,DTArray(i).prim,DTArray(i).sw,DTArray(i).animate)
Next
End Sub
Class StandupTarget
Private m_primary, m_prim, m_sw, m_animate
Public Property Get Primary(): Set Primary = m_primary: End Property
Public Property Let Primary(input): Set m_primary = input: End Property
Public Property Get Prim(): Set Prim = m_prim: End Property
Public Property Let Prim(input): Set m_prim = input: End Property
Public Property Get Sw(): Sw = m_sw: End Property
Public Property Let Sw(input): m_sw = input: End Property
Public Property Get Animate(): Animate = m_animate: End Property
Public Property Let Animate(input): m_animate = input: End Property
Public default Function init(primary, prim, sw, animate)
Set m_primary = primary
Set m_prim = prim
m_sw = sw
m_animate = animate
Set Init = Me
End Function
End Class
'Define a variable for each stand-up target
Dim ST41, ST42
Set ST41= (new StandupTarget)(sw41, Target_Rect_Fat_011_BM_Lit_Room, 41, 0)
Set ST42 = (new StandupTarget)(sw42, Target_Rect_Fat_010_BM_Lit_Room, 42, 0)
Dim STArray
STArray = Array(ST41,ST42)
Sub DoSTAnim()
Dim i
For i=0 to Ubound(STArray)
STArray(i).animate = STAnimate(STArray(i).primary,STArray(i).prim,STArray(i).sw,STArray(i).animate)
Next
End Sub
"#;
let expected = expected.replace('\n', "\r\n");
let (result, applied) = patch_script(script.to_string());
assert_eq!(
applied,
HashSet::from([PatchType::DropTarget, PatchType::StandupTarget])
);
assert_eq!(expected, result);
}
}