diff options
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/fparkan-game/src/main.rs | 195 |
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] { |
