feat(runtime): resolve clan init writes
This commit is contained in:
@@ -56,7 +56,7 @@ fn run() -> Result<(), String> {
|
|||||||
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
|
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
|
||||||
.map_err(|err| format!("{err}"))?;
|
.map_err(|err| format!("{err}"))?;
|
||||||
println!(
|
println!(
|
||||||
"mission objects={} areals={} surfaces={} graph_roots={} components={} wear={} material_slots={} textures={} lightmaps={} scripts={} script_events={} script_varset_declarations={} graph_failures={}",
|
"mission objects={} areals={} surfaces={} graph_roots={} components={} wear={} material_slots={} textures={} lightmaps={} scripts={} script_events={} script_varset_declarations={} script_init_states={} graph_failures={}",
|
||||||
loaded.object_count,
|
loaded.object_count,
|
||||||
loaded.areal_count,
|
loaded.areal_count,
|
||||||
loaded.surface_count,
|
loaded.surface_count,
|
||||||
@@ -69,6 +69,7 @@ fn run() -> Result<(), String> {
|
|||||||
loaded.script_bundle_count,
|
loaded.script_bundle_count,
|
||||||
loaded.script_event_count,
|
loaded.script_event_count,
|
||||||
loaded.script_varset_declaration_count,
|
loaded.script_varset_declaration_count,
|
||||||
|
loaded.script_init_state_count,
|
||||||
loaded.graph_failure_count
|
loaded.graph_failure_count
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ use fparkan_prototype::{
|
|||||||
UnitComponentRecord,
|
UnitComponentRecord,
|
||||||
};
|
};
|
||||||
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
|
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
|
||||||
use fparkan_script::{ScriptPackage, VarSet};
|
use fparkan_script::{
|
||||||
|
Handler19DwordWrite, Handler19InitInput, ScriptDispatchSelector, ScriptPackage, VarSet,
|
||||||
|
};
|
||||||
use fparkan_terrain::SurfaceQuery;
|
use fparkan_terrain::SurfaceQuery;
|
||||||
use fparkan_vfs::{Vfs, VfsError};
|
use fparkan_vfs::{Vfs, VfsError};
|
||||||
use fparkan_world::{
|
use fparkan_world::{
|
||||||
@@ -248,7 +250,8 @@ pub struct MissionScriptBundle {
|
|||||||
pub base_path: String,
|
pub base_path: String,
|
||||||
/// Resolved compiled `.scr` path.
|
/// Resolved compiled `.scr` path.
|
||||||
pub script_path: String,
|
pub script_path: String,
|
||||||
/// Losslessly decoded compiled package; not yet executed by runtime.
|
/// Losslessly decoded compiled package; only proven `Handler(19)` Init
|
||||||
|
/// writes are resolved and retained by runtime.
|
||||||
pub package: ScriptPackage,
|
pub package: ScriptPackage,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +259,7 @@ pub struct MissionScriptBundle {
|
|||||||
///
|
///
|
||||||
/// GOG `ai.dll` first tries `<bundle-base>.var`, then falls back to the shared
|
/// GOG `ai.dll` first tries `<bundle-base>.var`, then falls back to the shared
|
||||||
/// `MISSIONS/SCRIPTS/varset.var`. This stores the successfully selected source
|
/// `MISSIONS/SCRIPTS/varset.var`. This stores the successfully selected source
|
||||||
/// without claiming that the not-yet-recovered VM executes its declarations.
|
/// and supplies the typed targets for the proven `Handler(19)` Init writes.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct MissionScriptVarSet {
|
pub struct MissionScriptVarSet {
|
||||||
/// Resolved `.var` path selected by the original fallback order.
|
/// Resolved `.var` path selected by the original fallback order.
|
||||||
@@ -267,6 +270,21 @@ pub struct MissionScriptVarSet {
|
|||||||
pub declarations: VarSet,
|
pub declarations: VarSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One resolved, corpus-proven `Init` result for a mission clan.
|
||||||
|
///
|
||||||
|
/// This is intentionally limited to `Handler(19)`: an `Init` event's other
|
||||||
|
/// selectors are retained in [`MissionScriptBundle::package`] but are not
|
||||||
|
/// guessed as executable behavior.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MissionScriptInitState {
|
||||||
|
/// TMA clan index that owns the `SuperAI` instance and varset.
|
||||||
|
pub clan_index: usize,
|
||||||
|
/// Raw event word preserved from the compiled `Init` event.
|
||||||
|
pub event_word: u32,
|
||||||
|
/// Exact three DWORD writes made by each proven `Handler(19)` instruction.
|
||||||
|
pub writes: Vec<Handler19DwordWrite>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
struct MissionLoadOptions {
|
struct MissionLoadOptions {
|
||||||
fail_after_registered_objects: Option<usize>,
|
fail_after_registered_objects: Option<usize>,
|
||||||
@@ -277,6 +295,7 @@ struct MissionLoadOptions {
|
|||||||
struct LoadedMissionScripts {
|
struct LoadedMissionScripts {
|
||||||
bundles: Vec<MissionScriptBundle>,
|
bundles: Vec<MissionScriptBundle>,
|
||||||
varset: Option<MissionScriptVarSet>,
|
varset: Option<MissionScriptVarSet>,
|
||||||
|
init_states: Vec<MissionScriptInitState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects how much of the resolved mission asset graph to prepare.
|
/// Selects how much of the resolved mission asset graph to prepare.
|
||||||
@@ -366,6 +385,8 @@ pub struct LoadedMission {
|
|||||||
pub script_event_count: usize,
|
pub script_event_count: usize,
|
||||||
/// Numeric script defaults loaded through the original bundle/fallback rule.
|
/// Numeric script defaults loaded through the original bundle/fallback rule.
|
||||||
pub script_varset_declaration_count: usize,
|
pub script_varset_declaration_count: usize,
|
||||||
|
/// `Handler(19)` Init results resolved for mission clans.
|
||||||
|
pub script_init_state_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frame result.
|
/// Frame result.
|
||||||
@@ -404,6 +425,7 @@ struct LoadedMissionState {
|
|||||||
object_drafts: Vec<MissionObjectDraft>,
|
object_drafts: Vec<MissionObjectDraft>,
|
||||||
script_bundles: Vec<MissionScriptBundle>,
|
script_bundles: Vec<MissionScriptBundle>,
|
||||||
script_varset: Option<MissionScriptVarSet>,
|
script_varset: Option<MissionScriptVarSet>,
|
||||||
|
script_init_states: Vec<MissionScriptInitState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Engine error.
|
/// Engine error.
|
||||||
@@ -441,6 +463,13 @@ pub enum EngineError {
|
|||||||
/// Parse diagnostic.
|
/// Parse diagnostic.
|
||||||
message: String,
|
message: String,
|
||||||
},
|
},
|
||||||
|
/// A recovered `Handler(19)` Init input or target was not representable.
|
||||||
|
ScriptInit {
|
||||||
|
/// Owning TMA clan index.
|
||||||
|
clan_index: usize,
|
||||||
|
/// Proven-contract diagnostic.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
/// `NRes` decode error.
|
/// `NRes` decode error.
|
||||||
Nres {
|
Nres {
|
||||||
/// Resource path.
|
/// Resource path.
|
||||||
@@ -577,6 +606,10 @@ impl std::fmt::Display for EngineError {
|
|||||||
Self::Vfs { path, source } => write!(f, "{path}: {source}"),
|
Self::Vfs { path, source } => write!(f, "{path}: {source}"),
|
||||||
Self::Script { path, message } => write!(f, "{path}: script decode failed: {message}"),
|
Self::Script { path, message } => write!(f, "{path}: script decode failed: {message}"),
|
||||||
Self::VarSet { path, message } => write!(f, "{path}: varset decode failed: {message}"),
|
Self::VarSet { path, message } => write!(f, "{path}: varset decode failed: {message}"),
|
||||||
|
Self::ScriptInit {
|
||||||
|
clan_index,
|
||||||
|
message,
|
||||||
|
} => write!(f, "clan {clan_index}: script Init failed: {message}"),
|
||||||
Self::Nres { path, source } => write!(f, "{path}: {source}"),
|
Self::Nres { path, source } => write!(f, "{path}: {source}"),
|
||||||
Self::Mission { path, source } => write!(f, "{path}: {source}"),
|
Self::Mission { path, source } => write!(f, "{path}: {source}"),
|
||||||
Self::TerrainFormat { path, source } => write!(f, "{path}: {source}"),
|
Self::TerrainFormat { path, source } => write!(f, "{path}: {source}"),
|
||||||
@@ -619,6 +652,7 @@ impl std::error::Error for EngineError {
|
|||||||
Self::MissingVfs
|
Self::MissingVfs
|
||||||
| Self::Script { .. }
|
| Self::Script { .. }
|
||||||
| Self::VarSet { .. }
|
| Self::VarSet { .. }
|
||||||
|
| Self::ScriptInit { .. }
|
||||||
| Self::Movement(_)
|
| Self::Movement(_)
|
||||||
| Self::PrototypeGraph { .. }
|
| Self::PrototypeGraph { .. }
|
||||||
| Self::SchedulerPhaseOrder { .. }
|
| Self::SchedulerPhaseOrder { .. }
|
||||||
@@ -1064,6 +1098,7 @@ fn load_mission_with_options_and_progress(
|
|||||||
.varset
|
.varset
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(0, |varset| varset.declarations.declarations.len()),
|
.map_or(0, |varset| varset.declarations.declarations.len()),
|
||||||
|
script_init_state_count: loaded_scripts.init_states.len(),
|
||||||
};
|
};
|
||||||
|
|
||||||
engine.world = new_runtime_world;
|
engine.world = new_runtime_world;
|
||||||
@@ -1079,6 +1114,7 @@ fn load_mission_with_options_and_progress(
|
|||||||
object_drafts,
|
object_drafts,
|
||||||
script_bundles: loaded_scripts.bundles,
|
script_bundles: loaded_scripts.bundles,
|
||||||
script_varset: loaded_scripts.varset,
|
script_varset: loaded_scripts.varset,
|
||||||
|
script_init_states: loaded_scripts.init_states,
|
||||||
});
|
});
|
||||||
Ok((summary, trace))
|
Ok((summary, trace))
|
||||||
}
|
}
|
||||||
@@ -1177,6 +1213,18 @@ pub fn loaded_mission_script_varset(engine: &Engine) -> Option<&MissionScriptVar
|
|||||||
.and_then(|state| state.script_varset.as_ref())
|
.and_then(|state| state.script_varset.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the proven script `Init` writes resolved for the loaded mission.
|
||||||
|
///
|
||||||
|
/// The list has one entry for each clan whose `Init` event contains recovered
|
||||||
|
/// `Handler(19)` instructions. It is not a general script event executor.
|
||||||
|
#[must_use]
|
||||||
|
pub fn loaded_mission_script_init_states(engine: &Engine) -> Option<&[MissionScriptInitState]> {
|
||||||
|
engine
|
||||||
|
.loaded
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| state.script_init_states.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns terrain runtime data for the loaded mission.
|
/// Returns terrain runtime data for the loaded mission.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn loaded_terrain(engine: &Engine) -> Option<&TerrainWorld> {
|
pub fn loaded_terrain(engine: &Engine) -> Option<&TerrainWorld> {
|
||||||
@@ -1341,7 +1389,99 @@ fn load_mission_scripts(
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|base_path| load_script_varset(vfs, base_path))
|
.map(|base_path| load_script_varset(vfs, base_path))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
Ok(LoadedMissionScripts { bundles, varset })
|
let init_states = match &varset {
|
||||||
|
Some(varset) => resolve_handler19_init_states(
|
||||||
|
&mission
|
||||||
|
.clans
|
||||||
|
.iter()
|
||||||
|
.map(|clan| clan.anchor)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
&bundles,
|
||||||
|
varset,
|
||||||
|
)?,
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
Ok(LoadedMissionScripts {
|
||||||
|
bundles,
|
||||||
|
varset,
|
||||||
|
init_states,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_handler19_init_states(
|
||||||
|
clan_anchors: &[[f32; 2]],
|
||||||
|
bundles: &[MissionScriptBundle],
|
||||||
|
varset: &MissionScriptVarSet,
|
||||||
|
) -> Result<Vec<MissionScriptInitState>, EngineError> {
|
||||||
|
let mut states = Vec::new();
|
||||||
|
for bundle in bundles {
|
||||||
|
let Some(anchor) = clan_anchors.get(bundle.clan_index) else {
|
||||||
|
return Err(EngineError::ScriptInit {
|
||||||
|
clan_index: bundle.clan_index,
|
||||||
|
message: "script bundle clan index is outside the decoded mission".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let input = Handler19InitInput {
|
||||||
|
first_x87_word: handler19_x87_truncated_u32(anchor[0], bundle.clan_index, "x")?,
|
||||||
|
second_x87_word: handler19_x87_truncated_u32(anchor[1], bundle.clan_index, "y")?,
|
||||||
|
third_word: u32::try_from(bundle.clan_index).map_err(|_| EngineError::ScriptInit {
|
||||||
|
clan_index: bundle.clan_index,
|
||||||
|
message: "clan index does not fit the recovered DWORD ClanID field".to_string(),
|
||||||
|
})?,
|
||||||
|
};
|
||||||
|
for event in &bundle.package.events {
|
||||||
|
if event.name_raw.as_slice() != b"Init\0" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let writes: Vec<Handler19DwordWrite> = event
|
||||||
|
.instructions
|
||||||
|
.iter()
|
||||||
|
.filter(|instruction| {
|
||||||
|
instruction.dispatch_selector() == ScriptDispatchSelector::Handler(19)
|
||||||
|
})
|
||||||
|
.map(|instruction| {
|
||||||
|
varset
|
||||||
|
.declarations
|
||||||
|
.resolve_handler19(instruction, input)
|
||||||
|
.map_err(|source| EngineError::ScriptInit {
|
||||||
|
clan_index: bundle.clan_index,
|
||||||
|
message: source.to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect();
|
||||||
|
if !writes.is_empty() {
|
||||||
|
states.push(MissionScriptInitState {
|
||||||
|
clan_index: bundle.clan_index,
|
||||||
|
event_word: event.event_word,
|
||||||
|
writes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(states)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handler19_x87_truncated_u32(
|
||||||
|
value: f32,
|
||||||
|
clan_index: usize,
|
||||||
|
axis: &'static str,
|
||||||
|
) -> Result<u32, EngineError> {
|
||||||
|
if !value.is_finite() || !(0.0..=10_000.0).contains(&value) {
|
||||||
|
return Err(EngineError::ScriptInit {
|
||||||
|
clan_index,
|
||||||
|
message: format!(
|
||||||
|
"ClanBase{axis}={value:?} is outside the recovered CreateSuperAI base range"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ai.dll's __ftol saves the x87 control word, sets rounding-control bits
|
||||||
|
// to truncate, performs `fistp qword`, and restores the control word.
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
let word = value.trunc() as u32;
|
||||||
|
Ok(word)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_script_varset(
|
fn load_script_varset(
|
||||||
@@ -1488,6 +1628,70 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handler_nineteen_init_executes_from_clan_anchor_for_each_bundle() {
|
||||||
|
let declarations = fparkan_script::parse_varset(
|
||||||
|
b"VAR( DWORD, ClanBaseX, 950)\nVAR( DWORD, ClanBaseY, 1000)\nVAR( DWORD, ClanID, 0)\n",
|
||||||
|
)
|
||||||
|
.expect("varset");
|
||||||
|
let varset = MissionScriptVarSet {
|
||||||
|
path: FALLBACK_SCRIPT_VARSET_PATH.to_string(),
|
||||||
|
used_fallback: true,
|
||||||
|
declarations,
|
||||||
|
};
|
||||||
|
let bundles = [MissionScriptBundle {
|
||||||
|
clan_index: 1,
|
||||||
|
base_path: "MISSIONS/SCRIPTS/default".to_string(),
|
||||||
|
script_path: "MISSIONS/SCRIPTS/default.scr".to_string(),
|
||||||
|
package: ScriptPackage {
|
||||||
|
opcode_handler_count: 73,
|
||||||
|
events: vec![fparkan_script::ScriptEvent {
|
||||||
|
name_len: 4,
|
||||||
|
name_raw: b"Init\0".to_vec(),
|
||||||
|
event_word: 0x1234_5678,
|
||||||
|
instructions: vec![fparkan_script::ScriptInstruction {
|
||||||
|
header_words: [19, 0, 0, 0, 0, 3, 0],
|
||||||
|
references: vec![0, 1, 2],
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
trailing_bytes: Vec::new(),
|
||||||
|
raw: Arc::from(&b""[..]),
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
let states =
|
||||||
|
resolve_handler19_init_states(&[[500.0, 752.0], [728.0, 449.0]], &bundles, &varset)
|
||||||
|
.expect("exact Init execution");
|
||||||
|
assert_eq!(
|
||||||
|
states,
|
||||||
|
vec![MissionScriptInitState {
|
||||||
|
clan_index: 1,
|
||||||
|
event_word: 0x1234_5678,
|
||||||
|
writes: vec![
|
||||||
|
Handler19DwordWrite {
|
||||||
|
index: 0,
|
||||||
|
value: 728
|
||||||
|
},
|
||||||
|
Handler19DwordWrite {
|
||||||
|
index: 1,
|
||||||
|
value: 449
|
||||||
|
},
|
||||||
|
Handler19DwordWrite { index: 2, value: 1 },
|
||||||
|
],
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handler_nineteen_init_rejects_anchor_outside_recovered_base_range() {
|
||||||
|
let error = handler19_x87_truncated_u32(-0.5, 0, "x").expect_err("negative anchor");
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
EngineError::ScriptInit { clan_index: 0, message }
|
||||||
|
if message.contains("outside the recovered CreateSuperAI base range")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn headless_scheduler_trace_skips_presentation_phases() {
|
fn headless_scheduler_trace_skips_presentation_phases() {
|
||||||
let mut engine = create(
|
let mut engine = create(
|
||||||
|
|||||||
@@ -190,6 +190,26 @@ a license to replace the first two conversions with Rust float casts: their
|
|||||||
rounding behavior remains an x87 compatibility boundary until it has captured
|
rounding behavior remains an x87 compatibility boundary until it has captured
|
||||||
test vectors.
|
test vectors.
|
||||||
|
|
||||||
|
The missing source-field provenance is now constrained by the public creation
|
||||||
|
boundary. `CreateSuperAI` at `ai.dll` VA `0x1000f710` allocates `0x8b0` bytes
|
||||||
|
and calls constructor `0x10001000` with its first eight arguments. That
|
||||||
|
constructor calls `0x10006340(this + 0x7c, clan_id, base_x, base_y)`: these are
|
||||||
|
the fields later read by `Handler(19)`. `base_x` and `base_y` are unsigned and
|
||||||
|
the constructor rejects values greater than `10000`. The actual `__ftol` helper
|
||||||
|
at `0x1001df70` saves the x87 control word, sets its rounding-control bits to
|
||||||
|
truncate, executes `fistp qword`, then restores the control word.
|
||||||
|
|
||||||
|
Consequently the runtime resolves and retains this one proved vertical slice
|
||||||
|
during mission loading: each selected clan's TMA anchor is accepted only in the recovered
|
||||||
|
`0..=10000` base range, truncated through the recovered x87 rule, and paired
|
||||||
|
with its zero-based clan index. For every `Init` instruction whose selector is
|
||||||
|
`Handler(19)`, `VarSet::resolve_handler19` produces the three per-clan DWORD
|
||||||
|
immutable `MissionScriptInitState` writes. Other Init selectors and all other events remain decoded but
|
||||||
|
unexecuted. AutoDemo validates the path end-to-end: its non-integral first
|
||||||
|
anchor (`500.2857`) yields captured `ClanBaseX=500`, and the live GOG process
|
||||||
|
contains two initialized SuperAI entries `(500, 752, 0)` and `(728, 449, 1)`;
|
||||||
|
the Rust loader reports `script_init_states=2`.
|
||||||
|
|
||||||
`GetSuperAI` returns element `n` of the 64-pointer global table at preferred
|
`GetSuperAI` returns element `n` of the 64-pointer global table at preferred
|
||||||
`ai.dll + 0x55398` for `n <= 63`. The read-only
|
`ai.dll + 0x55398` for `n <= 63`. The read-only
|
||||||
`tools/capture-ai-init.ps1` probe observed the running GOG AutoDemo values
|
`tools/capture-ai-init.ps1` probe observed the running GOG AutoDemo values
|
||||||
@@ -199,11 +219,14 @@ profile.
|
|||||||
|
|
||||||
The Rust reader exposes `VarSet::resolve_handler19`. It accepts the already
|
The Rust reader exposes `VarSet::resolve_handler19`. It accepts the already
|
||||||
converted first two words and the third raw word, produces three typed writes,
|
converted first two words and the third raw word, produces three typed writes,
|
||||||
and rejects missing, out-of-range, or non-`DWORD` targets. It deliberately does
|
and rejects missing, out-of-range, or non-`DWORD` targets. The runtime only
|
||||||
not guess how the mission creates the three source fields or execute a script
|
binds it to the proven creation/anchor path above; it does not guess the
|
||||||
event yet. The associated Ghidra scripts are
|
remaining script event semantics. The associated Ghidra scripts are
|
||||||
`ExportAiVmHandler19.java`, `ExportAiVmHandler19Setter.java`,
|
`ExportAiVmHandler19.java`, `ExportAiVmHandler19Setter.java`,
|
||||||
`ExportAiVmHandler19SetterCallee.java`, and `ExportAiGetSuperAi.java`.
|
`ExportAiVmHandler19SetterCallee.java`, and `ExportAiGetSuperAi.java`.
|
||||||
|
The creation and conversion boundaries are reproducible with
|
||||||
|
`ExportAiCreateSuperAi.java`, `ExportAiSuperAiConstructor.java`, and
|
||||||
|
`ExportAiFtol.java`.
|
||||||
|
|
||||||
### Handler(8): problem-record state write
|
### Handler(8): problem-record state write
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Emits the GOG CreateSuperAI export to recover the mission-to-SuperAI
|
||||||
|
// initialization boundary. Run headless; the original PE remains read only.
|
||||||
|
import ghidra.app.decompiler.DecompInterface;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.program.model.address.Address;
|
||||||
|
import ghidra.program.model.listing.Function;
|
||||||
|
|
||||||
|
public class ExportAiCreateSuperAi extends GhidraScript {
|
||||||
|
private static final long ADDRESS = 0x1000f710L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() throws Exception {
|
||||||
|
Address address = currentProgram.getAddressFactory().getDefaultAddressSpace()
|
||||||
|
.getAddress(ADDRESS);
|
||||||
|
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||||
|
println("===== AI CreateSuperAI =====");
|
||||||
|
if (function == null) { println("missing"); return; }
|
||||||
|
DecompInterface decompiler = new DecompInterface();
|
||||||
|
decompiler.openProgram(currentProgram);
|
||||||
|
println(decompiler.decompileFunction(function, 120, monitor).getDecompiledFunction().getC());
|
||||||
|
decompiler.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Emits the x87-to-integer helper called by Handler(19). Run headless; the
|
||||||
|
// original PE remains read only.
|
||||||
|
import ghidra.app.decompiler.DecompInterface;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.program.model.address.Address;
|
||||||
|
import ghidra.program.model.listing.Function;
|
||||||
|
|
||||||
|
public class ExportAiFtol extends GhidraScript {
|
||||||
|
private static final long ADDRESS = 0x1001df70L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() throws Exception {
|
||||||
|
Address address = currentProgram.getAddressFactory().getDefaultAddressSpace()
|
||||||
|
.getAddress(ADDRESS);
|
||||||
|
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||||
|
println("===== AI x87 __ftol helper =====");
|
||||||
|
if (function == null) { println("missing"); return; }
|
||||||
|
DecompInterface decompiler = new DecompInterface();
|
||||||
|
decompiler.openProgram(currentProgram);
|
||||||
|
println(decompiler.decompileFunction(function, 60, monitor).getDecompiledFunction().getC());
|
||||||
|
decompiler.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Emits the SuperAI constructor called by CreateSuperAI. Run headless; the
|
||||||
|
// original PE remains read only.
|
||||||
|
import ghidra.app.decompiler.DecompInterface;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.program.model.address.Address;
|
||||||
|
import ghidra.program.model.listing.Function;
|
||||||
|
|
||||||
|
public class ExportAiSuperAiConstructor extends GhidraScript {
|
||||||
|
private static final long ADDRESS = 0x10001000L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() throws Exception {
|
||||||
|
Address address = currentProgram.getAddressFactory().getDefaultAddressSpace()
|
||||||
|
.getAddress(ADDRESS);
|
||||||
|
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||||
|
println("===== AI SuperAI constructor =====");
|
||||||
|
if (function == null) { println("missing"); return; }
|
||||||
|
DecompInterface decompiler = new DecompInterface();
|
||||||
|
decompiler.openProgram(currentProgram);
|
||||||
|
println(decompiler.decompileFunction(function, 120, monitor).getDecompiledFunction().getC());
|
||||||
|
decompiler.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user