aboutsummaryrefslogtreecommitdiff
path: root/apps/fparkan-game
diff options
context:
space:
mode:
Diffstat (limited to 'apps/fparkan-game')
-rw-r--r--apps/fparkan-game/src/main.rs195
1 files changed, 164 insertions, 31 deletions
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs
index b57d690..dc996c9 100644
--- a/apps/fparkan-game/src/main.rs
+++ b/apps/fparkan-game/src/main.rs
@@ -61,8 +61,9 @@ use fparkan_render::{
RawCameraTransform,
};
use fparkan_render_vulkan::{
- node38_pose, node38_pose_from_hierarchy, node38_pose_relative_to_root,
- node38_pose_relative_to_root_from_hierarchy, project_land_msh_to_static_mesh_in_world_space,
+ legacy_packet_distance_squared, node38_pose, node38_pose_from_hierarchy,
+ node38_pose_relative_to_root, node38_pose_relative_to_root_from_hierarchy,
+ project_land_msh_to_static_mesh_in_world_space,
project_msh_to_static_mesh_in_world_space_with_node_fallback_poses,
project_msh_to_static_mesh_in_world_space_with_node_fallback_poses_and_mount,
project_msh_to_static_mesh_in_world_space_with_node_pose_buffer,
@@ -131,7 +132,6 @@ fn run(args: &[String]) -> Result<String, String> {
.ok_or_else(|| "mission terrain is unavailable after loading".to_string())?;
let roots = loaded_mission_object_drafts(&engine)
.map(|drafts| &drafts[..args.preview_roots.get().min(drafts.len())])
- .filter(|roots| !roots.is_empty())
.ok_or_else(|| {
"selected mission object drafts are unavailable after loading".to_string()
})?;
@@ -1374,7 +1374,7 @@ fn native_msh_diffuse_alpha_override(
let distance = if range.batch_flags & 0x0050 == 0x0040 {
0.0
} else {
- // shortcut: use the first indexed vertex; replace with native CPU clipping selection when clipped-portal captures are available.
+ // The native alpha callback reads the input packet before renderer clipping.
first_indexed_vertex_distance(mesh, range_index, eye_world)?
};
legacy_msh_portal_alpha(range.batch_flags, distance, horizontal_fov)
@@ -3979,10 +3979,7 @@ fn first_indexed_vertex_distance(
.get(vertex_index)
.ok_or_else(|| "world draw range vertex index is outside the vertex buffer".to_string())?;
let delta = sub3(vertex.position, camera_position);
- let distance = dot3(delta, delta).sqrt();
- distance
- .is_finite()
- .then_some(distance)
+ legacy_packet_distance_squared(dot3(delta, delta))
.ok_or_else(|| "world draw range sort distance is non-finite".to_string())
}
@@ -5498,6 +5495,8 @@ fn append_initial_control_effect_slots(
let Some(point_list) = control.point_list.as_ref() else {
return Ok(());
};
+ let mut unsupported_commands = 0;
+ let mut state_dependent_commands = 0;
for row in
selected_initial_control_effect_rows(control.initial_effect_group, &control.effect_section)
{
@@ -5511,10 +5510,22 @@ fn append_initial_control_effect_slots(
)
})?;
let key = format!("{archive}/{name}");
+ let mut record_skip = |reason: &str,
+ count: usize,
+ needs_state: bool,
+ command: Option<usize>| {
+ if needs_state {
+ state_dependent_commands += count;
+ } else {
+ unsupported_commands += count;
+ }
+ eprintln!("unexpanded Control FX reason:{reason} count:{count} needs_game_state:{needs_state} object:{object_index} component:{component_index} source:{key} table:{} row:{} command:{command:?}", row.table_index, row.row_index);
+ };
if !effect_documents.contains_key(&key) {
let bytes = load_resource_entry_bytes_from_root(root, &archive, &name)
.map_err(|error| format!("load Control FX {key}: {error}"))?;
let Ok(document) = decode_fxid(bytes) else {
+ eprintln!("render_coverage control_fx_invalid_documents=1 (object:{object_index} component:{component_index} source:{key})");
continue;
};
effect_documents.insert(key.clone(), document);
@@ -5531,17 +5542,24 @@ fn append_initial_control_effect_slots(
let duration_seconds = header.duration_seconds;
if !duration_seconds.is_finite()
|| duration_seconds < 0.0
- || !matches!(header.time_mode, 0 | 1 | 2)
- || (header.time_mode != 0 && duration_seconds <= 0.0)
|| !header.phase_jitter.is_finite()
|| !header.random_shift.iter().all(|value| value.is_finite())
|| !header.scale.iter().all(|value| value.is_finite())
{
- // Control can reference action effects whose header timing and
- // scale conventions are outside this ambient subset. Leave them
- // unexpanded until their lifetime and transform contract are
- // understood rather than failing the whole mission preview.
*unsupported_sounds += sound_commands;
+ record_skip("header_values", document.commands().len(), false, None);
+ continue;
+ }
+ if !matches!(header.time_mode, 0 | 1 | 2)
+ || (header.time_mode != 0 && duration_seconds <= 0.0)
+ {
+ *unsupported_sounds += sound_commands;
+ record_skip(
+ "time_mode",
+ document.commands().len(),
+ fparkan_fx::fx_time_mode_requires_owner_state(header.time_mode),
+ None,
+ );
continue;
}
let owner_node_index =
@@ -5554,6 +5572,7 @@ fn append_initial_control_effect_slots(
});
let Some(owner_node_index) = owner_node_index else {
*unsupported_sounds += sound_commands;
+ record_skip("owner_node", document.commands().len(), false, None);
continue;
};
let owner_node_pose = control_effect_owner_node_pose(
@@ -5563,6 +5582,7 @@ fn append_initial_control_effect_slots(
);
let Some(owner_node_pose) = owner_node_pose else {
*unsupported_sounds += sound_commands;
+ record_skip("owner_pose", document.commands().len(), false, None);
continue;
};
let root_transform = LegacyIron3dEulerTransform {
@@ -5629,6 +5649,7 @@ fn append_initial_control_effect_slots(
});
} else {
*unsupported_sounds += 1;
+ record_skip("sound_body", 1, false, Some(command_index));
eprintln!("unsupported Control Op2 object={object_index} component={component_index} effect={key} command={command_index} (mode, backend parameter or malformed body)");
}
} else {
@@ -5637,6 +5658,7 @@ fn append_initial_control_effect_slots(
continue;
}
let Some(kind) = ambient_effect_command_kind(command) else {
+ record_skip("opcode_or_body", 1, false, Some(command_index));
continue;
};
if !ambient_effect_render_selector_allows_header(
@@ -5646,11 +5668,13 @@ fn append_initial_control_effect_slots(
continue;
}
let Some(reference) = command.resource_refs.first() else {
+ record_skip("material_reference", 1, false, Some(command_index));
continue;
};
let material_archive = String::from_utf8_lossy(reference.archive_name()).into_owned();
let material_name = String::from_utf8_lossy(reference.resource_name()).into_owned();
if material_archive.is_empty() || material_name.is_empty() {
+ record_skip("empty_material_reference", 1, false, Some(command_index));
continue;
}
let cache_key = format!("{material_archive}/{material_name}");
@@ -5671,6 +5695,7 @@ fn append_initial_control_effect_slots(
&prepared.phase_directional_rgb,
&prepared.phase_additive_rgb,
) {
+ record_skip("material_tint", 1, false, Some(command_index));
continue;
}
let first_material = *prepared
@@ -5688,6 +5713,7 @@ fn append_initial_control_effect_slots(
[0.0; 3],
AMBIENT_EFFECT_NATIVE_CATEGORY,
) else {
+ record_skip("mesh_template", 1, false, Some(command_index));
continue;
};
Some(template)
@@ -5822,6 +5848,7 @@ fn append_initial_control_effect_slots(
});
}
}
+ eprintln!("render_coverage control_fx_unsupported={unsupported_commands} control_fx_requires_game_state={state_dependent_commands}");
Ok(())
}
@@ -8534,9 +8561,6 @@ fn static_preview_mesh_and_materials(
dynamic_component_geometry_by_object.push(dynamic_component_geometry);
control_animation_instances_by_object.push(control_animation_instances);
}
- if mesh_components == 0 {
- return Err("selected static preview roots have no mesh-backed visual".to_string());
- }
let free_camera = if legacy_camera.is_none() {
Some(FreeFlightCamera::from_mesh_and_terrain(&mesh, terrain)?)
} else {
@@ -9575,17 +9599,6 @@ impl StaticVulkanApp {
horizontal_fov(&self.view_camera, aspect),
)?;
}
- let approximate_portals = self
- .world_range_indices
- .iter()
- .filter(|&&index| {
- let flags = self.mesh.draw_ranges[index].batch_flags;
- flags & 8 != 0
- && flags & 0x50 != 0x40
- && !self.type16_present_ranges.contains(&index)
- })
- .count();
- eprintln!("render_coverage portal_fade_packet_selection_approx={approximate_portals} (first indexed vertex; native CPU clipping selection pending)");
eprintln!("render_coverage msh_type16_colors_unsupported={} (authored vertex colors are not decoded; using white)", self.type16_present_ranges.len());
let randomized_lights = self
.control_point_light_slots
@@ -13137,6 +13150,35 @@ mod tests {
#[test]
#[ignore = "requires installed GOG resources; set FPARKAN_GAME_ROOT"]
+ fn gog_static_preview_accepts_terrain_without_placed_objects() {
+ let root = std::env::var_os("FPARKAN_GAME_ROOT")
+ .map(PathBuf::from)
+ .unwrap_or_else(|| PathBuf::from(r"C:\GOG Games\Parkan - Iron Strategy"));
+ let path = "DATA/MAPS/41/Land.msh";
+ let bytes = std::fs::read(root.join(path)).expect("original map mesh");
+ let document = fparkan_assets::decode_nres_payload(Arc::from(bytes)).unwrap();
+ let land = fparkan_terrain_format::decode_land_msh(&document).unwrap();
+ let terrain = TerrainWorld::from_land_msh(&land).unwrap();
+ let preview = static_preview_mesh_and_materials(
+ &MissionAssets::default(),
+ &terrain,
+ &[],
+ None,
+ None,
+ None,
+ &root,
+ path,
+ )
+ .expect("terrain is a complete scene without object meshes");
+ assert_eq!(preview.mesh_components, 0);
+ assert_eq!(preview.terrain_components, 1);
+ assert!(!preview.mesh.vertices.is_empty());
+ assert!(!preview.mesh.indices.is_empty());
+ assert!(!preview.materials.is_empty());
+ }
+
+ #[test]
+ #[ignore = "requires installed GOG resources; set FPARKAN_GAME_ROOT"]
fn gog_terrain_detail_uses_multitexture_animation_block() {
let root = std::env::var_os("FPARKAN_GAME_ROOT")
.map(PathBuf::from)
@@ -13300,7 +13342,8 @@ mod tests {
let alpha = native_msh_diffuse_alpha_override(&mesh, 0, false, [0.0, -0.55, -25.0], 1.04)
.unwrap()
.unwrap();
- assert!((alpha - 0.5).abs() < 0.000_001);
+ // Native RSQRTSS perturbs the distance before the extra square root.
+ assert!((alpha - 0.5).abs() < 0.000_3);
assert_eq!(
native_msh_diffuse_alpha_override(&mesh, 0, false, [0.0, -0.55, -60.0], 1.04),
Ok(Some(1.0))
@@ -15305,7 +15348,97 @@ mod tests {
let mesh = VulkanStaticMesh::smoke_triangle();
let distance = first_indexed_vertex_distance(&mesh, 0, [0.0, -0.55, -3.0])
.expect("first indexed vertex distance");
- assert!((distance - 3.0).abs() < 1.0e-6);
+ assert!((distance - 3.0).abs() < 1.0e-3);
+ }
+
+ #[test]
+ #[ignore = "requires the original GOG installation and native SSE reference"]
+ fn gog_portal_alpha_keeps_the_reference_vertex_outside_the_viewport() {
+ let root = std::env::var_os("FPARKAN_CORPUS_PART1_ROOT")
+ .expect("set FPARKAN_CORPUS_PART1_ROOT to the original GOG installation");
+ let mut engine =
+ create(EngineServices::new(Arc::new(DirectoryVfs::new(root)))).expect("engine");
+ load_mission_static_preview_roots(
+ &mut engine,
+ MissionRequest {
+ key: "MISSIONS/Autodemo.00/data.tma".to_string(),
+ mode: MissionMode::SingleMission,
+ },
+ NonZeroUsize::new(usize::MAX).unwrap(),
+ )
+ .expect("original AutoDemo");
+ let assets = loaded_mission_assets(&engine).expect("mission assets");
+ let root = &loaded_mission_object_drafts(&engine).expect("mission objects")[4];
+ let visual = assets
+ .visual_by_id(assets.object_visuals[4][0])
+ .expect("factory visual");
+ let model = assets.model_by_id(visual.model_id.unwrap()).unwrap();
+ let mesh = project_msh_to_static_mesh_in_world_space_with_node_sampled_poses(
+ &model.validated,
+ LegacyIron3dEulerTransform {
+ translation: root.position,
+ orientation_radians: root.orientation_raw,
+ },
+ root.scale,
+ 0,
+ )
+ .expect("factory mesh");
+ // GOG capture: yaw/roll expose only part of the window; alpha was read
+ // after Terrain's callback stored it, before the renderer consumed it.
+ let transform = RawCameraTransform {
+ words: [
+ 3_204_541_090,
+ 3_209_392_467,
+ 1_051_461_443,
+ 1_135_728_591,
+ 1_063_051_501,
+ 3_203_294_891,
+ 1_045_010_100,
+ 1_143_538_729,
+ 0,
+ 1_053_254_103,
+ 1_064_028_839,
+ 1_093_589_271,
+ 0,
+ 0,
+ 0,
+ 1_065_353_216,
+ ],
+ };
+ let camera = VulkanStaticCamera::from_legacy_d3d7(
+ transform,
+ LegacyD3d7Projection {
+ viewport: [0, 0, 800, 600],
+ near_plane: 0.5,
+ far_plane: 700.0,
+ field_of_view_radians: 1.04,
+ },
+ )
+ .expect("native edge camera");
+ let range = mesh.draw_ranges[33];
+ assert_eq!(range.batch_flags, 0x0108);
+ let clip_vertex = |offset: usize| -> [f32; 4] {
+ let index = mesh.indices[range.first_index as usize + offset] as usize;
+ let [x, y, z] = mesh.vertices[index].position;
+ let point = [x, y, z, 1.0];
+ std::array::from_fn(|column| {
+ (0..4)
+ .map(|row| point[row] * camera.clip_from_world[row * 4 + column])
+ .sum()
+ })
+ };
+ let outside = clip_vertex(0);
+ let inside = clip_vertex(1);
+ assert!(outside[0] > outside[3]);
+ assert!(inside[0].abs() < inside[3]);
+ let distance = first_indexed_vertex_distance(&mesh, 33, transform.translation())
+ .expect("portal reference distance");
+ assert!((distance - f32::from_bits(0x42D5_7EE4)).abs() < 2.0e-5);
+ let alpha =
+ native_msh_diffuse_alpha_override(&mesh, 33, false, transform.translation(), 1.04)
+ .expect("portal alpha")
+ .expect("portal override");
+ assert!((alpha - f32::from_bits(0x3F05_190C)).abs() < 1.0e-6);
}
#[test]
@@ -16460,7 +16593,7 @@ mod tests {
let expected = dot3(expected, expected).sqrt();
let actual = first_indexed_vertex_distance(&mesh, range.range_index, camera)
.expect("native Op9 primitive distance key");
- assert!((actual - expected).abs() < 1e-5);
+ assert!((actual - expected).abs() < expected * 0.000_3 + 1e-5);
}
for selector in [1, 2] {