feat(runtime): load mission script bundles
This commit is contained in:
Generated
+1
@@ -608,6 +608,7 @@ dependencies = [
|
|||||||
"fparkan-prototype",
|
"fparkan-prototype",
|
||||||
"fparkan-render",
|
"fparkan-render",
|
||||||
"fparkan-resource",
|
"fparkan-resource",
|
||||||
|
"fparkan-script",
|
||||||
"fparkan-terrain",
|
"fparkan-terrain",
|
||||||
"fparkan-vfs",
|
"fparkan-vfs",
|
||||||
"fparkan-world",
|
"fparkan-world",
|
||||||
|
|||||||
@@ -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={} graph_failures={}",
|
"mission objects={} areals={} surfaces={} graph_roots={} components={} wear={} material_slots={} textures={} lightmaps={} scripts={} script_events={} graph_failures={}",
|
||||||
loaded.object_count,
|
loaded.object_count,
|
||||||
loaded.areal_count,
|
loaded.areal_count,
|
||||||
loaded.surface_count,
|
loaded.surface_count,
|
||||||
@@ -66,6 +66,8 @@ fn run() -> Result<(), String> {
|
|||||||
loaded.graph_material_resolved_count,
|
loaded.graph_material_resolved_count,
|
||||||
loaded.graph_texture_resolved_count,
|
loaded.graph_texture_resolved_count,
|
||||||
loaded.graph_lightmap_resolved_count,
|
loaded.graph_lightmap_resolved_count,
|
||||||
|
loaded.script_bundle_count,
|
||||||
|
loaded.script_event_count,
|
||||||
loaded.graph_failure_count
|
loaded.graph_failure_count
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use fparkan_material::{
|
|||||||
decode_wear, resolve_material, Mat0Document, MaterialError, ResolvedMaterial, WearTable,
|
decode_wear, resolve_material, Mat0Document, MaterialError, ResolvedMaterial, WearTable,
|
||||||
MAT0_KIND, WEAR_KIND,
|
MAT0_KIND, WEAR_KIND,
|
||||||
};
|
};
|
||||||
use fparkan_mission_format::{decode_tma, decode_tma_land_path};
|
use fparkan_mission_format::{decode_tma, decode_tma_land_path, ClanBody};
|
||||||
pub use fparkan_mission_format::{LpString, MissionDocument, MissionError, TmaProfile};
|
pub use fparkan_mission_format::{LpString, MissionDocument, MissionError, TmaProfile};
|
||||||
use fparkan_msh::{decode_msh, validate_msh, ModelAsset, MshError};
|
use fparkan_msh::{decode_msh, validate_msh, ModelAsset, MshError};
|
||||||
use fparkan_nres::{decode as decode_nres, ReadProfile};
|
use fparkan_nres::{decode as decode_nres, ReadProfile};
|
||||||
@@ -63,6 +63,17 @@ pub struct MissionTerrainPaths {
|
|||||||
pub land_map: NormalizedPath,
|
pub land_map: NormalizedPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raw compiled-script base selected by one TMA clan.
|
||||||
|
///
|
||||||
|
/// The path remains raw legacy bytes until the runtime applies its path policy.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MissionScriptBundleBase {
|
||||||
|
/// Index in the mission clan array.
|
||||||
|
pub clan_index: usize,
|
||||||
|
/// First clan resource path, without a required `.scr` suffix.
|
||||||
|
pub path_raw: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Terrain loading errors that include runtime world construction failures.
|
/// Terrain loading errors that include runtime world construction failures.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum TerrainPreparationError {
|
pub enum TerrainPreparationError {
|
||||||
@@ -121,6 +132,29 @@ pub fn decode_mission_land_path(
|
|||||||
decode_tma_land_path(bytes, profile)
|
decode_tma_land_path(bytes, profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns non-empty script/formula bases selected by TMA clans.
|
||||||
|
///
|
||||||
|
/// The first resource has the same position in the standard and spatial clan
|
||||||
|
/// layouts. Its consumer-specific meaning is intentionally left to runtime.
|
||||||
|
#[must_use]
|
||||||
|
pub fn mission_script_bundle_bases(mission: &MissionDocument) -> Vec<MissionScriptBundleBase> {
|
||||||
|
mission
|
||||||
|
.clans
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(clan_index, clan)| {
|
||||||
|
let path_raw = match &clan.body {
|
||||||
|
ClanBody::Standard { first_resource, .. } => first_resource.path.raw.clone(),
|
||||||
|
ClanBody::Spatial { first_resource, .. } => first_resource.raw.clone(),
|
||||||
|
};
|
||||||
|
(!path_raw.is_empty()).then_some(MissionScriptBundleBase {
|
||||||
|
clan_index,
|
||||||
|
path_raw,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds canonical mission terrain paths from the mission `Land` reference.
|
/// Builds canonical mission terrain paths from the mission `Land` reference.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ fparkan-prototype = { path = "../fparkan-prototype", version = "0.1.0" }
|
|||||||
fparkan-render = { path = "../fparkan-render", version = "0.1.0" }
|
fparkan-render = { path = "../fparkan-render", version = "0.1.0" }
|
||||||
fparkan-terrain = { path = "../fparkan-terrain", version = "0.1.0" }
|
fparkan-terrain = { path = "../fparkan-terrain", version = "0.1.0" }
|
||||||
fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
|
fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
|
||||||
|
fparkan-script = { path = "../fparkan-script", version = "0.1.0" }
|
||||||
fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
|
fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
|
||||||
fparkan-world = { path = "../fparkan-world", version = "0.1.0" }
|
fparkan-world = { path = "../fparkan-world", version = "0.1.0" }
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,10 @@
|
|||||||
use fparkan_assets::{
|
use fparkan_assets::{
|
||||||
decode_mission_land_path, decode_mission_payload, decode_nres_payload,
|
decode_mission_land_path, decode_mission_payload, decode_nres_payload,
|
||||||
derive_mission_land_paths, extend_graph_report_with_visual_dependencies_with_progress,
|
derive_mission_land_paths, extend_graph_report_with_visual_dependencies_with_progress,
|
||||||
prepare_terrain_world, AssetError as AssetPreparationError, AssetId, AssetManager,
|
mission_script_bundle_bases, prepare_terrain_world, AssetError as AssetPreparationError,
|
||||||
AssetPreparationPhase, BuildCategory, MissionAssetPlan, MissionDocument, MissionError,
|
AssetId, AssetManager, AssetPreparationPhase, BuildCategory, MissionAssetPlan, MissionDocument,
|
||||||
MissionTerrainPaths, NresError, PreparedVisual, TerrainFormatError, TerrainPreparationError,
|
MissionError, MissionTerrainPaths, NresError, PreparedVisual, TerrainFormatError,
|
||||||
TerrainWorld, TmaProfile, VisualDependencyPhase,
|
TerrainPreparationError, TerrainWorld, TmaProfile, VisualDependencyPhase,
|
||||||
};
|
};
|
||||||
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
|
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
|
||||||
use fparkan_prototype::{
|
use fparkan_prototype::{
|
||||||
@@ -34,6 +34,7 @@ use fparkan_prototype::{
|
|||||||
UnitComponentRecord,
|
UnitComponentRecord,
|
||||||
};
|
};
|
||||||
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
|
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
|
||||||
|
use fparkan_script::ScriptPackage;
|
||||||
use fparkan_terrain::SurfaceQuery;
|
use fparkan_terrain::SurfaceQuery;
|
||||||
use fparkan_vfs::{Vfs, VfsError};
|
use fparkan_vfs::{Vfs, VfsError};
|
||||||
use fparkan_world::{
|
use fparkan_world::{
|
||||||
@@ -237,6 +238,19 @@ pub struct MissionObjectDraft {
|
|||||||
pub unit_components: Vec<UnitComponentRecord>,
|
pub unit_components: Vec<UnitComponentRecord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A compiled script bundle selected by one mission clan.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MissionScriptBundle {
|
||||||
|
/// TMA clan index selecting this bundle.
|
||||||
|
pub clan_index: usize,
|
||||||
|
/// Lossy display form of the raw bundle base path from the clan resource.
|
||||||
|
pub base_path: String,
|
||||||
|
/// Resolved compiled `.scr` path.
|
||||||
|
pub script_path: String,
|
||||||
|
/// Losslessly decoded compiled package; not yet executed by runtime.
|
||||||
|
pub package: ScriptPackage,
|
||||||
|
}
|
||||||
|
|
||||||
#[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>,
|
||||||
@@ -324,6 +338,10 @@ pub struct LoadedMission {
|
|||||||
pub asset_texture_count: usize,
|
pub asset_texture_count: usize,
|
||||||
/// Mission asset plan lightmap count after dependency preparation.
|
/// Mission asset plan lightmap count after dependency preparation.
|
||||||
pub asset_lightmap_count: usize,
|
pub asset_lightmap_count: usize,
|
||||||
|
/// Script bundles selected by mission clans.
|
||||||
|
pub script_bundle_count: usize,
|
||||||
|
/// Total named events in loaded script packages.
|
||||||
|
pub script_event_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frame result.
|
/// Frame result.
|
||||||
@@ -360,6 +378,7 @@ struct LoadedMissionState {
|
|||||||
mission_assets: MissionAssets,
|
mission_assets: MissionAssets,
|
||||||
asset_plan: MissionAssetPlan,
|
asset_plan: MissionAssetPlan,
|
||||||
object_drafts: Vec<MissionObjectDraft>,
|
object_drafts: Vec<MissionObjectDraft>,
|
||||||
|
script_bundles: Vec<MissionScriptBundle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Engine error.
|
/// Engine error.
|
||||||
@@ -383,6 +402,13 @@ pub enum EngineError {
|
|||||||
/// Source error.
|
/// Source error.
|
||||||
source: VfsError,
|
source: VfsError,
|
||||||
},
|
},
|
||||||
|
/// Compiled script package could not be decoded.
|
||||||
|
Script {
|
||||||
|
/// Script path.
|
||||||
|
path: String,
|
||||||
|
/// Decode diagnostic.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
/// `NRes` decode error.
|
/// `NRes` decode error.
|
||||||
Nres {
|
Nres {
|
||||||
/// Resource path.
|
/// Resource path.
|
||||||
@@ -517,6 +543,7 @@ impl std::fmt::Display for EngineError {
|
|||||||
write!(f, "invalid {role} path '{value}': {source}")
|
write!(f, "invalid {role} path '{value}': {source}")
|
||||||
}
|
}
|
||||||
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::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}"),
|
||||||
@@ -557,6 +584,7 @@ impl std::error::Error for EngineError {
|
|||||||
Self::World(source) => Some(source),
|
Self::World(source) => Some(source),
|
||||||
Self::AssetPreparation { source, .. } => Some(source),
|
Self::AssetPreparation { source, .. } => Some(source),
|
||||||
Self::MissingVfs
|
Self::MissingVfs
|
||||||
|
| Self::Script { .. }
|
||||||
| Self::Movement(_)
|
| Self::Movement(_)
|
||||||
| Self::PrototypeGraph { .. }
|
| Self::PrototypeGraph { .. }
|
||||||
| Self::SchedulerPhaseOrder { .. }
|
| Self::SchedulerPhaseOrder { .. }
|
||||||
@@ -775,6 +803,7 @@ fn load_mission_with_options_and_progress(
|
|||||||
source,
|
source,
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
let script_bundles = load_mission_script_bundles(&vfs, &mission)?;
|
||||||
trace.transforms = mission
|
trace.transforms = mission
|
||||||
.objects
|
.objects
|
||||||
.iter()
|
.iter()
|
||||||
@@ -991,6 +1020,11 @@ fn load_mission_with_options_and_progress(
|
|||||||
asset_material_count: mission_asset_plan.material_count,
|
asset_material_count: mission_asset_plan.material_count,
|
||||||
asset_texture_count: mission_asset_plan.texture_count,
|
asset_texture_count: mission_asset_plan.texture_count,
|
||||||
asset_lightmap_count: mission_asset_plan.lightmap_count,
|
asset_lightmap_count: mission_asset_plan.lightmap_count,
|
||||||
|
script_bundle_count: script_bundles.len(),
|
||||||
|
script_event_count: script_bundles
|
||||||
|
.iter()
|
||||||
|
.map(|bundle| bundle.package.events.len())
|
||||||
|
.sum(),
|
||||||
};
|
};
|
||||||
|
|
||||||
engine.world = new_runtime_world;
|
engine.world = new_runtime_world;
|
||||||
@@ -1004,6 +1038,7 @@ fn load_mission_with_options_and_progress(
|
|||||||
mission_assets,
|
mission_assets,
|
||||||
asset_plan: mission_asset_plan,
|
asset_plan: mission_asset_plan,
|
||||||
object_drafts,
|
object_drafts,
|
||||||
|
script_bundles,
|
||||||
});
|
});
|
||||||
Ok((summary, trace))
|
Ok((summary, trace))
|
||||||
}
|
}
|
||||||
@@ -1084,6 +1119,15 @@ pub fn loaded_mission_document(engine: &Engine) -> Option<&MissionDocument> {
|
|||||||
engine.loaded.as_ref().map(|state| &state.mission)
|
engine.loaded.as_ref().map(|state| &state.mission)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns compiled script bundles selected by TMA clan resources.
|
||||||
|
#[must_use]
|
||||||
|
pub fn loaded_mission_script_bundles(engine: &Engine) -> Option<&[MissionScriptBundle]> {
|
||||||
|
engine
|
||||||
|
.loaded
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| state.script_bundles.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> {
|
||||||
@@ -1214,6 +1258,35 @@ fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPa
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_mission_script_bundles(
|
||||||
|
vfs: &Arc<dyn Vfs>,
|
||||||
|
mission: &MissionDocument,
|
||||||
|
) -> Result<Vec<MissionScriptBundle>, EngineError> {
|
||||||
|
let script_bases = mission_script_bundle_bases(mission);
|
||||||
|
let mut bundles = Vec::with_capacity(script_bases.len());
|
||||||
|
for script_base in script_bases {
|
||||||
|
let base_path = String::from_utf8_lossy(&script_base.path_raw).into_owned();
|
||||||
|
let script_path = if base_path.to_ascii_lowercase().ends_with(".scr") {
|
||||||
|
base_path.clone()
|
||||||
|
} else {
|
||||||
|
format!("{base_path}.scr")
|
||||||
|
};
|
||||||
|
let normalized = normalize_engine_path("mission script", &script_path)?;
|
||||||
|
let bytes = read_vfs(vfs, &normalized)?;
|
||||||
|
let package = fparkan_script::decode(&bytes).map_err(|source| EngineError::Script {
|
||||||
|
path: normalized.as_str().to_string(),
|
||||||
|
message: source.to_string(),
|
||||||
|
})?;
|
||||||
|
bundles.push(MissionScriptBundle {
|
||||||
|
clan_index: script_base.clan_index,
|
||||||
|
base_path,
|
||||||
|
script_path: normalized.as_str().to_string(),
|
||||||
|
package,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(bundles)
|
||||||
|
}
|
||||||
|
|
||||||
fn read_vfs(vfs: &Arc<dyn Vfs>, path: &NormalizedPath) -> Result<Arc<[u8]>, EngineError> {
|
fn read_vfs(vfs: &Arc<dyn Vfs>, path: &NormalizedPath) -> Result<Arc<[u8]>, EngineError> {
|
||||||
vfs.read(path).map_err(|source| EngineError::Vfs {
|
vfs.read(path).map_err(|source| EngineError::Vfs {
|
||||||
path: path.as_str().to_string(),
|
path: path.as_str().to_string(),
|
||||||
|
|||||||
@@ -73,6 +73,14 @@ raw seven slots, resolved variable values и вход/выход вызова `0
|
|||||||
controlled mission. До него compatibility VM возвращает явный unsupported
|
controlled mission. До него compatibility VM возвращает явный unsupported
|
||||||
result, а не «примерный» game command.
|
result, а не «примерный» game command.
|
||||||
|
|
||||||
|
На границе mission runtime выбранный TMA clan `first_resource` теперь
|
||||||
|
материализуется как отдельный `MissionScriptBundle`: loader нормализует
|
||||||
|
`<base>.scr`, декодирует его тем же bounded reader-ом и публикует immutable
|
||||||
|
package вместе с clan provenance. Headless report выводит число таких packages
|
||||||
|
и их named events. Это именно wiring входных данных, не VM execution: Init и
|
||||||
|
остальные events пока не dispatch-ятся, а ошибка чтения сохраняет
|
||||||
|
transactional rollback mission loader-а.
|
||||||
|
|
||||||
TMA properties остаются four raw `u32` words плюс имя, пока consumer/schema не
|
TMA properties остаются four raw `u32` words плюс имя, пока consumer/schema не
|
||||||
задаст тип (integer/float bits/ObjectId/enum/fixed-point/index). В том числе
|
задаст тип (integer/float bits/ObjectId/enum/fixed-point/index). В том числе
|
||||||
сохраняются `NOT USED`; corpus подтверждает `Invulnerability`, life state,
|
сохраняются `NOT USED`; corpus подтверждает `Invulnerability`, life state,
|
||||||
|
|||||||
@@ -135,6 +135,14 @@ repeat relation_count:
|
|||||||
TRF или пустой ресурс. Tags различаются между кланами и должны сохраняться как
|
TRF или пустой ресурс. Tags различаются между кланами и должны сохраняться как
|
||||||
raw-поля, пока их потребительская семантика не закрыта.
|
raw-поля, пока их потребительская семантика не закрыта.
|
||||||
|
|
||||||
|
Runtime уже использует первую строку как доказанный путь выбора пакета: к ней
|
||||||
|
добавляется `.scr`, если расширение отсутствует, и полученный compiled package
|
||||||
|
декодируется losslessly до регистрации мира. Для каждого клана сохраняются
|
||||||
|
индекс, исходный base path, нормализованный `.scr` path и пакет событий. Это
|
||||||
|
не запускает opcode и не приписывает ему gameplay-эффект, но делает сценарный
|
||||||
|
input частью транзакции загрузки: missing/invalid package отменяет mission
|
||||||
|
load до публикации частичного world state.
|
||||||
|
|
||||||
Mode `0` имеет отдельный count-driven layout:
|
Mode `0` имеет отдельный count-driven layout:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
Reference in New Issue
Block a user