feat(script): evaluate additive FML expressions during Init
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s

This commit is contained in:
Valentin Popov committed 2026-10-31 12:56:30 +04:00
1 parent 2a3b826c26
commit a9f84d4be0
5 files changed
+856 -104

No files matched your search

+249 -31
View File
@@ -7014,7 +7014,6 @@ fn read_vfs(vfs: &Arc<dyn Vfs>, path: &NormalizedPath) -> Result<Arc<[u8]>, Engi
#[cfg(test)]
mod tests {
use super::*;
use fparkan_script::ScriptDispatchSelector;
use fparkan_vfs::{DirectoryVfs, MemoryVfs, OverlayVfs, VfsEntry, VfsMetadata};
use std::path::{Path, PathBuf};
@@ -13350,12 +13349,14 @@ mod tests {
(
"MISSIONS/SCRIPTS/formula.var",
Arc::from(
&b"VAR(DWORD, ClanBaseX, 0)\nVAR(DWORD, ClanBaseY, 0)\nVAR(DWORD, ClanID, 0)\nVAR(float, target, 0.5)\n"[..],
&b"VAR(DWORD, ClanBaseX, 0)\nVAR(DWORD, ClanBaseY, 0)\nVAR(DWORD, ClanID, 99)\nVAR(float, target, 0.5)\n"[..],
),
),
(
"MISSIONS/SCRIPTS/formula.fml",
Arc::from(&b"// authored literal\n\nFUNCTION( , 0.8, )\nFUNCTION( later, target + 2, )\n"[..]),
Arc::from(
&b"// authored Init expression\n\nFUNCTION( , ClanID + 0.8, )\nFUNCTION( later, target * 2, )\n"[..],
),
),
] {
files.insert(
@@ -13391,6 +13392,185 @@ mod tests {
);
}
#[test]
#[ignore = "requires licensed corpus"]
fn licensed_fml_variable_rows_resolve_from_shared_varset_defaults() {
fn formula_action(target: u32, row: u32) -> ScriptInstruction {
ScriptInstruction {
header_words: [u32::MAX, target, u32::MAX, u32::MAX, 6, 0, row],
references: Vec::new(),
}
}
fn init_event_index(package: &ScriptPackage) -> usize {
package
.events
.iter()
.position(|event| {
event.name_raw.split(|byte| *byte == 0).next() == Some(&b"Init"[..])
})
.expect("Init event")
}
fn row_is_assigned(instructions: &[ScriptInstruction], row: u32, target: u32) -> bool {
instructions.iter().any(|instruction| {
instruction.header_words[0] == u32::MAX
&& instruction.header_words[1] == target
&& instruction.header_words[2] == u32::MAX
&& instruction.header_words[3] == u32::MAX
&& instruction.header_words[6] == row
})
}
let root = licensed_root("IS");
let script_dir = root.join("MISSIONS/SCRIPTS");
let declarations = fparkan_script::parse_varset(
&std::fs::read(script_dir.join("varset.var")).expect("read shared varset"),
)
.expect("parse shared varset");
let formulas3 = FormulaTable::parse(
&std::fs::read(script_dir.join("c1m3e.fml")).expect("read c1m3e.fml"),
);
let package3 = fparkan_script::decode(
&std::fs::read(script_dir.join("c1m3e.scr")).expect("read c1m3e.scr"),
)
.expect("decode c1m3e.scr");
let init3_index = init_event_index(&package3);
let init3_event_word = package3.events[init3_index].event_word;
let init3 = &package3.events[init3_index].instructions;
assert!(row_is_assigned(init3, 0, 40));
assert!(row_is_assigned(init3, 1, 41));
assert!(row_is_assigned(init3, 2, 164));
assert!(!row_is_assigned(init3, 19, 158));
assert!(!row_is_assigned(init3, 22, 174));
assert!(row_is_assigned(
&package3
.events
.iter()
.find(|event| event.name_raw.split(|byte| *byte == 0).next()
== Some(&b"Fort_Captured"[..]))
.expect("Fort_Captured event")
.instructions,
19,
158,
));
assert!(row_is_assigned(
&package3
.events
.iter()
.find(|event| event.name_raw.split(|byte| *byte == 0).next()
== Some(&b"PBM_BUILDING_PROTECT_Start"[..]))
.expect("building-protection event")
.instructions,
22,
174,
));
assert_eq!(formulas3.rows[19].expression_raw, b" 0.8 + fT");
assert_eq!(
formulas3.rows[22].expression_raw,
b" dBuildingProtectHits - dT2"
);
let f_temp = u32::try_from(
declarations
.declarations
.iter()
.position(|declaration| declaration.name == "fTemp")
.expect("fTemp declaration"),
)
.expect("fTemp index fits u32");
let d_temp3 = u32::try_from(
declarations
.declarations
.iter()
.position(|declaration| declaration.name == "dTemp3")
.expect("dTemp3 declaration"),
)
.expect("dTemp3 index fits u32");
assert_eq!((f_temp, d_temp3), (158, 174));
let mut synthetic3 = package3.clone();
synthetic3.events[init3_index].instructions =
vec![formula_action(f_temp, 19), formula_action(d_temp3, 22)];
assert_eq!(
declarations.resolve_init(
&synthetic3,
Handler19InitInput {
first_x87_word: 0,
second_x87_word: 0,
third_word: 0,
},
Some(&formulas3),
),
Ok(ScriptInitResolution::Executed {
event_index: init3_index,
event_word: init3_event_word,
writes: vec![
ScriptInitWrite {
index: f_temp,
value: VarSetDefault::FloatBits(0.8_f32.to_bits()),
},
ScriptInitWrite {
index: d_temp3,
value: VarSetDefault::Dword(2_000_000),
},
],
})
);
let formulas4 = FormulaTable::parse(
&std::fs::read(script_dir.join("c1m4e.fml")).expect("read c1m4e.fml"),
);
let package4 = fparkan_script::decode(
&std::fs::read(script_dir.join("c1m4e.scr")).expect("read c1m4e.scr"),
)
.expect("decode c1m4e.scr");
let init4_index = init_event_index(&package4);
let init4_event_word = package4.events[init4_index].event_word;
let init4 = &package4.events[init4_index].instructions;
assert!(!row_is_assigned(init4, 0, f_temp));
assert!(row_is_assigned(
&package4
.events
.iter()
.find(|event| event.name_raw.split(|byte| *byte == 0).next()
== Some(&b"PBM_BUILDING_INF_CAPTURE_Start"[..]))
.expect("building-infantry-capture event")
.instructions,
0,
f_temp,
));
assert_eq!(formulas4.rows[0].expression_raw, b" fTemp + 0.00001");
let mut synthetic4 = package4.clone();
synthetic4.events[init4_index].instructions =
vec![formula_action(f_temp, 0), formula_action(f_temp, 0)];
assert_eq!(
declarations.resolve_init(
&synthetic4,
Handler19InitInput {
first_x87_word: 0,
second_x87_word: 0,
third_word: 0,
},
Some(&formulas4),
),
Ok(ScriptInitResolution::Executed {
event_index: init4_index,
event_word: init4_event_word,
writes: vec![
ScriptInitWrite {
index: f_temp,
value: VarSetDefault::FloatBits(0.00001_f32.to_bits()),
},
ScriptInitWrite {
index: f_temp,
value: VarSetDefault::FloatBits((0.00001_f32 + 0.00001_f32).to_bits()),
},
],
})
);
}
#[test]
fn script_varset_prefers_bundle_adjacent_file_then_uses_shared_fallback() {
let mut personal_files = MemoryVfs::default();
@@ -13558,6 +13738,8 @@ mod tests {
#[test]
fn handler_nineteen_preview_keeps_unsupported_package_without_partial_writes() {
let formula_table =
FormulaTable::parse(b"FUNCTION(add, x + 2, )\nFUNCTION(unsupported, x * 2, )\n");
let varset = MissionScriptVarSet {
path: FALLBACK_SCRIPT_VARSET_PATH.to_string(),
used_fallback: true,
@@ -13578,7 +13760,11 @@ mod tests {
references: vec![0, 1, 2],
},
fparkan_script::ScriptInstruction {
header_words: [18, u32::MAX, u32::MAX, 0, 0, 0, 0],
header_words: [u32::MAX, 0, u32::MAX, u32::MAX, 6, 0, 0],
references: Vec::new(),
},
fparkan_script::ScriptInstruction {
header_words: [u32::MAX, 0, u32::MAX, u32::MAX, 6, 0, 1],
references: Vec::new(),
},
],
@@ -13591,7 +13777,7 @@ mod tests {
base_path: "MISSIONS/SCRIPTS/default".to_string(),
script_path: "MISSIONS/SCRIPTS/default.scr".to_string(),
package: package.clone(),
formula_table: None,
formula_table: Some(formula_table.clone()),
init_execution: MissionScriptInitExecution::Missing,
behavior_warm_start: None,
};
@@ -13609,9 +13795,10 @@ mod tests {
preview_bundles[0].init_execution,
MissionScriptInitExecution::Unsupported {
event_index: 0,
instruction_index: 1,
error: ScriptInitResolveError::UnsupportedSelector {
found: ScriptDispatchSelector::Handler(18),
instruction_index: 2,
error: ScriptInitResolveError::UnsupportedFormulaExpression {
index: 1,
expression: b" x * 2".to_vec(),
},
}
);
@@ -13640,12 +13827,13 @@ mod tests {
Err(EngineError::ScriptInitUnsupported {
clan_index: 0,
event_index: 0,
instruction_index: 1,
source: ScriptInitResolveError::UnsupportedSelector {
found: ScriptDispatchSelector::Handler(18),
instruction_index: 2,
source: ScriptInitResolveError::UnsupportedFormulaExpression {
index: 1,
expression,
},
..
})
}) if expression == b" x * 2"
));
let mut malformed_package = package.clone();
@@ -15789,16 +15977,31 @@ mod tests {
.iter()
.position(|event| event.name_raw.split(|byte| *byte == 0).next() == Some(&b"Init"[..]))
.expect("Init event");
assert_eq!(
package.events[init_index].instructions[0].header_words[0],
19
let init = &mut package.events[init_index];
let handler = init
.instructions
.first()
.expect("original Handler(19) instruction");
assert_eq!(handler.header_words[0], 19);
assert_eq!(handler.references.as_slice(), &[224, 225, 226]);
let original_instruction_count = init.instructions.len();
init.instructions.push(ScriptInstruction {
header_words: [u32::MAX, 164, u32::MAX, u32::MAX, 6, 0, 0],
references: Vec::new(),
});
init.instructions.push(ScriptInstruction {
header_words: [u32::MAX, 158, u32::MAX, u32::MAX, 6, 0, 1],
references: Vec::new(),
});
let replacement_script = Arc::<[u8]>::from(encode_script_package(&package));
let replacement_formula = Arc::<[u8]>::from(
&b"FUNCTION(add, ClanBaseX + 0.8, )\nFUNCTION(unsupported, fT * 2, )\n"[..],
);
package.events[init_index].instructions[0].header_words[0] = 18;
let replacement = Arc::<[u8]>::from(encode_script_package(&package));
let vfs = Arc::new(SwappingScriptVfs {
inner: DirectoryVfs::new(&root),
replacement,
replacement_script,
replacement_formula,
enabled: std::sync::atomic::AtomicBool::new(false),
});
let services_vfs: Arc<dyn Vfs> = vfs.clone();
@@ -15843,11 +16046,13 @@ mod tests {
assert!(matches!(
error,
EngineError::ScriptInitUnsupported {
source: ScriptInitResolveError::UnsupportedSelector {
found: ScriptDispatchSelector::Handler(18),
instruction_index,
source: ScriptInitResolveError::UnsupportedFormulaExpression {
index: 1,
expression,
},
..
}
} if instruction_index == original_instruction_count + 1 && expression == b" fT * 2"
));
assert_eq!(loaded_mission(&engine), Some(&previous_loaded));
assert_eq!(
@@ -16731,24 +16936,37 @@ mod tests {
struct SwappingScriptVfs {
inner: DirectoryVfs,
replacement: Arc<[u8]>,
replacement_script: Arc<[u8]>,
replacement_formula: Arc<[u8]>,
enabled: std::sync::atomic::AtomicBool,
}
impl SwappingScriptVfs {
fn use_replacement(&self, path: &NormalizedPath) -> bool {
self.enabled.load(std::sync::atomic::Ordering::SeqCst)
&& path
.as_str()
.eq_ignore_ascii_case("MISSIONS/SCRIPTS/default.scr")
fn replacement(&self, path: &NormalizedPath) -> Option<&Arc<[u8]>> {
if !self.enabled.load(std::sync::atomic::Ordering::SeqCst) {
return None;
}
if path
.as_str()
.eq_ignore_ascii_case("MISSIONS/SCRIPTS/default.scr")
{
Some(&self.replacement_script)
} else if path
.as_str()
.eq_ignore_ascii_case("MISSIONS/SCRIPTS/default.fml")
{
Some(&self.replacement_formula)
} else {
None
}
}
}
impl Vfs for SwappingScriptVfs {
fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError> {
if self.use_replacement(path) {
if let Some(replacement) = self.replacement(path) {
return Ok(VfsMetadata {
len: self.replacement.len() as u64,
len: replacement.len() as u64,
fingerprint: [0x18; 32],
});
}
@@ -16756,8 +16974,8 @@ mod tests {
}
fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError> {
if self.use_replacement(path) {
return Ok(self.replacement.clone());
if let Some(replacement) = self.replacement(path) {
return Ok(replacement.clone());
}
self.inner.read(path)
}
+460 -26
View File
@@ -3,7 +3,7 @@
//!
//! This module preserves the layout proven by the GOG `ai.dll` reader. It
//! deliberately does not implement a general bytecode VM. It contains a
//! narrow resolver for confirmed `Init` handlers and numeric FML literals.
//! narrow resolver for confirmed `Init` handlers and bounded FML expressions.
use fparkan_binary::{checked_allocation_len, Cursor, DecodeError, Limits};
use std::sync::Arc;
@@ -214,7 +214,7 @@ pub enum ScriptInitResolveError {
/// Number of preserved FML rows.
rows: usize,
},
/// The referenced FML row is not one numeric token.
/// The referenced FML row is outside the supported atom-expression subset.
UnsupportedFormulaExpression {
/// Zero-based FML row index.
index: u32,
@@ -259,7 +259,7 @@ impl std::fmt::Display for ScriptInitResolveError {
),
Self::UnsupportedFormulaExpression { index, expression } => write!(
formatter,
"formula {index} expression {:?} is not one numeric literal",
"formula {index} expression {:?} is outside the supported atom and binary +/− subset",
String::from_utf8_lossy(expression)
),
Self::DwordConversionOutOfRange { index, value_bits } => write!(
@@ -736,6 +736,7 @@ impl VarSet {
/// sentinel selector is accepted only for the proven formula assignment
/// with control and copy-source words both equal to `0xffffffff`; the
/// referenced row is evaluated only when that instruction is reached.
/// Formula atoms read defaults and earlier successful writes in this event.
///
/// # Errors
///
@@ -753,10 +754,24 @@ impl VarSet {
return Ok(ScriptInitResolution::Missing);
};
let mut values = self
.declarations
.iter()
.map(|declaration| declaration.default_value)
.collect::<Vec<_>>();
let mut writes = Vec::new();
for (instruction_index, instruction) in event.instructions.iter().enumerate() {
match self.resolve_init_instruction(instruction, input, formulas)? {
match self.resolve_init_instruction(instruction, input, formulas, &values)? {
InitInstructionResolution::Executed(instruction_writes) => {
for write in &instruction_writes {
let Some(value) = values.get_mut(write.index as usize) else {
return Err(ScriptInitResolveError::VarSetIndexOutOfBounds {
index: write.index,
declarations: self.declarations.len(),
});
};
*value = write.value;
}
writes.extend(instruction_writes);
}
InitInstructionResolution::Unsupported(error) => {
@@ -781,6 +796,7 @@ impl VarSet {
instruction: &ScriptInstruction,
input: Handler19InitInput,
formulas: Option<&FormulaTable>,
values: &[VarSetDefault],
) -> Result<InitInstructionResolution, ScriptInitResolveError> {
match instruction.dispatch_selector() {
ScriptDispatchSelector::Handler(19) => {
@@ -842,22 +858,23 @@ impl VarSet {
));
};
let formula_index = instruction.header_words[6];
let value = match formulas.evaluate_numeric_literal(formula_index) {
Ok(value) => value,
Err(FormulaLiteralError::IndexOutOfBounds { index, rows }) => {
return Err(ScriptInitResolveError::FormulaIndexOutOfBounds {
index,
rows,
});
}
Err(FormulaLiteralError::UnsupportedExpression { index, expression }) => {
return Ok(InitInstructionResolution::Unsupported(
ScriptInitResolveError::UnsupportedFormulaExpression {
index,
expression,
},
));
}
let Some(formula) = formulas.rows.get(formula_index as usize) else {
return Err(ScriptInitResolveError::FormulaIndexOutOfBounds {
index: formula_index,
rows: formulas.rows.len(),
});
};
let Some(value) = parse_init_formula_expression(
&formula.expression_raw,
&self.declarations,
values,
) else {
return Ok(InitInstructionResolution::Unsupported(
ScriptInitResolveError::UnsupportedFormulaExpression {
index: formula_index,
expression: formula.expression_raw.clone(),
},
));
};
let target_index = instruction.header_words[1];
let Some(target) = self.declarations.get(target_index as usize) else {
@@ -866,6 +883,20 @@ impl VarSet {
declarations: self.declarations.len(),
});
};
let Some(current_target) = values.get(target_index as usize) else {
return Err(ScriptInitResolveError::VarSetIndexOutOfBounds {
index: target_index,
declarations: values.len(),
});
};
if !varset_value_matches_type(target.type_name, *current_target) {
return Ok(InitInstructionResolution::Unsupported(
ScriptInitResolveError::UnsupportedFormulaExpression {
index: formula_index,
expression: formula.expression_raw.clone(),
},
));
}
let value = match target.type_name {
VarSetType::Float => VarSetDefault::FloatBits(value.to_bits()),
VarSetType::Dword => {
@@ -1357,6 +1388,113 @@ fn native_decimal_prefix_len(token: &[u8]) -> Option<usize> {
Some(index)
}
fn parse_init_formula_expression(
expression: &[u8],
declarations: &[VarSetDeclaration],
values: &[VarSetDefault],
) -> Option<f32> {
let (left, left_end) = parse_init_formula_atom(expression, 0, declarations, values)?;
let operator_index = skip_native_formula_space(expression, left_end);
let Some(&operator @ (b'+' | b'-')) = expression.get(operator_index) else {
return expression[left_end..]
.iter()
.all(|byte| *byte == 0 || *byte <= 32)
.then_some(left);
};
let (right, right_end) =
parse_init_formula_atom(expression, operator_index + 1, declarations, values)?;
if !expression[right_end..]
.iter()
.all(|byte| *byte == 0 || *byte <= 32)
{
return None;
}
Some(match operator {
b'+' => right + left,
b'-' => left - right,
_ => unreachable!(),
})
}
fn parse_init_formula_atom(
expression: &[u8],
start: usize,
declarations: &[VarSetDeclaration],
values: &[VarSetDefault],
) -> Option<(f32, usize)> {
let start = skip_native_formula_space(expression, start);
let remaining = expression.get(start..)?;
let first = *remaining.first()?;
let token_len = if first.is_ascii_digit() {
native_numeric_token_len(remaining)?
} else {
native_variable_token_len(remaining)?
};
let token = &remaining[..token_len];
if is_native_operator_atom(token) {
return None;
}
let value = if let Some(index) = declarations
.iter()
.position(|declaration| declaration.name.as_bytes() == token)
{
let declaration = &declarations[index];
let value = *values.get(index)?;
varset_value_as_f32(declaration.type_name, value)?
} else {
parse_native_numeric_literal(token)?
};
Some((value, start + token_len))
}
fn native_variable_token_len(token: &[u8]) -> Option<usize> {
let first = *token.first()?;
if first == b'^' || !is_native_variable_byte(first) {
return None;
}
Some(
token
.iter()
.take_while(|byte| is_native_variable_byte(**byte))
.count(),
)
}
fn is_native_variable_byte(byte: u8) -> bool {
byte.is_ascii() && ((byte > b'@' && byte < b'{') || byte.is_ascii_digit() || byte == b'_')
}
fn is_native_operator_atom(token: &[u8]) -> bool {
matches!(
token,
b"+" | b"-" | b"*" | b"/" | b"^" | b"&" | b"|" | b"!" | b"A" | b"B" | b"N" | b"S"
)
}
fn skip_native_formula_space(expression: &[u8], start: usize) -> usize {
expression[start..]
.iter()
.position(|byte| *byte == 0 || *byte > 32)
.map_or(expression.len(), |offset| start + offset)
}
fn varset_value_as_f32(type_name: VarSetType, value: VarSetDefault) -> Option<f32> {
match (type_name, value) {
(VarSetType::Float, VarSetDefault::FloatBits(bits)) => Some(f32::from_bits(bits)),
(VarSetType::Dword, VarSetDefault::Dword(value)) => Some(value as f32),
_ => None,
}
}
fn varset_value_matches_type(type_name: VarSetType, value: VarSetDefault) -> bool {
matches!(
(type_name, value),
(VarSetType::Float, VarSetDefault::FloatBits(_))
| (VarSetType::Dword, VarSetDefault::Dword(_))
)
}
fn formula_dword_value(value: f32) -> Option<u32> {
const SIGNED_QWORD_LIMIT: f64 = 9_223_372_036_854_775_808.0;
let value = f64::from(value);
@@ -1822,6 +1960,49 @@ mod tests {
};
use fparkan_binary::{DecodeError, Limits};
fn init_package(instructions: Vec<ScriptInstruction>) -> ScriptPackage {
ScriptPackage {
opcode_handler_count: 73,
events: vec![ScriptEvent {
name_len: 4,
name_raw: b"Init\0".to_vec(),
event_word: 0,
instructions,
}],
trailing_bytes: Vec::new(),
raw: std::sync::Arc::from(&b""[..]),
}
}
fn init_formula_assignment(target: u32, formula: u32) -> ScriptInstruction {
ScriptInstruction {
header_words: [u32::MAX, target, u32::MAX, u32::MAX, 6, 0, formula],
references: Vec::new(),
}
}
fn init_handler19() -> ScriptInstruction {
ScriptInstruction {
header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX],
references: vec![0, 1, 2],
}
}
fn init_input() -> Handler19InitInput {
Handler19InitInput {
first_x87_word: 10,
second_x87_word: 20,
third_word: 30,
}
}
fn one_formula(expression: &[u8]) -> FormulaTable {
let mut source = b"FUNCTION(test, ".to_vec();
source.extend_from_slice(expression);
source.extend_from_slice(b", )\n");
FormulaTable::parse(&source)
}
fn handler_two_input(
scalar_1: f32,
scalar_4: f32,
@@ -2624,13 +2805,266 @@ STRING( 8, ignored, ignored, ignored)\r\n";
);
}
#[test]
fn script_init_formula_variables_read_prior_writes_and_self() {
let varset = parse_varset(
b"VAR(DWORD, source, 0)\nVAR(DWORD, y, 0)\nVAR(DWORD, id, 0)\nVAR(float, target, 0)\nVAR(DWORD, integer, 0)\n",
)
.expect("numeric declarations");
let formulas = FormulaTable::parse(
b"FUNCTION(literal, 7.0, )\nFUNCTION(add, source + 2.0, )\nFUNCTION(subtract, target - source, )\nFUNCTION(self, target + 1.0, )\nFUNCTION(integer, 10.75 + 0.5, )\nFUNCTION(exponent, target + 1e-2, )\n",
);
let package = init_package(vec![
init_handler19(),
init_formula_assignment(3, 0),
init_formula_assignment(3, 1),
init_formula_assignment(3, 2),
init_formula_assignment(3, 3),
init_formula_assignment(4, 4),
init_formula_assignment(3, 5),
]);
assert_eq!(
varset.resolve_init(&package, init_input(), Some(&formulas),),
Ok(ScriptInitResolution::Executed {
event_index: 0,
event_word: 0,
writes: vec![
ScriptInitWrite {
index: 0,
value: VarSetDefault::Dword(10),
},
ScriptInitWrite {
index: 1,
value: VarSetDefault::Dword(20),
},
ScriptInitWrite {
index: 2,
value: VarSetDefault::Dword(30),
},
ScriptInitWrite {
index: 3,
value: VarSetDefault::FloatBits(7.0_f32.to_bits()),
},
ScriptInitWrite {
index: 3,
value: VarSetDefault::FloatBits(12.0_f32.to_bits()),
},
ScriptInitWrite {
index: 3,
value: VarSetDefault::FloatBits(2.0_f32.to_bits()),
},
ScriptInitWrite {
index: 3,
value: VarSetDefault::FloatBits(3.0_f32.to_bits()),
},
ScriptInitWrite {
index: 4,
value: VarSetDefault::Dword(11),
},
ScriptInitWrite {
index: 3,
value: VarSetDefault::FloatBits(3.01_f32.to_bits()),
},
],
})
);
}
#[test]
fn script_init_formula_atoms_use_native_token_and_name_rules() {
let varset = parse_varset(
b"VAR(float, dup, 1)\nVAR(float, dup, 2)\nVAR(float, Name, 3)\nVAR(float, name, 4)\nVAR(float, 1e-2, 41)\nVAR(float, A, 8)\nVAR(float, A1, 9)\nVAR(DWORD, high, 4294967295)\nVAR(float, large, 16777216)\nVAR(float, target, 0)\nVAR(DWORD, integer, 0)\n",
)
.expect("numeric declarations");
let formulas = FormulaTable::parse(
b"FUNCTION(duplicate, dup + 0, )\nFUNCTION(upper, Name + 0, )\nFUNCTION(lower, name + 0, )\nFUNCTION(numeric_name, 1e-2 + 0, )\nFUNCTION(long_operator_prefix, A1 + 0, )\nFUNCTION(unsigned_dword, high + 0, )\nFUNCTION(negative_exponent, 5e-2 + 2e-2, )\nFUNCTION(positive_exponent, 1e+2 + 2e-2, )\nFUNCTION(binary32_rounding, 16777216 + 1, )\nFUNCTION(cancellation, target - 16777215, )\nFUNCTION(dword_target, 10.75 + 0.5, )\nFUNCTION(single_variable, large, )\nFUNCTION(negative_dword_wrap, 2 - 3, )\nFUNCTION(unsigned_dword_wrap, high + 0, )\n",
);
let package = init_package(
(0..14)
.map(|formula| {
init_formula_assignment(
if matches!(formula, 10 | 12 | 13) {
10
} else {
9
},
formula,
)
})
.collect(),
);
let resolved = varset
.resolve_init(
&package,
Handler19InitInput {
first_x87_word: 0,
second_x87_word: 0,
third_word: 0,
},
Some(&formulas),
)
.expect("resolve Init")
.clone();
assert_eq!(
resolved,
ScriptInitResolution::Executed {
event_index: 0,
event_word: 0,
writes: vec![
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits(1.0_f32.to_bits())
},
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits(3.0_f32.to_bits())
},
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits(4.0_f32.to_bits())
},
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits(41.0_f32.to_bits())
},
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits(9.0_f32.to_bits())
},
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits(4_294_967_296.0_f32.to_bits())
},
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits((0.05_f32 + 0.02_f32).to_bits())
},
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits((100.0_f32 + 0.02_f32).to_bits())
},
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits(16_777_216.0_f32.to_bits())
},
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits(1.0_f32.to_bits())
},
ScriptInitWrite {
index: 10,
value: VarSetDefault::Dword(11)
},
ScriptInitWrite {
index: 9,
value: VarSetDefault::FloatBits(16_777_216.0_f32.to_bits())
},
ScriptInitWrite {
index: 10,
value: VarSetDefault::Dword(u32::MAX)
},
ScriptInitWrite {
index: 10,
value: VarSetDefault::Dword(0)
},
],
}
);
assert!(formulas.evaluate_numeric_literal(0).is_err());
}
#[test]
fn script_init_rejects_unsupported_formula_variable_forms() {
let varset = parse_varset(
b"VAR(float, target, 1)\nVAR(float, A, 2)\nVAR(float, A1, 3)\nVAR(float, ^name, 4)\n",
)
.expect("numeric declarations");
for expression in [
&b"target * 2"[..],
b"-1",
b"target + 1 + 2",
b"missing + 1",
b"A + 1",
b"^name + 1",
b"name\xc3\xa9 + 1",
] {
let formulas = one_formula(expression);
let package = init_package(vec![init_formula_assignment(0, 0)]);
assert!(
matches!(
varset.resolve_init(
&package,
Handler19InitInput {
first_x87_word: 0,
second_x87_word: 0,
third_word: 0,
},
Some(&formulas),
),
Ok(ScriptInitResolution::Unsupported {
error: ScriptInitResolveError::UnsupportedFormulaExpression {
index: 0,
..
},
..
})
),
"accepted unsupported formula {:?}",
String::from_utf8_lossy(expression)
);
}
let mut inconsistent = varset;
inconsistent.declarations[0].default_value = VarSetDefault::Dword(1);
let formulas = one_formula(b"target + 1");
let package = init_package(vec![init_formula_assignment(0, 0)]);
assert!(matches!(
inconsistent.resolve_init(
&package,
Handler19InitInput {
first_x87_word: 0,
second_x87_word: 0,
third_word: 0,
},
Some(&formulas),
),
Ok(ScriptInitResolution::Unsupported {
error: ScriptInitResolveError::UnsupportedFormulaExpression { index: 0, .. },
..
})
));
let mut inconsistent_target =
parse_varset(b"VAR(float, target, 1)\n").expect("numeric declaration");
inconsistent_target.declarations[0].default_value = VarSetDefault::Dword(1);
let formulas = one_formula(b"2.0 + 1");
assert!(matches!(
inconsistent_target.resolve_init(
&init_package(vec![init_formula_assignment(0, 0)]),
Handler19InitInput {
first_x87_word: 0,
second_x87_word: 0,
third_word: 0,
},
Some(&formulas),
),
Ok(ScriptInitResolution::Unsupported {
error: ScriptInitResolveError::UnsupportedFormulaExpression { index: 0, .. },
..
})
));
}
#[test]
fn script_init_keeps_unsupported_formula_expressions_lazy_and_transactional() {
let varset = parse_varset(b"VAR(DWORD, integer, 0)\n").expect("numeric declaration");
let varset = parse_varset(b"VAR(DWORD, target, 0)\nVAR(DWORD, integer, 0)\n")
.expect("numeric declaration");
let formulas =
FormulaTable::parse(b"FUNCTION(unsupported, target + 2, )\nFUNCTION(literal, 7.5, )\n");
FormulaTable::parse(b"FUNCTION(unsupported, target * 2, )\nFUNCTION(literal, 7.5, )\n");
let action = |formula_index| ScriptInstruction {
header_words: [u32::MAX, 0, u32::MAX, u32::MAX, 6, 0, formula_index],
header_words: [u32::MAX, 1, u32::MAX, u32::MAX, 6, 0, formula_index],
references: Vec::new(),
};
let package = |formula_index| ScriptPackage {
@@ -2656,7 +3090,7 @@ STRING( 8, ignored, ignored, ignored)\r\n";
event_index: 0,
event_word: 0,
writes: vec![ScriptInitWrite {
index: 0,
index: 1,
value: VarSetDefault::Dword(7),
}],
})
@@ -2668,7 +3102,7 @@ STRING( 8, ignored, ignored, ignored)\r\n";
instruction_index: 0,
error: ScriptInitResolveError::UnsupportedFormulaExpression {
index: 0,
expression: b" target + 2".to_vec(),
expression: b" target * 2".to_vec(),
},
})
);
@@ -2682,7 +3116,7 @@ STRING( 8, ignored, ignored, ignored)\r\n";
instruction_index: 1,
error: ScriptInitResolveError::UnsupportedFormulaExpression {
index: 0,
expression: b" target + 2".to_vec(),
expression: b" target * 2".to_vec(),
},
})
);