diff options
Diffstat (limited to 'crates/fparkan-script/src')
| -rw-r--r-- | crates/fparkan-script/src/lib.rs | 1476 |
1 files changed, 1343 insertions, 133 deletions
diff --git a/crates/fparkan-script/src/lib.rs b/crates/fparkan-script/src/lib.rs index 088b51f..20821fe 100644 --- a/crates/fparkan-script/src/lib.rs +++ b/crates/fparkan-script/src/lib.rs @@ -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, handler-specific resolver for the confirmed `Init` path. +//! narrow resolver for confirmed `Init` handlers and bounded FML expressions. use fparkan_binary::{checked_allocation_len, Cursor, DecodeError, Limits}; use std::sync::Arc; @@ -40,7 +40,7 @@ pub enum VarSetType { Dword, } -/// A lossless-in-meaning numeric default from `varset.var`. +/// A numeric cell value that preserves the declared `varset.var` type. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum VarSetDefault { /// IEEE-754 bits parsed from a `float` literal. @@ -151,33 +151,155 @@ pub struct Handler19DwordWrite { pub value: u32, } -/// Result of resolving the first event whose case-sensitive C-string name is -/// `Init` (bytes through the first NUL) with the supported -/// `Handler(19)` action subset. +/// A typed variable write emitted by the recovered first-`Init` subset. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ScriptInitWrite { + /// Compiled varset declaration index. + pub index: u32, + /// Value in the original cell's numeric representation. + pub value: VarSetDefault, +} + +/// Result of resolving the first case-sensitive `Init` event. #[derive(Clone, Debug, Eq, PartialEq)] -pub enum Handler19InitResolution { - /// No event has the case-sensitive C-string name `Init`; the native lookup - /// makes no call. +pub enum ScriptInitResolution { + /// No event has the case-sensitive C-string name `Init`. Missing, - /// The first `Init\0` event was walked in source order. + /// The first `Init` event completed within the recovered subset. Executed { - /// Zero-based event table index selected by the native name lookup. + /// Zero-based selected event index. event_index: usize, - /// Opaque event word retained from the selected record. + /// Opaque event word retained from the package. event_word: u32, - /// Ordered writes emitted by every supported instruction in the event. - writes: Vec<Handler19DwordWrite>, + /// Ordered, typed writes emitted by the event. + writes: Vec<ScriptInitWrite>, }, - /// The first unsupported instruction stopped this restricted resolver. - /// Any earlier local writes are discarded because the event did not finish. + /// The event reached a known but unsupported form; earlier writes are discarded. Unsupported { - /// Zero-based event table index selected by the native name lookup. + /// Zero-based selected event index. event_index: usize, - /// Zero-based instruction index within that event. + /// Zero-based unsupported instruction index. instruction_index: usize, - /// Unsupported action shape. - error: Handler19ResolveError, + /// Unsupported instruction or expression form. + error: ScriptInitResolveError, + }, +} + +/// A typed failure or unsupported form in the recovered `Init` subset. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ScriptInitResolveError { + /// The selector is outside the recovered `Handler(19)` and formula assignment paths. + UnsupportedSelector { + /// Observed selector. + found: ScriptDispatchSelector, + }, + /// A `Handler(19)` reference or declaration did not match its recovered contract. + Handler19(Handler19ResolveError), + /// A formula record has an unsupported control word. + UnsupportedFormulaControlWord { + /// Raw disk word at index 3. + found: u32, + }, + /// A formula record requests the separate whole-varset-record copy path. + UnsupportedFormulaCopySource { + /// Raw disk word at index 2. + found: u32, + }, + /// The selected script base did not provide an FML table. + MissingFormulaTable, + /// The FML ordinal is outside the preserved row table. + FormulaIndexOutOfBounds { + /// Zero-based FML row index. + index: u32, + /// Number of preserved FML rows. + rows: usize, }, + /// The referenced FML row is outside the supported atom-expression subset. + UnsupportedFormulaExpression { + /// Zero-based FML row index. + index: u32, + /// Raw expression bytes from the row. + expression: Vec<u8>, + }, + /// A DWORD target cannot represent the native truncating qword conversion. + DwordConversionOutOfRange { + /// Compiled target index. + index: u32, + /// Evaluated binary32 payload. + value_bits: u32, + }, + /// The formula record target is outside the selected varset. + VarSetIndexOutOfBounds { + /// Compiled target index. + index: u32, + /// Available declaration count. + declarations: usize, + }, +} + +impl std::fmt::Display for ScriptInitResolveError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnsupportedSelector { found } => { + write!(formatter, "unsupported script selector {found:?} in Init") + } + Self::Handler19(source) => write!(formatter, "Handler(19): {source}"), + Self::UnsupportedFormulaControlWord { found } => write!( + formatter, + "unsupported formula control word 0x{found:08x}; expected 0xffffffff" + ), + Self::UnsupportedFormulaCopySource { found } => write!( + formatter, + "unsupported formula source 0x{found:08x}; whole-varset copy is not modeled" + ), + Self::MissingFormulaTable => write!(formatter, "formula assignment has no FML table"), + Self::FormulaIndexOutOfBounds { index, rows } => write!( + formatter, + "formula index {index} is outside the FML table with {rows} rows" + ), + Self::UnsupportedFormulaExpression { index, expression } => write!( + formatter, + "formula {index} expression {:?} is outside the supported atom and binary +/− subset", + String::from_utf8_lossy(expression) + ), + Self::DwordConversionOutOfRange { index, value_bits } => write!( + formatter, + "formula value 0x{value_bits:08x} cannot be converted to DWORD target {index}" + ), + Self::VarSetIndexOutOfBounds { + index, + declarations, + } => write!( + formatter, + "formula target {index} is outside {declarations} varset declarations" + ), + } + } +} + +impl std::error::Error for ScriptInitResolveError {} + +enum InitInstructionResolution { + Executed(Vec<ScriptInitWrite>), + Unsupported(ScriptInitResolveError), +} + +/// Rows loaded from one script base's `.fml` file in native table order. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct FormulaTable { + /// Accepted `FUNCTION` rows in their original order, including unsupported expressions. + pub rows: Vec<FormulaRow>, +} + +/// One source row accepted by the native FML line loader. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FormulaRow { + /// Name token retained from the first comma-delimited field. + pub name_raw: Vec<u8>, + /// Expression bytes from the second field; interpretation is lazy. + pub expression_raw: Vec<u8>, + /// Final field bytes, retained without interpreting their meaning. + pub trailing_raw: Vec<u8>, } /// Inputs already expressed at the original `Handler(19)` setter ABI boundary. @@ -205,12 +327,12 @@ pub enum Handler19ResolveError { }, /// Only action instructions with control word `0xffff_ffff` are supported. UnsupportedControlWord { - /// Opaque control word from instruction header word one. + /// Opaque control word from disk-order header index 3. found: u32, }, /// Only instructions with no result-save reference are supported. UnsupportedResultSave { - /// Opaque variable reference from instruction header word two. + /// Opaque variable reference from disk-order header index 1. found: u32, }, /// One of the three required references was absent. @@ -583,95 +705,220 @@ impl VarSet { ]) } - /// Resolves only the first event whose case-sensitive C-string name is - /// `Init` (bytes through the first NUL), and its supported - /// `Handler(19)` action instructions in source order. + fn resolve_handler19_target( + &self, + instruction: &ScriptInstruction, + position: usize, + ) -> Result<u32, Handler19ResolveError> { + let index = *instruction + .references + .get(position) + .ok_or(Handler19ResolveError::MissingReference { position })?; + match self.declarations.get(index as usize) { + Some(VarSetDeclaration { + type_name: VarSetType::Dword, + .. + }) => Ok(index), + Some(declaration) => Err(Handler19ResolveError::UnexpectedType { + position, + found: declaration.type_name, + }), + None => Err(Handler19ResolveError::VarSetIndexOutOfBounds { + index, + declarations: self.declarations.len(), + }), + } + } + + /// Resolves the first case-sensitive `Init` event's recovered write subset. + /// + /// Handler(19) writes use the caller's original setter-boundary words. A + /// 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 /// - /// A missing event is a normal no-write result, matching the native event - /// lookup. Unsupported selector/control/result-save forms stop at their - /// first instruction and return no partial writes. Malformed handler - /// references and declaration types remain errors. - pub fn resolve_handler19_init( + /// Returns a typed error for malformed Handler(19) references, invalid + /// formula or varset indices, and targets outside the selected varset. + pub fn resolve_init( &self, package: &ScriptPackage, input: Handler19InitInput, - ) -> Result<Handler19InitResolution, Handler19ResolveError> { + formulas: Option<&FormulaTable>, + ) -> Result<ScriptInitResolution, ScriptInitResolveError> { let Some((event_index, event)) = package.events.iter().enumerate().find(|(_, event)| { event.name_raw.split(|byte| *byte == 0).next() == Some(&b"Init"[..]) }) else { - return Ok(Handler19InitResolution::Missing); + 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() { - let instruction_writes = - match self.resolve_handler19_init_instruction(instruction, input) { - Ok(writes) => writes, - Err(error @ Handler19ResolveError::UnsupportedSelector { .. }) - | Err(error @ Handler19ResolveError::UnsupportedControlWord { .. }) - | Err(error @ Handler19ResolveError::UnsupportedResultSave { .. }) => { - return Ok(Handler19InitResolution::Unsupported { - event_index, - instruction_index, - error, - }); + 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; } - Err(error) => return Err(error), - }; - writes.extend(instruction_writes); + writes.extend(instruction_writes); + } + InitInstructionResolution::Unsupported(error) => { + return Ok(ScriptInitResolution::Unsupported { + event_index, + instruction_index, + error, + }); + } + } } - Ok(Handler19InitResolution::Executed { + Ok(ScriptInitResolution::Executed { event_index, event_word: event.event_word, writes, }) } - fn resolve_handler19_init_instruction( + fn resolve_init_instruction( &self, instruction: &ScriptInstruction, input: Handler19InitInput, - ) -> Result<[Handler19DwordWrite; 3], Handler19ResolveError> { - let selector = instruction.dispatch_selector(); - if selector != ScriptDispatchSelector::Handler(19) { - return Err(Handler19ResolveError::UnsupportedSelector { found: selector }); - } - if instruction.header_words[1] != u32::MAX { - return Err(Handler19ResolveError::UnsupportedControlWord { - found: instruction.header_words[1], - }); - } - if instruction.header_words[2] != u32::MAX { - return Err(Handler19ResolveError::UnsupportedResultSave { - found: instruction.header_words[2], - }); - } - self.resolve_handler19(instruction, input) - } - - fn resolve_handler19_target( - &self, - instruction: &ScriptInstruction, - position: usize, - ) -> Result<u32, Handler19ResolveError> { - let index = *instruction - .references - .get(position) - .ok_or(Handler19ResolveError::MissingReference { position })?; - match self.declarations.get(index as usize) { - Some(VarSetDeclaration { - type_name: VarSetType::Dword, - .. - }) => Ok(index), - Some(declaration) => Err(Handler19ResolveError::UnexpectedType { - position, - found: declaration.type_name, - }), - None => Err(Handler19ResolveError::VarSetIndexOutOfBounds { - index, - declarations: self.declarations.len(), - }), + formulas: Option<&FormulaTable>, + values: &[VarSetDefault], + ) -> Result<InitInstructionResolution, ScriptInitResolveError> { + match instruction.dispatch_selector() { + ScriptDispatchSelector::Handler(19) => { + if instruction.header_words[3] != u32::MAX { + return Ok(InitInstructionResolution::Unsupported( + ScriptInitResolveError::Handler19( + Handler19ResolveError::UnsupportedControlWord { + found: instruction.header_words[3], + }, + ), + )); + } + if instruction.header_words[1] != u32::MAX { + return Ok(InitInstructionResolution::Unsupported( + ScriptInitResolveError::Handler19( + Handler19ResolveError::UnsupportedResultSave { + found: instruction.header_words[1], + }, + ), + )); + } + match self.resolve_handler19(instruction, input) { + Ok(writes) => Ok(InitInstructionResolution::Executed( + writes + .into_iter() + .map(|write| ScriptInitWrite { + index: write.index, + value: VarSetDefault::Dword(write.value), + }) + .collect(), + )), + Err(error @ Handler19ResolveError::UnsupportedControlWord { .. }) + | Err(error @ Handler19ResolveError::UnsupportedResultSave { .. }) => { + Ok(InitInstructionResolution::Unsupported( + ScriptInitResolveError::Handler19(error), + )) + } + Err(error) => Err(ScriptInitResolveError::Handler19(error)), + } + } + ScriptDispatchSelector::Sentinel => { + if instruction.header_words[3] != u32::MAX { + return Ok(InitInstructionResolution::Unsupported( + ScriptInitResolveError::UnsupportedFormulaControlWord { + found: instruction.header_words[3], + }, + )); + } + if instruction.header_words[2] != u32::MAX { + return Ok(InitInstructionResolution::Unsupported( + ScriptInitResolveError::UnsupportedFormulaCopySource { + found: instruction.header_words[2], + }, + )); + } + let Some(formulas) = formulas else { + return Ok(InitInstructionResolution::Unsupported( + ScriptInitResolveError::MissingFormulaTable, + )); + }; + let formula_index = instruction.header_words[6]; + 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 { + return Err(ScriptInitResolveError::VarSetIndexOutOfBounds { + index: target_index, + 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 => { + let Some(value) = formula_dword_value(value) else { + return Ok(InitInstructionResolution::Unsupported( + ScriptInitResolveError::DwordConversionOutOfRange { + index: target_index, + value_bits: value.to_bits(), + }, + )); + }; + VarSetDefault::Dword(value) + } + }; + Ok(InitInstructionResolution::Executed(vec![ScriptInitWrite { + index: target_index, + value, + }])) + } + found => Ok(InitInstructionResolution::Unsupported( + ScriptInitResolveError::UnsupportedSelector { found }, + )), } } @@ -870,6 +1117,393 @@ impl std::fmt::Display for VarSetError { impl std::error::Error for VarSetError {} +/// A failure evaluating one row as a single native numeric literal. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum FormulaLiteralError { + /// The referenced ordinal was outside the preserved table. + IndexOutOfBounds { + /// Zero-based row index. + index: u32, + /// Number of accepted rows. + rows: usize, + }, + /// The row's expression was not one numeric token. + UnsupportedExpression { + /// Zero-based row index. + index: u32, + /// Raw expression bytes. + expression: Vec<u8>, + }, +} + +impl std::fmt::Display for FormulaLiteralError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IndexOutOfBounds { index, rows } => { + write!(formatter, "formula index {index} is outside {rows} rows") + } + Self::UnsupportedExpression { index, expression } => write!( + formatter, + "formula {index} expression {:?} is not one numeric literal", + String::from_utf8_lossy(expression) + ), + } + } +} + +impl std::error::Error for FormulaLiteralError {} + +impl FormulaTable { + /// Parses native `FUNCTION` rows without compiling their expressions. + /// + /// Input is read as text: CRLF becomes LF, Ctrl-Z ends input, and each + /// 255-byte read chunk is parsed independently. Accepted rows stay in + /// source order so formula references retain their original ordinals. + pub fn parse(bytes: &[u8]) -> Self { + let mut text = Vec::with_capacity(bytes.len()); + let mut offset = 0; + while let Some(&byte) = bytes.get(offset) { + if byte == 0x1a { + break; + } + if byte == b'\r' && bytes.get(offset + 1) == Some(&b'\n') { + text.push(b'\n'); + offset += 2; + } else { + text.push(byte); + offset += 1; + } + } + + let mut table = Self::default(); + let mut offset = 0; + while offset < text.len() { + let limit = (offset + 255).min(text.len()); + let end = text[offset..limit] + .iter() + .position(|byte| *byte == b'\n') + .map_or(limit, |newline| offset + newline + 1); + parse_formula_chunk(&mut table, &text[offset..end]); + offset = end; + } + table + } + + /// Evaluates a preserved row only when it is referenced by a formula assignment. + /// + /// The lexer behavior is bounded to the original number-token rules. The + /// converter consumes the longest decimal prefix, converts it to `f64`, + /// then rounds to the node's binary32 payload. + /// + /// # Errors + /// + /// Returns a typed error for an out-of-range ordinal or an expression + /// containing more than one supported numeric token. + pub fn evaluate_numeric_literal(&self, index: u32) -> Result<f32, FormulaLiteralError> { + let Some(row) = self.rows.get(index as usize) else { + return Err(FormulaLiteralError::IndexOutOfBounds { + index, + rows: self.rows.len(), + }); + }; + parse_native_numeric_literal(&row.expression_raw).ok_or_else(|| { + FormulaLiteralError::UnsupportedExpression { + index, + expression: row.expression_raw.clone(), + } + }) + } +} + +fn parse_formula_chunk(table: &mut FormulaTable, chunk: &[u8]) { + let visible_len = chunk + .iter() + .position(|byte| *byte == 0) + .unwrap_or(chunk.len()); + let visible = &chunk[..visible_len]; + let first_token = native_first_token(visible); + if first_token.iter().take(2).any(|byte| *byte == b'/') { + return; + } + if !visible + .windows(b"FUNCTION".len()) + .any(|part| part == b"FUNCTION") + { + return; + } + let Some(open) = visible.iter().position(|byte| *byte == b'(') else { + return; + }; + let first_comma = visible.iter().position(|byte| *byte == b','); + let second_comma = first_comma.and_then(|first| { + visible[first + 1..] + .iter() + .position(|byte| *byte == b',') + .map(|next| first + 1 + next) + }); + let mut buffer = visible.to_vec(); + buffer.push(0); + for byte in &mut buffer[..visible_len] { + if *byte == b',' { + *byte = 0; + } + } + + let name_start = open + 1; + let expression_start = first_comma.map(|comma| comma + 1); + let trailing_start = second_comma.map(|comma| comma + 1); + let last_field_start = trailing_start.or(expression_start).unwrap_or(name_start); + let last_field_end = c_string_end(&buffer, last_field_start); + if let Some(close) = buffer[last_field_start..last_field_end] + .iter() + .rposition(|byte| *byte == b')') + { + buffer[last_field_start + close] = 0; + } + + let name_field = c_string(&buffer, name_start); + if name_field.is_empty() { + return; + } + let expression_field = expression_start.map_or(&[][..], |start| c_string(&buffer, start)); + let trailing_field = trailing_start.map_or(&[][..], |start| c_string(&buffer, start)); + let name_token = native_first_token(name_field); + let name_raw = if name_token.is_empty() { + b"no_name".to_vec() + } else { + name_token.to_vec() + }; + table.rows.push(FormulaRow { + name_raw, + expression_raw: expression_field.to_vec(), + trailing_raw: trailing_field.to_vec(), + }); +} + +fn c_string_end(buffer: &[u8], start: usize) -> usize { + buffer[start..] + .iter() + .position(|byte| *byte == 0) + .map_or(buffer.len(), |offset| start + offset) +} + +fn c_string(buffer: &[u8], start: usize) -> &[u8] { + &buffer[start..c_string_end(buffer, start)] +} + +fn native_first_token(field: &[u8]) -> &[u8] { + let start = field + .iter() + .position(|byte| *byte == 0 || *byte > 32) + .unwrap_or(field.len()); + let end = field[start..] + .iter() + .position(|byte| *byte == 0 || *byte <= 32) + .map_or(field.len(), |offset| start + offset); + &field[start..end] +} + +fn parse_native_numeric_literal(expression: &[u8]) -> Option<f32> { + let start = expression + .iter() + .position(|byte| *byte == 0 || *byte > 32) + .unwrap_or(expression.len()); + let expression = &expression[start..]; + let token_len = native_numeric_token_len(expression)?; + if expression[token_len..] + .iter() + .any(|byte| *byte != 0 && *byte > 32) + { + return None; + } + let converted_len = native_decimal_prefix_len(&expression[..token_len])?; + let prefix = std::str::from_utf8(&expression[..converted_len]).ok()?; + prefix.parse::<f64>().ok().map(|value| value as f32) +} + +fn native_numeric_token_len(expression: &[u8]) -> Option<usize> { + if !expression.first().is_some_and(u8::is_ascii_digit) { + return None; + } + let mut index = 0; + let mut dot_allowed = true; + let mut exponent_sign_allowed = false; + while let Some(&byte) = expression.get(index) { + if byte.is_ascii_digit() { + exponent_sign_allowed = false; + index += 1; + } else if byte == b'.' && dot_allowed { + dot_allowed = false; + exponent_sign_allowed = false; + index += 1; + } else if matches!(byte, b'e' | b'E') { + dot_allowed = true; + exponent_sign_allowed = true; + index += 1; + } else if matches!(byte, b'+' | b'-') && exponent_sign_allowed { + exponent_sign_allowed = false; + index += 1; + } else { + break; + } + } + Some(index) +} + +fn native_decimal_prefix_len(token: &[u8]) -> Option<usize> { + let mut index = 0; + while token.get(index).is_some_and(u8::is_ascii_digit) { + index += 1; + } + if token.get(index) == Some(&b'.') { + index += 1; + while token.get(index).is_some_and(u8::is_ascii_digit) { + index += 1; + } + } + if index == 0 { + return None; + } + + if token + .get(index) + .is_some_and(|byte| matches!(byte, b'e' | b'E')) + { + let exponent_start = index; + index += 1; + if token + .get(index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + index += 1; + } + let digits_start = index; + while token.get(index).is_some_and(u8::is_ascii_digit) { + index += 1; + } + if digits_start == index { + index = exponent_start; + } + } + 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); + if !value.is_finite() || value < -SIGNED_QWORD_LIMIT || value >= SIGNED_QWORD_LIMIT { + return None; + } + Some(value.trunc() as i64 as u32) +} + /// A compiled `.scr` package. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ScriptPackage { @@ -1317,15 +1951,58 @@ fn read_instruction( #[cfg(test)] mod tests { use super::{ - decode, decode_with_limits, parse_varset, Handler15ResolveError, Handler15TargetPayload, - Handler19DwordWrite, Handler19InitInput, Handler19InitResolution, Handler19ResolveError, + decode, decode_with_limits, parse_varset, FormulaTable, Handler15ResolveError, + Handler15TargetPayload, Handler19DwordWrite, Handler19InitInput, Handler19ResolveError, Handler2RecordInput, Handler2RecordScheduler, Handler30ResolveError, Handler8Reset, - Handler8ResolveError, ScriptDispatchSelector, ScriptEvent, ScriptInstruction, - ScriptPackage, VarSetDefault, VarSetError, VarSetType, VmHostCallbackCommand, - GOG_HANDLER_COUNT, INSTRUCTION_WORDS, + Handler8ResolveError, ScriptDispatchSelector, ScriptEvent, ScriptInitResolution, + ScriptInitResolveError, ScriptInitWrite, ScriptInstruction, ScriptPackage, VarSetDefault, + VarSetError, VarSetType, VmHostCallbackCommand, GOG_HANDLER_COUNT, INSTRUCTION_WORDS, }; 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, @@ -1470,6 +2147,73 @@ STRING( 8, ignored, ignored, ignored)\r\n"; } #[test] + fn formula_table_preserves_native_rows_and_field_defaults() { + let table = FormulaTable::parse( + b"// a comment\r\nFUNCTION(no_fields)\r\nFUNCTION(name,0)\r\nFUNCTION(name,0,)\r\nFUNCTION( , 2, )\r\nFUNCTION(, 3,)\r\nFUNCTION missing_parenthesis,4,\r\nFUNCTION()\r\n/x FUNCTION(ignored,5,)\r\nx/FUNCTION(ignored,6,)\r\nx,FUNCTION(name,0,)\r\nx,FUNCTION(name)\r\n", + ); + + assert_eq!(table.rows.len(), 6); + assert_eq!(table.rows[0].name_raw, b"no_fields"); + assert!(table.rows[0].expression_raw.is_empty()); + assert_eq!(table.rows[1].name_raw, b"name"); + assert_eq!(table.rows[1].expression_raw, b"0"); + assert!(table.rows[1].trailing_raw.is_empty()); + assert_eq!(table.rows[2].expression_raw, b"0"); + assert!(table.rows[2].trailing_raw.is_empty()); + assert_eq!(table.rows[3].name_raw, b"no_name"); + assert_eq!(table.rows[3].expression_raw, b" 2"); + assert_eq!(table.rows[4].name_raw, b"name"); + assert_eq!(table.rows[4].expression_raw, b"FUNCTION(name"); + assert_eq!(table.rows[4].trailing_raw, b"0"); + assert_eq!(table.rows[5].name_raw, b"name"); + assert_eq!(table.rows[5].expression_raw, b"FUNCTION(name"); + assert!(table.rows[5].trailing_raw.is_empty()); + } + + #[test] + fn formula_table_chunks_without_reassembling_and_stops_at_text_eof() { + let mut source = vec![b' '; 255]; + source.extend_from_slice(b"FUNCTION(before, 1, )\r\n"); + source.extend_from_slice(b"FUNCTION(after, 2, )\r\n"); + source.push(0x1a); + source.extend_from_slice(b"FUNCTION(ignored, 3, )\r\n"); + + let table = FormulaTable::parse(&source); + assert_eq!(table.rows.len(), 2); + assert_eq!(table.rows[0].name_raw, b"before"); + assert_eq!(table.rows[1].name_raw, b"after"); + } + + #[test] + fn formula_table_uses_only_the_nul_terminated_view_of_each_chunk() { + let table = FormulaTable::parse( + b"FUNCTION(visible, 1, )\0 FUNCTION(hidden, 2, )\nFUNCTION(next, 3, )\n", + ); + assert_eq!(table.rows.len(), 2); + assert_eq!(table.rows[0].name_raw, b"visible"); + assert_eq!(table.rows[1].name_raw, b"next"); + } + + #[test] + fn formula_literal_evaluation_uses_the_native_numeric_prefix() { + let table = FormulaTable::parse( + b"FUNCTION(scientific,1e-2,)\nFUNCTION(integer_dot,1.,)\nFUNCTION(exp_prefix,1e2.3,)\nFUNCTION(incomplete,1e+,)\nFUNCTION(unary,-1,)\nFUNCTION(operator,1+2,)\n", + ); + + for (index, expected) in [(0, 0.01_f32), (1, 1.0), (2, 100.0), (3, 1.0)] { + assert_eq!( + table + .evaluate_numeric_literal(index) + .expect("native numeric token"), + expected + ); + } + assert!(table.evaluate_numeric_literal(4).is_err()); + assert!(table.evaluate_numeric_literal(5).is_err()); + assert!(table.evaluate_numeric_literal(6).is_err()); + } + + #[test] fn handler_thirty_resolves_only_proven_dword_operands() { let varset = parse_varset( b"VAR( float, f0, 0.5)\nVAR( DWORD, first, 0x12)\nVAR( DWORD, second, 9)\n", @@ -1684,11 +2428,11 @@ STRING( 8, ignored, ignored, ignored)\r\n"; } #[test] - fn handler_nineteen_init_uses_first_exact_event_and_keeps_instruction_order() { + fn script_init_uses_first_exact_event_and_keeps_instruction_order() { let varset = parse_varset(b"VAR( DWORD, x, 0)\nVAR( DWORD, y, 0)\nVAR( DWORD, id, 0)\n") .expect("DWORD targets"); let action = |references: Vec<u32>| ScriptInstruction { - header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0], + header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX], references, }; let package = ScriptPackage { @@ -1715,34 +2459,53 @@ STRING( 8, ignored, ignored, ignored)\r\n"; }; let result = varset - .resolve_handler19_init( + .resolve_init( &package, Handler19InitInput { first_x87_word: 7, second_x87_word: 8, third_word: 9, }, + None, ) .expect("valid first Init event"); assert_eq!( result, - Handler19InitResolution::Executed { + ScriptInitResolution::Executed { event_index: 0, event_word: 0x1234, writes: vec![ - Handler19DwordWrite { index: 0, value: 7 }, - Handler19DwordWrite { index: 1, value: 8 }, - Handler19DwordWrite { index: 2, value: 9 }, - Handler19DwordWrite { index: 1, value: 7 }, - Handler19DwordWrite { index: 0, value: 8 }, - Handler19DwordWrite { index: 2, value: 9 }, + ScriptInitWrite { + index: 0, + value: VarSetDefault::Dword(7), + }, + ScriptInitWrite { + index: 1, + value: VarSetDefault::Dword(8), + }, + ScriptInitWrite { + index: 2, + value: VarSetDefault::Dword(9), + }, + ScriptInitWrite { + index: 1, + value: VarSetDefault::Dword(7), + }, + ScriptInitWrite { + index: 0, + value: VarSetDefault::Dword(8), + }, + ScriptInitWrite { + index: 2, + value: VarSetDefault::Dword(9), + }, ], } ); } #[test] - fn handler_nineteen_init_missing_exact_name_has_no_writes() { + fn script_init_missing_exact_name_has_no_writes() { let varset = parse_varset(b"").expect("empty varset"); let package = ScriptPackage { opcode_handler_count: 73, @@ -1765,20 +2528,21 @@ STRING( 8, ignored, ignored, ignored)\r\n"; }; assert_eq!( - varset.resolve_handler19_init( + varset.resolve_init( &package, Handler19InitInput { first_x87_word: 7, second_x87_word: 8, third_word: 9, }, + None, ), - Ok(Handler19InitResolution::Missing) + Ok(ScriptInitResolution::Missing) ); } #[test] - fn handler_nineteen_init_accepts_empty_first_event_as_executed_with_no_writes() { + fn script_init_accepts_empty_first_event_as_executed_with_no_writes() { let varset = parse_varset(b"").expect("empty varset"); let package = ScriptPackage { opcode_handler_count: 73, @@ -1804,15 +2568,16 @@ STRING( 8, ignored, ignored, ignored)\r\n"; }; assert_eq!( - varset.resolve_handler19_init( + varset.resolve_init( &package, Handler19InitInput { first_x87_word: 7, second_x87_word: 8, third_word: 9, }, + None, ), - Ok(Handler19InitResolution::Executed { + Ok(ScriptInitResolution::Executed { event_index: 0, event_word: 0x1234, writes: Vec::new(), @@ -1821,7 +2586,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; } #[test] - fn handler_nineteen_init_reports_unsupported_selector_and_discards_prior_writes() { + fn script_init_reports_unsupported_selector_and_discards_prior_writes() { let varset = parse_varset(b"VAR( DWORD, x, 0)\nVAR( DWORD, y, 0)\nVAR( DWORD, id, 0)\n") .expect("DWORD targets"); let package = ScriptPackage { @@ -1832,7 +2597,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; event_word: 0, instructions: vec![ ScriptInstruction { - header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0], + header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX], references: vec![0, 1, 2], }, ScriptInstruction { @@ -1840,7 +2605,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; references: Vec::new(), }, ScriptInstruction { - header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0], + header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX], references: vec![0, 1, 2], }, ], @@ -1850,18 +2615,19 @@ STRING( 8, ignored, ignored, ignored)\r\n"; }; assert_eq!( - varset.resolve_handler19_init( + varset.resolve_init( &package, Handler19InitInput { first_x87_word: 7, second_x87_word: 8, third_word: 9, }, + None, ), - Ok(Handler19InitResolution::Unsupported { + Ok(ScriptInitResolution::Unsupported { event_index: 0, instruction_index: 1, - error: Handler19ResolveError::UnsupportedSelector { + error: ScriptInitResolveError::UnsupportedSelector { found: super::ScriptDispatchSelector::Handler(18), }, }) @@ -1869,7 +2635,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; } #[test] - fn handler_nineteen_init_stops_at_unsupported_selector_before_any_write() { + fn script_init_stops_at_unsupported_selector_before_any_write() { let varset = parse_varset(b"VAR( DWORD, x, 0)\nVAR( DWORD, y, 0)\nVAR( DWORD, id, 0)\n") .expect("DWORD targets"); let package = ScriptPackage { @@ -1884,7 +2650,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; references: Vec::new(), }, ScriptInstruction { - header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0], + header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX], references: vec![0, 1, 2], }, ], @@ -1894,18 +2660,19 @@ STRING( 8, ignored, ignored, ignored)\r\n"; }; assert_eq!( - varset.resolve_handler19_init( + varset.resolve_init( &package, Handler19InitInput { first_x87_word: 7, second_x87_word: 8, third_word: 9, }, + None, ), - Ok(Handler19InitResolution::Unsupported { + Ok(ScriptInitResolution::Unsupported { event_index: 0, instruction_index: 0, - error: Handler19ResolveError::UnsupportedSelector { + error: ScriptInitResolveError::UnsupportedSelector { found: super::ScriptDispatchSelector::Handler(18), }, }) @@ -1913,7 +2680,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; } #[test] - fn handler_nineteen_init_types_unsupported_control_and_result_save() { + fn script_init_uses_handler_nineteen_control_and_result_fields() { let varset = parse_varset(b"VAR( DWORD, x, 0)\nVAR( DWORD, y, 0)\nVAR( DWORD, id, 0)\n") .expect("DWORD targets"); let input = Handler19InitInput { @@ -1921,41 +2688,484 @@ STRING( 8, ignored, ignored, ignored)\r\n"; second_x87_word: 8, third_word: 9, }; - let instruction = |control_word, result_word| ScriptInstruction { - header_words: [19, control_word, result_word, 0, 0, 3, 0], - references: vec![0, 1, 2], - }; - let resolve = |instruction| { + let resolve = |header_words| { let package = ScriptPackage { opcode_handler_count: 73, events: vec![ScriptEvent { name_len: 4, name_raw: b"Init\0".to_vec(), event_word: 0, - instructions: vec![instruction], + instructions: vec![ScriptInstruction { + header_words, + references: vec![0, 1, 2], + }], }], trailing_bytes: Vec::new(), raw: std::sync::Arc::from(&b""[..]), }; - varset.resolve_handler19_init(&package, input) + varset.resolve_init(&package, input, None) + }; + + let actual = ( + resolve([19, u32::MAX, u32::MAX, 2, 6, 3, u32::MAX]), + resolve([19, 0x1234, u32::MAX, u32::MAX, 6, 3, u32::MAX]), + resolve([19, u32::MAX, 0xdecafbad, u32::MAX, 6, 3, u32::MAX]), + ); + assert_eq!( + actual, + ( + Ok(ScriptInitResolution::Unsupported { + event_index: 0, + instruction_index: 0, + error: ScriptInitResolveError::Handler19( + Handler19ResolveError::UnsupportedControlWord { found: 2 }, + ), + }), + Ok(ScriptInitResolution::Unsupported { + event_index: 0, + instruction_index: 0, + error: ScriptInitResolveError::Handler19( + Handler19ResolveError::UnsupportedResultSave { found: 0x1234 }, + ), + }), + Ok(ScriptInitResolution::Executed { + event_index: 0, + event_word: 0, + writes: vec![ + ScriptInitWrite { + index: 0, + value: VarSetDefault::Dword(7), + }, + ScriptInitWrite { + index: 1, + value: VarSetDefault::Dword(8), + }, + ScriptInitWrite { + index: 2, + value: VarSetDefault::Dword(9), + }, + ], + }), + ) + ); + } + + #[test] + fn script_init_assigns_numeric_literals_using_the_target_type() { + let varset = parse_varset(b"VAR(DWORD, integer, 0)\nVAR(float, scalar, 0)\n") + .expect("numeric declarations"); + let package = ScriptPackage { + opcode_handler_count: 73, + events: vec![ScriptEvent { + name_len: 4, + name_raw: b"Init\0".to_vec(), + event_word: 0, + instructions: vec![ + ScriptInstruction { + header_words: [u32::MAX, 0, u32::MAX, u32::MAX, 0xdead_beef, 0, 0], + references: Vec::new(), + }, + ScriptInstruction { + header_words: [u32::MAX, 1, u32::MAX, u32::MAX, 0xdead_beef, 0, 1], + references: Vec::new(), + }, + ], + }], + trailing_bytes: Vec::new(), + raw: std::sync::Arc::from(&b""[..]), + }; + let formulas = FormulaTable::parse( + b"FUNCTION(integer, 12.75, )\nFUNCTION(scalar, 0.8, )\nFUNCTION(unused, target + 2, )\n", + ); + + assert_eq!( + varset.resolve_init( + &package, + Handler19InitInput { + first_x87_word: 0, + second_x87_word: 0, + third_word: 0, + }, + Some(&formulas), + ), + Ok(ScriptInitResolution::Executed { + event_index: 0, + event_word: 0, + writes: vec![ + ScriptInitWrite { + index: 0, + value: VarSetDefault::Dword(12), + }, + ScriptInitWrite { + index: 1, + value: VarSetDefault::FloatBits(0x3f4c_cccd), + }, + ], + }) + ); + } + + #[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, target, 0)\nVAR(DWORD, integer, 0)\n") + .expect("numeric declaration"); + let formulas = + FormulaTable::parse(b"FUNCTION(unsupported, target * 2, )\nFUNCTION(literal, 7.5, )\n"); + let action = |formula_index| ScriptInstruction { + header_words: [u32::MAX, 1, u32::MAX, u32::MAX, 6, 0, formula_index], + references: Vec::new(), + }; + let package = |formula_index| ScriptPackage { + opcode_handler_count: 73, + events: vec![ScriptEvent { + name_len: 4, + name_raw: b"Init\0".to_vec(), + event_word: 0, + instructions: vec![action(formula_index)], + }], + trailing_bytes: Vec::new(), + raw: std::sync::Arc::from(&b""[..]), + }; + let input = Handler19InitInput { + first_x87_word: 0, + second_x87_word: 0, + third_word: 0, }; assert_eq!( - resolve(instruction(0, u32::MAX)), - Ok(Handler19InitResolution::Unsupported { + varset.resolve_init(&package(1), input, Some(&formulas)), + Ok(ScriptInitResolution::Executed { + event_index: 0, + event_word: 0, + writes: vec![ScriptInitWrite { + index: 1, + value: VarSetDefault::Dword(7), + }], + }) + ); + assert_eq!( + varset.resolve_init(&package(0), input, Some(&formulas)), + Ok(ScriptInitResolution::Unsupported { event_index: 0, instruction_index: 0, - error: Handler19ResolveError::UnsupportedControlWord { found: 0 }, + error: ScriptInitResolveError::UnsupportedFormulaExpression { + index: 0, + expression: b" target * 2".to_vec(), + }, }) ); + + let mut transactional = package(0); + transactional.events[0].instructions.insert(0, action(1)); assert_eq!( - resolve(instruction(u32::MAX, 2)), - Ok(Handler19InitResolution::Unsupported { + varset.resolve_init(&transactional, input, Some(&formulas)), + Ok(ScriptInitResolution::Unsupported { + event_index: 0, + instruction_index: 1, + error: ScriptInitResolveError::UnsupportedFormulaExpression { + index: 0, + expression: b" target * 2".to_vec(), + }, + }) + ); + } + + #[test] + fn script_init_keeps_missing_table_and_bad_indices_typed() { + let varset = parse_varset(b"VAR(DWORD, integer, 0)\n").expect("numeric declaration"); + let instruction = |target, formula_index| ScriptInstruction { + header_words: [u32::MAX, target, u32::MAX, u32::MAX, 6, 0, formula_index], + references: Vec::new(), + }; + let package = |target, formula_index| ScriptPackage { + opcode_handler_count: 73, + events: vec![ScriptEvent { + name_len: 4, + name_raw: b"Init\0".to_vec(), + event_word: 0, + instructions: vec![instruction(target, formula_index)], + }], + trailing_bytes: Vec::new(), + raw: std::sync::Arc::from(&b""[..]), + }; + let input = Handler19InitInput { + first_x87_word: 0, + second_x87_word: 0, + third_word: 0, + }; + let formulas = FormulaTable::parse(b"FUNCTION(literal, 1, )\n"); + + assert_eq!( + varset.resolve_init(&package(0, 0), input, None), + Ok(ScriptInitResolution::Unsupported { event_index: 0, instruction_index: 0, - error: Handler19ResolveError::UnsupportedResultSave { found: 2 }, + error: ScriptInitResolveError::MissingFormulaTable, }) ); + assert_eq!( + varset.resolve_init(&package(1, 0), input, Some(&formulas)), + Err(ScriptInitResolveError::VarSetIndexOutOfBounds { + index: 1, + declarations: 1, + }) + ); + assert_eq!( + varset.resolve_init(&package(0, 1), input, Some(&formulas)), + Err(ScriptInitResolveError::FormulaIndexOutOfBounds { index: 1, rows: 1 }) + ); } #[test] |
