mirror of
https://github.com/widberg/bff
synced 2026-08-23 14:26:03 -04:00
Add a TSC executor
This commit is contained in:
parent
6fb040b3c4
commit
3a85c5fcad
5 changed files with 944 additions and 0 deletions
|
|
@ -60,6 +60,7 @@ A ✔ indicates that the format has been tested and is working. An ❌ indicates
|
|||
|
||||
| Format | Status |
|
||||
|--------|--------|
|
||||
| tsc | ✔ |
|
||||
| csc | ✔ |
|
||||
| psc | ✔ |
|
||||
| CPS | ✔ |
|
||||
|
|
|
|||
|
|
@ -2,8 +2,14 @@ mod cps;
|
|||
mod csc;
|
||||
mod mqfel_settings_bin;
|
||||
mod psc;
|
||||
#[expect(
|
||||
clippy::module_inception,
|
||||
reason = "tsc.rs is the implementation module for the tsc format"
|
||||
)]
|
||||
mod tsc;
|
||||
|
||||
pub use cps::*;
|
||||
pub use csc::*;
|
||||
pub use mqfel_settings_bin::*;
|
||||
pub use psc::*;
|
||||
pub use tsc::*;
|
||||
|
|
|
|||
831
bff/src/tsc/tsc.rs
Normal file
831
bff/src/tsc/tsc.rs
Normal file
|
|
@ -0,0 +1,831 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::error::Error;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::{fmt, fs};
|
||||
|
||||
use encoding_rs::WINDOWS_1252;
|
||||
|
||||
// TODO: This should all probably work better with the other tsc formats.
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Script {
|
||||
pub commands: Vec<Command>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CommandArgument {
|
||||
pub string: String,
|
||||
pub number: Option<f32>,
|
||||
}
|
||||
|
||||
impl CommandArgument {
|
||||
#[must_use]
|
||||
pub fn new(string: impl Into<String>) -> Self {
|
||||
let string = string.into();
|
||||
let numeric_string = string.strip_suffix('f').unwrap_or(&string);
|
||||
let number = match normalize(numeric_string).as_str() {
|
||||
"TRUE" | "ON" => Some(1.0),
|
||||
"FALSE" | "OFF" => Some(0.0),
|
||||
_ => numeric_string.parse::<f32>().ok(),
|
||||
};
|
||||
Self { string, number }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Command {
|
||||
pub command_name: String,
|
||||
pub arguments: Vec<CommandArgument>,
|
||||
pub line: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Condition {
|
||||
first: String,
|
||||
rest: Vec<(LogicalOperator, String)>,
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
fn parse(arguments: &[CommandArgument], single_name: bool) -> Result<Self, String> {
|
||||
let Some(first) = arguments.first() else {
|
||||
return Err("conditional directive requires an expression".to_owned());
|
||||
};
|
||||
|
||||
if single_name && arguments.len() != 1 {
|
||||
return Err("conditional directive requires exactly one variable name".to_owned());
|
||||
}
|
||||
if arguments.len().is_multiple_of(2) {
|
||||
return Err("conditional expression must alternate names and operators".to_owned());
|
||||
}
|
||||
|
||||
let mut rest = Vec::new();
|
||||
for [operator, name] in arguments[1..].as_chunks::<2>().0 {
|
||||
let operator = match operator.string.as_str() {
|
||||
"&&" => LogicalOperator::And,
|
||||
"||" => LogicalOperator::Or,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"unknown conditional operator {:?}",
|
||||
operator.string
|
||||
));
|
||||
}
|
||||
};
|
||||
rest.push((operator, name.string.clone()));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
first: first.string.clone(),
|
||||
rest,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum LogicalOperator {
|
||||
And,
|
||||
Or,
|
||||
}
|
||||
|
||||
pub trait ScriptParser {
|
||||
type Script;
|
||||
type Error;
|
||||
|
||||
fn parse(&self, input: &str) -> Result<Self::Script, Self::Error>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct AsoboParser;
|
||||
|
||||
impl ScriptParser for AsoboParser {
|
||||
type Script = Script;
|
||||
type Error = ParseError;
|
||||
|
||||
fn parse(&self, input: &str) -> Result<Script, ParseError> {
|
||||
let uncommented = strip_comments(input);
|
||||
let mut commands = Vec::new();
|
||||
|
||||
for (line_index, line) in uncommented.lines().enumerate() {
|
||||
let line_number = line_index + 1;
|
||||
let arguments = tokenize(line, line_number)?;
|
||||
let Some((name, arguments)) = arguments.split_first() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
commands.push(Command {
|
||||
command_name: name.string.clone(),
|
||||
arguments: arguments.to_vec(),
|
||||
line: line_number,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Script { commands })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ParseError {
|
||||
pub line: usize,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, "line {}: {}", self.line, self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseError {}
|
||||
|
||||
pub trait ScriptExecutor<S> {
|
||||
type Error;
|
||||
|
||||
fn execute(&mut self, script: &S) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
pub trait ScriptLoader {
|
||||
fn load(&self, path: &Path) -> std::io::Result<String>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileSystemScriptLoader {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl FileSystemScriptLoader {
|
||||
#[must_use]
|
||||
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||
Self { root: root.into() }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
}
|
||||
|
||||
impl ScriptLoader for FileSystemScriptLoader {
|
||||
fn load(&self, path: &Path) -> std::io::Result<String> {
|
||||
let bytes = fs::read(self.root.join(path))?;
|
||||
Ok(WINDOWS_1252.decode(&bytes).0.into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
pub type CommandCallback<P, L, U> =
|
||||
Rc<dyn Fn(&mut AsoboExecutor<P, L, U>, &Command) -> Result<(), ExecutionError>>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CommandKey {
|
||||
pub full_name: String,
|
||||
pub short_name: String,
|
||||
}
|
||||
|
||||
impl CommandKey {
|
||||
#[must_use]
|
||||
pub fn new(command_name: impl AsRef<str>) -> Self {
|
||||
let command_name = command_name.as_ref();
|
||||
Self {
|
||||
full_name: normalize(command_name),
|
||||
short_name: command_name
|
||||
.chars()
|
||||
.filter(|character| character.is_ascii_uppercase() || character.is_ascii_digit())
|
||||
.take(15)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn matches(&self, command_name: &str) -> bool {
|
||||
command_name == self.full_name
|
||||
|| (!self.short_name.is_empty() && command_name == self.short_name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ExecutionError {
|
||||
Parse {
|
||||
path: PathBuf,
|
||||
source: ParseError,
|
||||
},
|
||||
Io {
|
||||
path: PathBuf,
|
||||
source: std::io::Error,
|
||||
},
|
||||
MissingSourcePath {
|
||||
command: String,
|
||||
line: usize,
|
||||
},
|
||||
InvalidDirective {
|
||||
command: String,
|
||||
line: usize,
|
||||
message: String,
|
||||
},
|
||||
UnmatchedElse {
|
||||
line: usize,
|
||||
},
|
||||
DuplicateElse {
|
||||
line: usize,
|
||||
},
|
||||
UnmatchedEndIf {
|
||||
line: usize,
|
||||
},
|
||||
UnterminatedIf,
|
||||
}
|
||||
|
||||
impl fmt::Display for ExecutionError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Parse { path, source } => {
|
||||
write!(formatter, "failed to parse {}: {source}", path.display())
|
||||
}
|
||||
Self::Io { path, source } => {
|
||||
write!(formatter, "failed to load {}: {source}", path.display())
|
||||
}
|
||||
Self::MissingSourcePath { command, line } => {
|
||||
write!(formatter, "line {line}: {command} requires a script path")
|
||||
}
|
||||
Self::InvalidDirective {
|
||||
command,
|
||||
line,
|
||||
message,
|
||||
} => write!(formatter, "line {line}: invalid {command}: {message}"),
|
||||
Self::UnmatchedElse { line } => write!(formatter, "line {line}: #else without #if"),
|
||||
Self::DuplicateElse { line } => write!(formatter, "line {line}: duplicate #else"),
|
||||
Self::UnmatchedEndIf { line } => write!(formatter, "line {line}: #endif without #if"),
|
||||
Self::UnterminatedIf => formatter.write_str("script ended before #endif"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ExecutionError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
Self::Parse { source, .. } => Some(source),
|
||||
Self::Io { source, .. } => Some(source),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum ExecutionFrame {
|
||||
CommandLine,
|
||||
Script {
|
||||
path: PathBuf,
|
||||
arguments: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct ConditionalFrame {
|
||||
parent_active: bool,
|
||||
condition: bool,
|
||||
active: bool,
|
||||
saw_else: bool,
|
||||
}
|
||||
|
||||
pub struct AsoboExecutor<P = AsoboParser, L = FileSystemScriptLoader, U = ()> {
|
||||
parser: P,
|
||||
loader: L,
|
||||
variables: BTreeSet<String>,
|
||||
user_data: U,
|
||||
handlers: Vec<(CommandKey, CommandCallback<P, L, U>)>,
|
||||
default_callback: Option<CommandCallback<P, L, U>>,
|
||||
}
|
||||
|
||||
impl<L> AsoboExecutor<AsoboParser, L, ()>
|
||||
where
|
||||
L: ScriptLoader,
|
||||
{
|
||||
#[must_use]
|
||||
pub fn new(loader: L) -> Self {
|
||||
Self::with_parser(AsoboParser, loader)
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, U> AsoboExecutor<AsoboParser, L, U>
|
||||
where
|
||||
L: ScriptLoader,
|
||||
{
|
||||
#[must_use]
|
||||
pub fn with_user_data(loader: L, user_data: U) -> Self {
|
||||
Self::with_parser_and_user_data(AsoboParser, loader, user_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, L, U> AsoboExecutor<P, L, U> {
|
||||
pub fn set_variable(&mut self, name: impl AsRef<str>) {
|
||||
self.variables.insert(normalize(name.as_ref()));
|
||||
}
|
||||
|
||||
pub fn unset_variable(&mut self, name: impl AsRef<str>) {
|
||||
self.variables.remove(&normalize(name.as_ref()));
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn has_variable(&self, name: impl AsRef<str>) -> bool {
|
||||
self.variables.contains(&normalize(name.as_ref()))
|
||||
}
|
||||
|
||||
pub fn variables(&self) -> impl Iterator<Item = &str> {
|
||||
self.variables.iter().map(String::as_str)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn user_data(&self) -> &U {
|
||||
&self.user_data
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn user_data_mut(&mut self) -> &mut U {
|
||||
&mut self.user_data
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_user_data(self) -> U {
|
||||
self.user_data
|
||||
}
|
||||
|
||||
pub fn on_command<F>(&mut self, command_name: impl AsRef<str>, callback: F)
|
||||
where
|
||||
F: Fn(&mut Self, &Command) -> Result<(), ExecutionError> + 'static,
|
||||
{
|
||||
self.handlers
|
||||
.push((CommandKey::new(command_name), Rc::new(callback)));
|
||||
}
|
||||
|
||||
pub fn remove_command(&mut self, command_name: impl AsRef<str>) -> bool {
|
||||
let command_name = normalize(command_name);
|
||||
let Some(index) = self
|
||||
.handlers
|
||||
.iter()
|
||||
.rposition(|(key, _)| key.matches(&command_name))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
self.handlers.remove(index);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn on_default<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut Self, &Command) -> Result<(), ExecutionError> + 'static,
|
||||
{
|
||||
self.default_callback = Some(Rc::new(callback));
|
||||
}
|
||||
|
||||
pub fn remove_default(&mut self) -> bool {
|
||||
self.default_callback.take().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, L> AsoboExecutor<P, L, ()>
|
||||
where
|
||||
P: ScriptParser<Script = Script, Error = ParseError>,
|
||||
L: ScriptLoader,
|
||||
{
|
||||
#[must_use]
|
||||
pub fn with_parser(parser: P, loader: L) -> Self {
|
||||
Self::with_parser_and_user_data(parser, loader, ())
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, L, U> AsoboExecutor<P, L, U>
|
||||
where
|
||||
P: ScriptParser<Script = Script, Error = ParseError>,
|
||||
L: ScriptLoader,
|
||||
{
|
||||
#[must_use]
|
||||
pub fn with_parser_and_user_data(parser: P, loader: L, user_data: U) -> Self {
|
||||
let mut executor = Self {
|
||||
parser,
|
||||
loader,
|
||||
variables: BTreeSet::new(),
|
||||
user_data,
|
||||
handlers: Vec::new(),
|
||||
default_callback: None,
|
||||
};
|
||||
executor.register_default_handlers();
|
||||
executor
|
||||
}
|
||||
|
||||
pub fn execute_file(&mut self, path: impl Into<PathBuf>) -> Result<(), ExecutionError> {
|
||||
self.execute_loaded(path.into(), Vec::new())
|
||||
}
|
||||
|
||||
pub fn execute_script_text(
|
||||
&mut self,
|
||||
name: impl Into<PathBuf>,
|
||||
text: &str,
|
||||
arguments: impl IntoIterator<Item = impl Into<String>>,
|
||||
) -> Result<(), ExecutionError> {
|
||||
let path = name.into();
|
||||
let script = self
|
||||
.parser
|
||||
.parse(text)
|
||||
.map_err(|source| ExecutionError::Parse {
|
||||
path: path.clone(),
|
||||
source,
|
||||
})?;
|
||||
let frame = ExecutionFrame::Script {
|
||||
path,
|
||||
arguments: arguments.into_iter().map(Into::into).collect(),
|
||||
};
|
||||
self.execute_script(&script, &frame)
|
||||
}
|
||||
|
||||
pub fn execute_command_line(&mut self, line: &str) -> Result<(), ExecutionError> {
|
||||
let script = self
|
||||
.parser
|
||||
.parse(line)
|
||||
.map_err(|source| ExecutionError::Parse {
|
||||
path: PathBuf::from("<command line>"),
|
||||
source,
|
||||
})?;
|
||||
self.execute_script(&script, &ExecutionFrame::CommandLine)
|
||||
}
|
||||
|
||||
fn execute_loaded(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
arguments: Vec<String>,
|
||||
) -> Result<(), ExecutionError> {
|
||||
let text = self
|
||||
.loader
|
||||
.load(&path)
|
||||
.map_err(|source| ExecutionError::Io {
|
||||
path: path.clone(),
|
||||
source,
|
||||
})?;
|
||||
let script = self
|
||||
.parser
|
||||
.parse(&text)
|
||||
.map_err(|source| ExecutionError::Parse {
|
||||
path: path.clone(),
|
||||
source,
|
||||
})?;
|
||||
self.execute_script(&script, &ExecutionFrame::Script { path, arguments })
|
||||
}
|
||||
|
||||
fn execute_script(
|
||||
&mut self,
|
||||
script: &Script,
|
||||
frame: &ExecutionFrame,
|
||||
) -> Result<(), ExecutionError> {
|
||||
let mut conditionals = Vec::new();
|
||||
|
||||
for command in &script.commands {
|
||||
let command = expand_command(command, frame);
|
||||
if is_asobo_directive(&command.command_name) {
|
||||
self.execute_directive(&command, &mut conditionals)?;
|
||||
} else if is_active(&conditionals) {
|
||||
self.execute_command(command)?;
|
||||
}
|
||||
}
|
||||
|
||||
if conditionals.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ExecutionError::UnterminatedIf)
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_directive(
|
||||
&mut self,
|
||||
command: &Command,
|
||||
conditionals: &mut Vec<ConditionalFrame>,
|
||||
) -> Result<(), ExecutionError> {
|
||||
match normalize(&command.command_name).as_str() {
|
||||
"#SET" if is_active(conditionals) => {
|
||||
let name = exactly_one_argument(command)?;
|
||||
self.set_variable(&name.string);
|
||||
}
|
||||
"#UNSET" if is_active(conditionals) => {
|
||||
let name = exactly_one_argument(command)?;
|
||||
self.unset_variable(&name.string);
|
||||
}
|
||||
"#IF" | "#IFDEF" | "#IFNOT" | "#IFNDEF" => {
|
||||
let parent_active = is_active(conditionals);
|
||||
let mut value = self.evaluate(
|
||||
&Condition::parse(
|
||||
&command.arguments,
|
||||
matches!(
|
||||
normalize(&command.command_name).as_str(),
|
||||
"#IFDEF" | "#IFNOT" | "#IFNDEF"
|
||||
),
|
||||
)
|
||||
.map_err(|message| invalid_directive(command, message))?,
|
||||
);
|
||||
if matches!(
|
||||
normalize(&command.command_name).as_str(),
|
||||
"#IFNOT" | "#IFNDEF"
|
||||
) {
|
||||
value = !value;
|
||||
}
|
||||
conditionals.push(ConditionalFrame {
|
||||
parent_active,
|
||||
condition: value,
|
||||
active: parent_active && value,
|
||||
saw_else: false,
|
||||
});
|
||||
}
|
||||
"#ELSE" => {
|
||||
require_no_arguments(command)?;
|
||||
let Some(conditional) = conditionals.last_mut() else {
|
||||
return Err(ExecutionError::UnmatchedElse { line: command.line });
|
||||
};
|
||||
if conditional.saw_else {
|
||||
return Err(ExecutionError::DuplicateElse { line: command.line });
|
||||
}
|
||||
conditional.saw_else = true;
|
||||
conditional.active = conditional.parent_active && !conditional.condition;
|
||||
}
|
||||
"#ENDIF" => {
|
||||
require_no_arguments(command)?;
|
||||
if conditionals.pop().is_none() {
|
||||
return Err(ExecutionError::UnmatchedEndIf { line: command.line });
|
||||
}
|
||||
}
|
||||
"#DEFINE" if is_active(conditionals) => {
|
||||
let Some((command_name, arguments)) = command.arguments.split_first() else {
|
||||
return Err(invalid_directive(command, "requires a command"));
|
||||
};
|
||||
self.execute_command(Command {
|
||||
command_name: command_name.string.clone(),
|
||||
arguments: arguments.to_vec(),
|
||||
line: command.line,
|
||||
})?;
|
||||
}
|
||||
"#SET" | "#UNSET" | "#DEFINE" => {}
|
||||
_ => unreachable!("only Asobo directives are sent to execute_directive"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn evaluate(&self, condition: &Condition) -> bool {
|
||||
let mut value = self.has_variable(&condition.first);
|
||||
for (operator, name) in &condition.rest {
|
||||
let next = self.has_variable(name);
|
||||
value = match operator {
|
||||
LogicalOperator::And => value && next,
|
||||
LogicalOperator::Or => value || next,
|
||||
};
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
fn execute_command(&mut self, command: Command) -> Result<(), ExecutionError> {
|
||||
let name = normalize(&command.command_name);
|
||||
if let Some(handler) = self
|
||||
.handlers
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|(key, _)| key.matches(&name))
|
||||
.map(|(_, handler)| Rc::clone(handler))
|
||||
{
|
||||
return handler(self, &command);
|
||||
}
|
||||
|
||||
if let Some(callback) = self.default_callback.clone() {
|
||||
callback(self, &command)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_default_handlers(&mut self) {
|
||||
self.on_command("SouRCe", |executor, command| {
|
||||
executor.execute_source_command(command)
|
||||
});
|
||||
self.on_command("BSouRCe", |executor, command| {
|
||||
executor.execute_source_command(command)
|
||||
});
|
||||
}
|
||||
|
||||
fn execute_source_command(&mut self, command: &Command) -> Result<(), ExecutionError> {
|
||||
let Some(path) = command.arguments.first() else {
|
||||
return Err(ExecutionError::MissingSourcePath {
|
||||
command: command.command_name.clone(),
|
||||
line: command.line,
|
||||
});
|
||||
};
|
||||
let arguments = command.arguments[1..]
|
||||
.iter()
|
||||
.map(|argument| argument.string.clone())
|
||||
.collect();
|
||||
self.execute_loaded(PathBuf::from(path.string.replace('\\', "/")), arguments)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, L, U> ScriptExecutor<Script> for AsoboExecutor<P, L, U>
|
||||
where
|
||||
P: ScriptParser<Script = Script, Error = ParseError>,
|
||||
L: ScriptLoader,
|
||||
{
|
||||
type Error = ExecutionError;
|
||||
|
||||
fn execute(&mut self, script: &Script) -> Result<(), Self::Error> {
|
||||
self.execute_script(script, &ExecutionFrame::CommandLine)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_asobo_directive(command_name: &str) -> bool {
|
||||
matches!(
|
||||
normalize(command_name).as_str(),
|
||||
"#SET"
|
||||
| "#UNSET"
|
||||
| "#IF"
|
||||
| "#IFDEF"
|
||||
| "#IFNOT"
|
||||
| "#IFNDEF"
|
||||
| "#ELSE"
|
||||
| "#ENDIF"
|
||||
| "#DEFINE"
|
||||
)
|
||||
}
|
||||
|
||||
fn invalid_directive(command: &Command, message: impl Into<String>) -> ExecutionError {
|
||||
ExecutionError::InvalidDirective {
|
||||
command: command.command_name.clone(),
|
||||
line: command.line,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn exactly_one_argument(command: &Command) -> Result<&CommandArgument, ExecutionError> {
|
||||
if command.arguments.len() == 1 {
|
||||
Ok(&command.arguments[0])
|
||||
} else {
|
||||
Err(invalid_directive(command, "requires exactly one argument"))
|
||||
}
|
||||
}
|
||||
|
||||
fn require_no_arguments(command: &Command) -> Result<(), ExecutionError> {
|
||||
if command.arguments.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(invalid_directive(command, "does not accept arguments"))
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_comments(input: &str) -> String {
|
||||
let mut output = String::with_capacity(input.len());
|
||||
let mut characters = input.chars().peekable();
|
||||
let mut in_block_comment = false;
|
||||
let mut in_quote = false;
|
||||
|
||||
while let Some(character) = characters.next() {
|
||||
if in_block_comment {
|
||||
if character == '*' && characters.peek() == Some(&'/') {
|
||||
characters.next();
|
||||
in_block_comment = false;
|
||||
} else if character == '\n' {
|
||||
output.push('\n');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_quote {
|
||||
output.push(character);
|
||||
if character == '\\' && characters.peek() == Some(&'"') {
|
||||
output.push(characters.next().expect("peeked quote must exist"));
|
||||
} else if character == '"' {
|
||||
in_quote = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
match character {
|
||||
'"' => {
|
||||
in_quote = true;
|
||||
output.push(character);
|
||||
}
|
||||
'/' if characters.peek() == Some(&'*') => {
|
||||
characters.next();
|
||||
in_block_comment = true;
|
||||
}
|
||||
'/' if characters.peek() == Some(&'/') => {
|
||||
characters.next();
|
||||
for next in characters.by_ref() {
|
||||
if next == '\n' {
|
||||
output.push('\n');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => output.push(character),
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn tokenize(line: &str, line_number: usize) -> Result<Vec<CommandArgument>, ParseError> {
|
||||
let mut arguments = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut characters = line.chars().peekable();
|
||||
let mut in_quote = false;
|
||||
let mut token_started = false;
|
||||
|
||||
while let Some(character) = characters.next() {
|
||||
match character {
|
||||
'"' => {
|
||||
in_quote = !in_quote;
|
||||
token_started = true;
|
||||
}
|
||||
'\\' if in_quote && characters.peek() == Some(&'"') => {
|
||||
current.push(characters.next().expect("peeked quote must exist"));
|
||||
token_started = true;
|
||||
}
|
||||
' ' | '\t' if !in_quote => {
|
||||
if token_started {
|
||||
arguments.push(CommandArgument::new(std::mem::take(&mut current)));
|
||||
token_started = false;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
current.push(character);
|
||||
token_started = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if in_quote {
|
||||
return Err(ParseError {
|
||||
line: line_number,
|
||||
message: "unterminated quoted string".to_owned(),
|
||||
});
|
||||
}
|
||||
if token_started {
|
||||
arguments.push(CommandArgument::new(current));
|
||||
}
|
||||
Ok(arguments)
|
||||
}
|
||||
|
||||
fn normalize(value: impl AsRef<str>) -> String {
|
||||
value.as_ref().to_ascii_uppercase()
|
||||
}
|
||||
|
||||
fn is_active(conditionals: &[ConditionalFrame]) -> bool {
|
||||
conditionals
|
||||
.last()
|
||||
.is_none_or(|conditional| conditional.active)
|
||||
}
|
||||
|
||||
fn expand_command(command: &Command, frame: &ExecutionFrame) -> Command {
|
||||
Command {
|
||||
command_name: expand(&command.command_name, frame),
|
||||
arguments: command
|
||||
.arguments
|
||||
.iter()
|
||||
.map(|argument| CommandArgument::new(expand(&argument.string, frame)))
|
||||
.collect(),
|
||||
line: command.line,
|
||||
}
|
||||
}
|
||||
|
||||
fn expand(value: &str, frame: &ExecutionFrame) -> String {
|
||||
let ExecutionFrame::Script { path, arguments } = frame else {
|
||||
return value.to_owned();
|
||||
};
|
||||
|
||||
let mut output = String::with_capacity(value.len());
|
||||
let mut characters = value.chars().peekable();
|
||||
|
||||
while let Some(character) = characters.next() {
|
||||
if character != '%' {
|
||||
output.push(character);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut index = String::new();
|
||||
while let Some(next) = characters.peek() {
|
||||
if next.is_ascii_digit() {
|
||||
index.push(*next);
|
||||
characters.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if index.is_empty() {
|
||||
output.push('%');
|
||||
continue;
|
||||
}
|
||||
|
||||
match index.parse::<usize>() {
|
||||
Ok(0) => output.push_str(
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
Ok(index) => {
|
||||
if let Some(argument) = arguments.get(index - 1) {
|
||||
output.push_str(argument);
|
||||
}
|
||||
}
|
||||
Err(_) => output.push('%'),
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
|
@ -10,3 +10,4 @@ mod bigfile;
|
|||
mod cps;
|
||||
mod mqfel_settings;
|
||||
mod path_helpers;
|
||||
mod tsc;
|
||||
|
|
|
|||
105
bff/tests/tsc.rs
Normal file
105
bff/tests/tsc.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use bff::tsc::{AsoboExecutor, FileSystemScriptLoader};
|
||||
|
||||
use crate::path_helpers::resolve_repo_data_path;
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestData {
|
||||
seen_commands: HashMap<String, usize>,
|
||||
default_callback_count: usize,
|
||||
inline_commands: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_command_handler() {
|
||||
let mut executor =
|
||||
AsoboExecutor::with_user_data(FileSystemScriptLoader::new("unused"), Vec::<u32>::new());
|
||||
executor.on_command("GiveMoney1000", |executor, _command| {
|
||||
executor.user_data_mut().push(1);
|
||||
Ok(())
|
||||
});
|
||||
executor.on_command("GiveMoney1000", |executor, _command| {
|
||||
executor.user_data_mut().push(2);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
executor.execute_command_line("GM1000").unwrap();
|
||||
assert!(executor.remove_command("GM1000"));
|
||||
executor.execute_command_line("GiveMoney1000").unwrap();
|
||||
assert!(executor.remove_command("givemoney1000"));
|
||||
assert!(!executor.remove_command("GM1000"));
|
||||
executor.execute_command_line("GM1000").unwrap();
|
||||
|
||||
assert_eq!(executor.into_user_data(), vec![2, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_default_handler() {
|
||||
let mut executor = AsoboExecutor::with_user_data(FileSystemScriptLoader::new("unused"), 0usize);
|
||||
executor.on_default(|executor, _command| {
|
||||
*executor.user_data_mut() += 1;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
executor.execute_command_line("Unknown").unwrap();
|
||||
assert!(executor.remove_default());
|
||||
executor.execute_command_line("Unknown").unwrap();
|
||||
assert!(!executor.remove_default());
|
||||
|
||||
assert_eq!(executor.into_user_data(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_asobo_tsc_sample() {
|
||||
let root = resolve_repo_data_path("FUEL_tsc_sample");
|
||||
let mut executor =
|
||||
AsoboExecutor::with_user_data(FileSystemScriptLoader::new(root), TestData::default());
|
||||
executor.on_command("InlineCommand", |executor, _command| {
|
||||
executor.user_data_mut().inline_commands += 1;
|
||||
Ok(())
|
||||
});
|
||||
executor.on_default(|executor, command| {
|
||||
let data = executor.user_data_mut();
|
||||
data.default_callback_count += 1;
|
||||
*data
|
||||
.seen_commands
|
||||
.entry(command.command_name.clone())
|
||||
.or_default() += 1;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
executor.execute_command_line("#set _PC").unwrap();
|
||||
executor
|
||||
.execute_script_text(
|
||||
"initial_state_multiline.tsc",
|
||||
r#"
|
||||
#set _MASTER
|
||||
#if _PC && _MASTER
|
||||
IC "quoted // text and /* text are part of this argument"
|
||||
#endif
|
||||
#set _BIGFILE
|
||||
"#,
|
||||
Vec::<String>::new(),
|
||||
)
|
||||
.unwrap();
|
||||
executor.execute_command_line("SRC Vibrations.tsc").unwrap();
|
||||
executor.execute_command_line("BSRC user.tsc").unwrap();
|
||||
|
||||
let has_pc = executor.has_variable("_PC");
|
||||
let has_master = executor.has_variable("_MASTER");
|
||||
let has_bigfile = executor.has_variable("_BIGFILE");
|
||||
let data = executor.into_user_data();
|
||||
eprintln!(
|
||||
"Asobo TSC callback activity: {} commands across {} names",
|
||||
data.default_callback_count,
|
||||
data.seen_commands.len(),
|
||||
);
|
||||
|
||||
assert!(has_pc);
|
||||
assert!(has_master);
|
||||
assert!(has_bigfile);
|
||||
assert_eq!(data.inline_commands, 1);
|
||||
assert!(data.default_callback_count > 100);
|
||||
assert!(!data.seen_commands.is_empty());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue