feat(script): evaluate additive FML expressions during Init
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
This commit is contained in:
5 files changed
+856
-104
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -133,7 +133,7 @@ control word процесса.
|
||||
Лицензированный AutoDemo по-прежнему загружается с raw TMA anchor-ами
|
||||
`[(500.2857, 751.8217), (728.0, 449.0)]` и записями `[(500, 752, 0),
|
||||
(728, 449, 1)]`. Rust теперь также читает optional `.fml` sibling для каждого
|
||||
script base и поддерживает numeric literal assignments в первом `Init`.
|
||||
script base и поддерживает узкий набор атомарных формул в первом `Init`.
|
||||
Отсутствующий `.fml` не мешает пакету с одними `Handler(19)`; другие ошибки
|
||||
чтения распространяются как VFS errors.
|
||||
|
||||
@@ -144,9 +144,11 @@ script base и поддерживает numeric literal assignments в перв
|
||||
два доказанных действия: `Handler(19)` и formula assignment selector `-1` при
|
||||
source `-1`, нулевой глубине условий и корректных индексах. У `Handler(19)`
|
||||
control находится в `header_words[3]`, а `header_words[1]` — result-save
|
||||
reference; оба равны `0xffffffff` в поддержанной форме. У formula assignment
|
||||
`header_words[1]` — индекс целевой varset ячейки, `header_words[2]` — source,
|
||||
а индекс formula row занимает `header_words[6]`. Маппинг подтверждён
|
||||
reference; оба равны `0xffffffff` в поддержанной форме. В дисковом заголовке
|
||||
formula assignment `header_words[1]` — индекс целевой varset ячейки,
|
||||
`header_words[2]` — source, а индекс formula row занимает `header_words[6]`;
|
||||
индексы цели и формулы в native record находятся по смещениям `+0x08` и `+0x1c`
|
||||
соответственно. Маппинг подтверждён
|
||||
`ai.dll!FUN_10011b20` (RVA `0x11b20`) и исполнителем `FUN_10012020` (RVA
|
||||
`0x12020`), SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`.
|
||||
`Handler(19)` (`FUN_1000aa38`, RVA `0xaa38`) использует references `0..2`;
|
||||
@@ -154,23 +156,58 @@ reference; оба равны `0xffffffff` в поддержанной форме
|
||||
|
||||
Formula table загружается из `<bundle-base>.fml`; native `FUN_100123b0`
|
||||
(RVA `0x123b0`) проверяет ordinal, compiler `FUN_100152a0` (RVA `0x152a0`)
|
||||
строит numeric literal node, а evaluator `FUN_10015b30` (RVA `0x15b30`)
|
||||
возвращает его raw `f32`. Target type `3` сохраняет эти bits; type `5` проходит
|
||||
через `FUN_10012c00` (RVA `0x12c00`) и `__ftol` (RVA `0x1df70`) к raw DWORD
|
||||
с усечением к нулю. Изолированный x86 oracle на оригинальном `ai.dll` и
|
||||
настоящих ресурсах подтвердил таблицы `tut1_pl2`/`scr_pl_1`/`scream` с
|
||||
строит числовые, переменные и операторные узлы, а evaluator `FUN_10015b30`
|
||||
(RVA `0x15b30`) в проверенных путях `+` и `-` считывает значения переменных и
|
||||
сохраняет результаты как `f32` при x87 control word `0x007f`. В GOG-сборке
|
||||
`ai.dll` с SHA-256
|
||||
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` поиск
|
||||
переменной через varset vtable slot `+0x10` ведёт на RVA `0x16ad0`: сравнение
|
||||
байтов точное, регистр учитывается, при повторах берётся первое объявление.
|
||||
Getter `FUN_10013190` (RVA `0x13190`) читает type `3` как `f32`, type `5` как
|
||||
беззнаковый `DWORD` и возвращает число через x87. Затем evaluator сохраняет
|
||||
его как `f32` до операции. Оператор `+`
|
||||
считает right+left, `-` — left-right; результат также сохраняется как `f32`.
|
||||
Target type `3` сохраняет эти bits; type `5` проходит через `FUN_10012c00`
|
||||
(RVA `0x12c00`) и `__ftol` (RVA `0x1df70`) к raw DWORD с усечением к нулю.
|
||||
Изолированный x86 oracle на оригинальном `ai.dll` и настоящих ресурсах
|
||||
подтвердил таблицы `tut1_pl2`/`scr_pl_1`/`scream` с
|
||||
26/5/149 строками и 15 присваиваний: кампанийные `df0..df3` и `Single.01`
|
||||
`dT8/dT9` получают `0`; `scream` задаёт `fDifficulty = 0.8f` и остальные
|
||||
проверенные DWORD `0, 0, 1, 10, 0, 100, 400, 2`.
|
||||
|
||||
Граница Rust остаётся узкой: ссылка должна вести на один digit-starting numeric
|
||||
token и типизированную `float`/`DWORD` ячейку. Неизвестные expression nodes,
|
||||
переменные, unary operators, составные выражения и source-copy ветвь
|
||||
`source != 0xffffffff` остаются явными ошибками; native source-copy переносит
|
||||
всю varset record, не только числовое значение. В preview package сохраняется,
|
||||
но `Unsupported` не применяет частичные writes; полная загрузка отклоняет весь
|
||||
новый мир и сохраняет предыдущие world/RNG. Это политика Rust, не правило
|
||||
оригинального dispatcher-а.
|
||||
Граница Rust остаётся узкой: один числовой токен либо ASCII-имя переменной,
|
||||
или два таких атома с одним бинарным `+`/`-`. Имя проверяется раньше числового
|
||||
преобразования, сравнивается с учётом регистра и выбирает первое совпадение;
|
||||
публичный `FormulaTable::evaluate_numeric_literal` принимает только числовые
|
||||
литералы. Временная копия значений начинается со значений по умолчанию и
|
||||
обновляется после каждого успешного действия `Init`, поэтому следующая формула
|
||||
видит предыдущие записи. Нет
|
||||
поддержки неизвестных операторов, унарных форм, третьего атома, отсутствующих
|
||||
или не-ASCII имён и source-copy ветви `source != 0xffffffff`; native source-copy
|
||||
переносит всю запись varset. Preview сохраняет package, но не применяет частичные
|
||||
writes; полный load отклоняет новый мир и сохраняет прежние world/RNG. Это
|
||||
политика ограниченного Rust resolver-а, а не правило оригинального dispatcher-а.
|
||||
|
||||
Ресурсные строки с переменными найдены в поздних событиях: `c1m3e.fml` row 19
|
||||
(`0.8 + fT`) вызывается из `Fort_Captured`, row 22
|
||||
(`dBuildingProtectHits - dT2`) — из `PBM_BUILDING_PROTECT_Start`, а `c1m4e.fml`
|
||||
row 0 (`fTemp + 0.00001`) — из `PBM_BUILDING_INF_CAPTURE_Start`. Декодирование
|
||||
этих `.scr` не показывает эти формулы в первом `Init`. Игнорируемая проверка
|
||||
читает настоящие `.fml` и `varset.var`, но вставляет строки в синтетический
|
||||
`Init`; она проверяет resolver, а не оригинальное исполнение. Одноразовый x86
|
||||
oracle на исходной GOG `ai.dll` с SHA-256
|
||||
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` проверил
|
||||
13 отдельных присваиваний: 9 контролируемых выражений и 4 вызова с настоящими
|
||||
строками `.fml` (`c1m3e` rows 19/22, `c1m4e` row 0 дважды подряд). В каждом векторе
|
||||
native harness записывал вычисленный `f32`, типизированное raw-слово и getter;
|
||||
scratch-проход через публичный Rust parser/resolver совпал со всеми 13
|
||||
векторами по `f32`-результату и raw-записи. Для native вызовов control word
|
||||
временно принудительно устанавливался в `0x007f`: 87 наблюдений сохранили
|
||||
`0x027f` на входе и после восстановления,
|
||||
а canary-проверки прошли. Менялись только varset-копии в памяти. Это
|
||||
подтверждает компиляцию формулы, локальное вычисление и typed writes для этих
|
||||
входов, но не запуск исходного поиска и обхода события `Init`, исполнение поздних
|
||||
событий или поведение живой миссии.
|
||||
|
||||
Лицензированный test загружает одну первую кампанийную миссию
|
||||
`CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma` после literal writes; это не проход
|
||||
|
||||
@@ -22,8 +22,8 @@ symbol/formula oracle; `varset.var` задаёт `VAR(...)`/`STRING(...)` defaul
|
||||
`fparkan-script::parse_varset` уже читает подтверждённые numeric
|
||||
`VAR(float|DWORD, name, default)` declarations byte-safe (comments остаются
|
||||
opaque, поэтому legacy non-UTF-8 text не ломает загрузку); `STRING(...)` и
|
||||
general `FUNCTION(...)` evaluation остаётся вне numeric contract, но принятые
|
||||
строки сохраняются для доказанных literal assignments первого `Init`;
|
||||
general `FUNCTION(...)` evaluation остаются вне восстановленного контракта,
|
||||
но принятые строки сохраняются для ограниченного поднабора формул первого `Init`;
|
||||
GOG `MISSIONS/SCRIPTS/varset.var` даёт через него ровно 231 declaration:
|
||||
31 `float` и 200 `DWORD` (от `f0` до `fY`);
|
||||
loader `ai.dll!0x10001000` сначала открывает `<bundle-base>.var` и только при
|
||||
@@ -424,10 +424,13 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D
|
||||
|
||||
Ограниченный Rust-исполнитель `VarSet::resolve_init` ищет первое событие
|
||||
`Init` и проходит его инструкции по порядку. Он возвращает записи для
|
||||
поддержанного `Handler(19)` и числовых literal assignments; прочая форма
|
||||
останавливает preview на `Unsupported`. Runtime применяет упорядоченные записи
|
||||
к отдельной копии varset выбранного клана лишь после успешного прохода всего
|
||||
события. Отсутствующий `Init` даёт
|
||||
поддержанного `Handler(19)` и формул с одним числовым или переменным атомом либо
|
||||
с двумя такими атомами, соединёнными одним бинарным `+` или `-`; прочая форма
|
||||
останавливает preview на `Unsupported`. Runtime начинает вычисление с копии
|
||||
defaults и записывает в неё каждый успешно разрешённый шаг, поэтому поздняя
|
||||
формула видит прежние записи этого события. Упорядоченные записи попадают в
|
||||
состояние клана только после успешного прохода всего `Init`. Отсутствующий
|
||||
`Init` даёт
|
||||
статус `Missing` и не создаёт записей инициализации; пустой первый `Init` даёт
|
||||
`Executed` без записей. Если форма инструкции пока не поддержана, package
|
||||
остаётся доступен в preview режиме со статусом `Unsupported`, а default-значения
|
||||
@@ -437,7 +440,7 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D
|
||||
Это правила ограниченного Rust-исполнителя, а не обнаруженное поведение
|
||||
оригинального dispatcher-а.
|
||||
|
||||
#### Числовые литералы `.fml` в `Init`
|
||||
#### Литералы и простые формулы `.fml` в `Init`
|
||||
|
||||
В той же сборке `ai.dll` (`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`)
|
||||
загрузчик `FUN_10001000` открывает `.fml` рядом с базой каждого `.scr`.
|
||||
@@ -455,16 +458,35 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D
|
||||
остаётся ошибкой VFS.
|
||||
|
||||
Native selector `-1` при source `-1` и нулевой глубине условий выбирает
|
||||
formula assignment; в native record `header_words[1]` — индекс целевой varset
|
||||
ячейки, а `header_words[6]` — индекс formula row. `FUN_100123b0` (RVA `0x123b0`) проверяет индекс строки,
|
||||
`FUN_10015b30` (RVA `0x15b30`) возвращает результат единственного literal node
|
||||
kind `3`, а `FUN_10013650` передаёт его типизированной ячейке через
|
||||
`FUN_10012c00` (RVA `0x12c00`). Компилятор `FUN_100152a0` получает числовой
|
||||
prefix как `double` через `FUN_10020af0` (RVA `0x20af0`), затем записывает
|
||||
его как `float` в node `+0x10`. Rust ограничивает эту ветвь одним
|
||||
digit-starting числовым токеном с необязательным хвостом пробельных байтов;
|
||||
ведущие unary `+`/`-`, variable references, операторы и другие node kinds
|
||||
остаются явными `UnsupportedExpression`.
|
||||
formula assignment. В дисковом заголовке `.scr` `header_words[1]` — индекс
|
||||
целевой varset-ячейки, а `header_words[6]` — индекс formula row; соответствующие
|
||||
поля native record находятся по смещениям `+0x08` и `+0x1c` соответственно.
|
||||
`FUN_100123b0` (RVA `0x123b0`) проверяет индекс строки. Компилятор
|
||||
`FUN_100152a0` (RVA `0x152a0`) создаёт
|
||||
числовые узлы, узлы переменных и узлы операторов; числовой prefix проходит через
|
||||
`FUN_10020af0` (RVA `0x20af0`) как `double`, после чего сохраняется в узле как
|
||||
`float`. Для переменной компилятор вызывает vtable slot `+0x10` таблицы varset;
|
||||
в проверенной GOG-сборке `ai.dll` этот вызов ведёт к поиску по RVA `0x16ad0`.
|
||||
Поиск сравнивает байты имени с учётом регистра и возвращает первое совпадение.
|
||||
`FUN_10015b30` (RVA `0x15b30`) читает числовой узел как `f32`, а переменную —
|
||||
через `FUN_10013190` (RVA `0x13190`): type `3` даёт `float`, type `5` —
|
||||
беззнаковый `DWORD`. Getter возвращает число через x87; evaluator сохраняет
|
||||
его как `f32` перед операцией. Оператор `+` складывает правый
|
||||
и левый операнды, а `-` вычисляет левый минус правый; после узла результат
|
||||
сохраняется как `f32`. `FUN_10013650` передаёт итог типизированной ячейке через
|
||||
`FUN_10012c00` (RVA `0x12c00`).
|
||||
|
||||
Rust в первом `Init` принимает один числовой токен или имя ASCII-переменной,
|
||||
а также ровно два таких атома с одним бинарным `+` либо `-`. Например,
|
||||
`source + 2.0` и `target - source` читают значения из defaults или из предыдущих
|
||||
успешных записей события. Имя ищется раньше числового преобразования, поэтому
|
||||
объявление с именем `1e-2` перекрывает такую числовую запись; сравнение имён
|
||||
чувствительно к регистру, а повтор объявлений выбирает первое. `DWORD` читается
|
||||
как беззнаковое число и округляется до `f32` до операции. Публичный
|
||||
`FormulaTable::evaluate_numeric_literal` по-прежнему принимает только литералы.
|
||||
Ведущие unary `+`/`-`, `*`, другие операторы, третий атом, отсутствующие или
|
||||
не-ASCII имена и зарезервированные одиночные операторы остаются `Unsupported`;
|
||||
так же не поддержана ветвь копирования `source != 0xffffffff`.
|
||||
|
||||
Проверенный native tokenizer отличается от обычного строгого decimal parser:
|
||||
`1e-2`, `1.`, `1e`, `1e+`, `1ee2` и `1e2.3` были приняты как один literal node
|
||||
@@ -477,8 +499,8 @@ digit-starting числовым токеном с необязательным
|
||||
исходные `f32` bits; `DWORD` (tag `5`) проходит через `__ftol` (RVA `0x1df70`)
|
||||
с усечением к нулю и записывает младший DWORD. Поэтому literal `0.8` остаётся
|
||||
`0x3f4ccccd` в `float`, но записывается как `0` в `DWORD`; `100.0` даёт raw
|
||||
DWORD `100`. Неизвестные выражения не заменяются нулём, а запись всего первого
|
||||
`Init` применяется только после успешного прохода.
|
||||
DWORD `100`. Неизвестные выражения не заменяются нулём, а записи первого `Init`
|
||||
публикуются только после успешного прохода.
|
||||
|
||||
Native x86 oracle на GOG `ai.dll` SHA-256
|
||||
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` загрузил
|
||||
@@ -500,6 +522,27 @@ Native x86 oracle на GOG `ai.dll` SHA-256
|
||||
| 7 | `dMaxPlant` (`DWORD`) | `400 → 400` |
|
||||
| 8 | `d7` (`DWORD`) | `2 → 2` |
|
||||
|
||||
Отдельный одноразовый x86 oracle на той же GOG `ai.dll` проверил ещё 13
|
||||
выражений: 9 строк контролируемой `controlled.fml`, строки 19 и 22 настоящей
|
||||
`c1m3e.fml` и два последовательных вызова строки 0 настоящей `c1m4e.fml`.
|
||||
Native harness записывал `f32`-результат evaluator-а, типизированную raw-запись
|
||||
и getter после записи. В scratch-проверке те же входы прошли через публичные
|
||||
`parse_varset`, `FormulaTable::parse` и `VarSet::resolve_init`; результат
|
||||
evaluator-а и raw-слово Rust совпали с native во всех 13 случаях. Среди
|
||||
проверенных границ: `2 - 3` записал в `DWORD` `0xffffffff`; чтение исходного
|
||||
`DWORD 0xffffffff` дало `f32 0x4f800000`, а последующее `+ 0` записало в
|
||||
`DWORD` ноль. Для настоящих строк `c1m3e` получены `fTemp = 0x40333333` и
|
||||
`dTemp3 = 0`; два последовательных вызова строки 0 `c1m4e` дали
|
||||
`fTemp = 0x4033335d`, затем `0x40333387`.
|
||||
|
||||
Oracle принудительно устанавливал x87 control word `0x007f` на время каждого
|
||||
проверяемого вызова: все 87 наблюдений показали входное и восстановленное
|
||||
значение `0x027f`, активное и выходное — `0x007f`; canary-проверки прошли.
|
||||
Менялись только varset-копии в памяти процесса. Эта выборка подтверждает
|
||||
компиляцию, вычисление и типизированные записи ровно для перечисленных входов.
|
||||
Она не запускала исходный поиск и обход события `Init` и не исполняла поздние
|
||||
события или миссию; выборка не доказывает полную грамматику evaluator-а.
|
||||
|
||||
Rust сохраняет всю таблицу строк, но вычисляет только формулу, на которую
|
||||
ссылается поддержанная запись. Лицензированный runtime-тест подтверждает
|
||||
полную загрузку кампании Mission.01 после этих присваиваний; у обычной
|
||||
@@ -511,6 +554,17 @@ Rust сохраняет всю таблицу строк, но вычисляе
|
||||
не перечисляют AI candidates и не выполняют этот handler. Это не подтверждение
|
||||
запуска самой игры или полной VM.
|
||||
|
||||
В других строках тех же ресурсов встречаются `0.8 + fT` (`c1m3e.fml`, row 19),
|
||||
`dBuildingProtectHits - dT2` (row 22) и `fTemp + 0.00001` (`c1m4e.fml`, row 0).
|
||||
Декодированные `c1m3e.scr` и `c1m4e.scr` ссылаются на них из более поздних
|
||||
событий `Fort_Captured`, `PBM_BUILDING_PROTECT_Start` и
|
||||
`PBM_BUILDING_INF_CAPTURE_Start`, а не из их `Init`. Игнорируемый Rust-тест
|
||||
подаёт эти сохранённые строки в изменённый синтетический `Init` и использует
|
||||
настоящий общий `varset.var`; это проверка resolver-а, а не утверждение о
|
||||
исполнении таких выражений в исходном `Init`. Отдельный native oracle и
|
||||
сравнение с Rust выше проверяют эти строки при явно заданных исходных ячейках,
|
||||
но не подтверждают их исполнение в оригинальном сценарии или в живой игре.
|
||||
|
||||
### Первый вызов Behavior и сценарного `Init`
|
||||
|
||||
В той же GOG-сборке `Behavior.dll` (SHA-256
|
||||
@@ -760,8 +814,9 @@ tie-векторов подтверждает реакцию `FISTP`, но не
|
||||
два слова и третье исходное слово, выдаёт три типизированные записи и отклоняет
|
||||
отсутствующие, выходящие за диапазон или не-`DWORD` цели. Общий для первого
|
||||
события метод `VarSet::resolve_init` добавляет этот `Handler(19)` и формулу из
|
||||
одного numeric literal; остальные формы инструкции и содержимое `.scr`
|
||||
сохраняются reader-ом, но остаются вне этого исполнения.
|
||||
одного числового или переменного атома либо двух таких атомов с одним бинарным
|
||||
`+` или `-`. Остальные формы инструкции и содержимое `.scr` сохраняются
|
||||
reader-ом, но остаются вне этого исполнения.
|
||||
|
||||
### Начальная группировка подготовленных Control-дескрипторов
|
||||
|
||||
|
||||
@@ -169,8 +169,10 @@ handler лишь инициирует execution context (`+0x50 = 1`); его ga
|
||||
показывает предупреждение для чисел выше `10000`, но не отменяет создание AI.
|
||||
|
||||
Rust исполняет подтверждённый поднабор первого `Init`: действие `Handler(19)`
|
||||
и числовые literal assignments, описанные ниже. Поддержанные записи применяются
|
||||
к состоянию лишь после успешного прохода всего первого `Init`.
|
||||
и формулы из одного атома либо двух атомов с бинарным `+` или `-`. Здесь атом —
|
||||
число или имя переменной. Поддержанные шаги сразу обновляют временную копию
|
||||
varset, поэтому следующая формула читает уже записанные значения. К состоянию
|
||||
клана записи попадают лишь после успешного прохода всего первого `Init`.
|
||||
Если встретилась неподдержанная форма, preview сохраняет пакет, сообщает
|
||||
`Unsupported` и оставляет значения по умолчанию; при полной загрузке та же форма
|
||||
становится ошибкой. Это политика Rust для ограниченного исполнителя, а не правило
|
||||
@@ -186,12 +188,18 @@ Rust исполняет подтверждённый поднабор перво
|
||||
нули в `dT8` и `dT9`.
|
||||
Клан 1 той же миссии использует первые девять строк `scream.fml`: восемь
|
||||
проверенных значений попадают в `DWORD` с усечением дробной части, а `0.8`
|
||||
сохраняется как число `float`. Пока Rust вычисляет только числовой литерал,
|
||||
начинающийся с цифры; операторы, ссылки на переменные и другие формы формул
|
||||
остаются явными ошибками. В тесте полной загрузки миссия `Mission.01`
|
||||
проходит этот `Init`; `Single.01` останавливается на следующем неподдержанном
|
||||
`Handler(43)` в clan 1, записи 10. Никакое значение не подставляется вместо
|
||||
этого действия.
|
||||
сохраняется как число `float`. Например, если `source` равен `10`, выражение
|
||||
`source + 2.0` записывает `12`; следующее `target - source` читает новое значение
|
||||
`target` и получает `2`. Каждая операция работает с `f32`: значение ячейки
|
||||
`DWORD` сначала читается как беззнаковое число и переводится в `f32`, а результат
|
||||
сохраняет тип целевой ячейки. Поддерживаются только один или два атома и один
|
||||
бинарный `+` либо `-`; произвольная грамматика формул и остальная VM не
|
||||
восстановлены. В отдельной точечной проверке вычисления и записи оригинальной
|
||||
DLL и Rust совпали для 13 выбранных формул. Эта сверка проверяет только
|
||||
эти значения и не запускает исходную миссию. Тест полной загрузки подтверждает,
|
||||
что `Mission.01` проходит этот `Init`; у `Single.01` следующий неподдержанный
|
||||
`Handler(43)` встречается в clan 1, записи 10. Никакое значение не подставляется
|
||||
вместо этого действия.
|
||||
|
||||
### Первый запуск сценария
|
||||
|
||||
@@ -246,7 +254,7 @@ SuperAI для записей кланов, затем обновляет Behavi
|
||||
всё равно устанавливается. В полной загрузке Rust вызывающая сторона передаёт
|
||||
значение времени очереди; после регистрации объектов загрузчик последовательно
|
||||
подготавливает каждый сценарный пакет и выполняет восстановленные действия
|
||||
`Handler(19)` и числовые literal assignments из первого `Init`. Статический
|
||||
`Handler(19)` и поддержанные формульные присваивания из первого `Init`. Статический
|
||||
просмотр не запускает Behavior, но может сохранить эти записи `Init` в сценарном
|
||||
varset.
|
||||
|
||||
|
||||
Reference in new issue
Block a user