diff options
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/fparkan-game/src/camera_capture.rs | 1 | ||||
| -rw-r--r-- | apps/fparkan-game/src/main.rs | 152 | ||||
| -rw-r--r-- | apps/fparkan-game/src/terrain_lod.rs | 1 |
3 files changed, 133 insertions, 21 deletions
diff --git a/apps/fparkan-game/src/camera_capture.rs b/apps/fparkan-game/src/camera_capture.rs index bc16807..dccb738 100644 --- a/apps/fparkan-game/src/camera_capture.rs +++ b/apps/fparkan-game/src/camera_capture.rs @@ -23,6 +23,7 @@ pub(super) struct LegacyCameraCapture { pub(super) struct SkyUvClock { pub(super) verified: bool, pub(super) elapsed_milliseconds: u32, + pub(super) raw_clock: Option<u32>, } #[derive(Clone, Debug, Deserialize, PartialEq)] diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 3432bda..ca8742d 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -131,7 +131,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() })?; @@ -2760,6 +2759,7 @@ fn append_environment_range( batch_flags: 0, pipeline_state, terrain_base_only: false, + terrain_reflection: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }); Ok(EnvironmentGpuRange { @@ -4637,6 +4637,7 @@ fn multiply_row_major(left: [f32; 16], right: [f32; 16]) -> [f32; 16] { } struct StaticPreviewScene { + fixed_water_clock_ms: Option<u32>, mesh: VulkanStaticMesh, dynamic_vertex_start: usize, terrain_lod: Option<terrain_lod::TerrainLodDraws>, @@ -5496,6 +5497,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) { @@ -5509,10 +5512,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); @@ -5529,17 +5544,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 = @@ -5552,6 +5574,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( @@ -5561,6 +5584,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 { @@ -5627,6 +5651,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 { @@ -5635,6 +5660,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( @@ -5644,11 +5670,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}"); @@ -5669,6 +5697,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 @@ -5686,6 +5715,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) @@ -5820,6 +5850,7 @@ fn append_initial_control_effect_slots( }); } } + eprintln!("render_coverage control_fx_unsupported={unsupported_commands} control_fx_requires_game_state={state_dependent_commands}"); Ok(()) } @@ -7606,7 +7637,7 @@ fn project_basement_foundation( let mut shadow_receivers = Vec::with_capacity(foundation.fragments.len()); for (fragment_index, fragment) in foundation.fragments.iter().enumerate() { - let (material_index, base_only, vertices) = match fragment.source_face { + let (material_index, base_only, reflection, vertices) = match fragment.source_face { Some(face_index) => { let face = terrain.faces.get(face_index).ok_or_else(|| { format!("foundation fragment references unknown terrain face {face_index}") @@ -7654,6 +7685,7 @@ fn project_basement_foundation( ( face.material_tag, fragment.surface_mask.0 & 0x400 == 0, + fragment.surface_mask.0 & 0x20000 != 0, vertices, ) } @@ -7671,7 +7703,7 @@ fn project_basement_foundation( fragment_uv0_words, fragment_normals, ); - (0xff00, true, vertices) + (0xff00, true, false, vertices) } }; @@ -7684,7 +7716,13 @@ fn project_basement_foundation( mesh.indices.push(index); } if let Some(range) = mesh.draw_ranges.last_mut().filter(|range| { - range.material_index == material_index && range.terrain_base_only == base_only + range.material_index == material_index + && range.terrain_base_only == base_only + && range.terrain_reflection == reflection + && (!reflection + || vertices[0].position[2] + == mesh.vertices[mesh.indices[range.first_index as usize] as usize] + .position[2]) }) { range.index_count = range .index_count @@ -7699,6 +7737,7 @@ fn project_basement_foundation( batch_flags: 0, pipeline_state, terrain_base_only: base_only, + terrain_reflection: reflection, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }); } @@ -8004,9 +8043,9 @@ fn static_preview_mesh_and_materials( let unverified_special_faces = terrain_mesh .faces .iter() - .filter(|face| face.flags.0 & 8 != 0) + .filter(|face| face.flags.0 & 8 != 0 && face.flags.0 & 0x20000 == 0) .count(); - eprintln!("render_coverage terrain_face8_pipeline_unverified={unverified_special_faces} (source faces across LODs use the ordinary terrain shader; native special pass and reflection/EMBM mapping pending)"); + eprintln!("render_coverage terrain_face8_pipeline_unverified={unverified_special_faces} (non-reflective source faces across LODs; native special-pass mapping pending)"); let footprint_sources = mission_basement_footprints(assets, roots)?; let foundation = if footprint_sources.is_empty() { None @@ -8524,9 +8563,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 { @@ -8545,7 +8581,23 @@ fn static_preview_mesh_and_materials( }; let camera = view_camera.vulkan_camera(); let shadow_material_index = append_shadow_material(&mut materials)?; + let fixed_water_clock_ms = legacy_camera.as_ref().and_then(|capture| { + capture + .sky_uv_clock + .as_ref() + .and_then(|clock| clock.raw_clock) + }); + if legacy_camera.is_some() + && fixed_water_clock_ms.is_none() + && mesh + .draw_ranges + .iter() + .any(|range| range.terrain_reflection) + { + eprintln!("render_coverage captured_water_phase_unverified=1 (capture has no verified raw queue clock; using replay elapsed time)"); + } Ok(StaticPreviewScene { + fixed_water_clock_ms, mesh, dynamic_vertex_start, terrain_lod, @@ -9360,6 +9412,7 @@ fn run_static_vulkan_mode( } struct StaticVulkanApp { + fixed_water_clock_ms: Option<u32>, mesh: VulkanStaticMesh, dynamic_vertex_start: usize, terrain_lod: Option<terrain_lod::TerrainLodDraws>, @@ -9488,6 +9541,7 @@ impl StaticVulkanApp { dynamic_shadow_casters: preview.dynamic_shadow_casters, sun_occlusion_range_indices: preview.sun_occlusion_range_indices, terrain: preview.terrain, + fixed_water_clock_ms: preview.fixed_water_clock_ms, world_range_indices, world_range_index_counts, camera: preview.camera, @@ -9558,6 +9612,7 @@ impl StaticVulkanApp { }) .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 .iter() @@ -10324,6 +10379,21 @@ impl StaticVulkanApp { &mut self.material_animations, renderer, )?; + let water_clock_ms = self + .fixed_water_clock_ms + .map_or(frame.clock.time_ms, |captured| { + if self.target_frames == 0 { + captured.wrapping_add(frame.clock.time_ms) + } else { + captured + } + }); + let mut uniforms = renderer.frame_uniforms(); + uniforms.camera_position = frame.camera.position; + uniforms.embm_phase = fparkan_render::water::legacy_embm_phase(water_clock_ms); + renderer + .set_frame_uniforms(uniforms) + .map_err(|error| format!("update water clock uniforms: {error}"))?; for update in &frame.effect_updates { renderer .set_draw_range_index_count(update.range_index, update.index_count) @@ -11208,6 +11278,7 @@ mod tests { batch_flags, pipeline_state: LegacyPipelineState::default(), terrain_base_only: false, + terrain_reflection: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }; let mesh = VulkanStaticMesh { @@ -12467,6 +12538,7 @@ mod tests { LegacyDepthMode::TestReadOnly, ), terrain_base_only: false, + terrain_reflection: false, alpha_test_reference: 0, }; let mut mesh = VulkanStaticMesh { @@ -13091,6 +13163,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) @@ -13149,6 +13250,7 @@ mod tests { LegacyDepthMode::TestReadOnly, ), terrain_base_only: false, + terrain_reflection: false, alpha_test_reference: 0, }; let mut mesh = VulkanStaticMesh { @@ -13210,6 +13312,7 @@ mod tests { batch_flags, pipeline_state: world_pipeline_state(blend, LegacyDepthMode::TestReadOnly), terrain_base_only: false, + terrain_reflection: false, alpha_test_reference: 0, }; @@ -13651,6 +13754,7 @@ mod tests { }; let view_camera = free_camera.preview_frame(16.0 / 9.0); let preview = StaticPreviewScene { + fixed_water_clock_ms: None, terrain_lod: None, dynamic_vertex_start: 3, mesh: VulkanStaticMesh { @@ -13673,6 +13777,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, terrain_base_only: false, + terrain_reflection: false, pipeline_state: world_pipeline_state( LegacyBlendMode::Opaque, LegacyDepthMode::TestReadOnly, @@ -15716,13 +15821,18 @@ mod tests { let mut value: serde_json::Value = serde_json::from_slice(include_bytes!("../testdata/legacy-camera-parented.json")) .unwrap(); - value["sky_uv_clock"] = - serde_json::json!({"verified": true, "elapsed_milliseconds": 4051271258_u32}); + value["sky_uv_clock"] = serde_json::json!({"verified": true, "elapsed_milliseconds": 4051271258_u32, "raw_clock": 272342176_u32}); let capture = parse_legacy_camera_capture(&serde_json::to_vec(&value).unwrap()).unwrap(); - assert_eq!( - capture.sky_uv_clock.unwrap().elapsed_milliseconds, - 4051271258 - ); + let clock = capture.sky_uv_clock.unwrap(); + assert_eq!(clock.elapsed_milliseconds, 4051271258); + assert_eq!(clock.raw_clock, Some(272342176)); + value["sky_uv_clock"] + .as_object_mut() + .unwrap() + .remove("raw_clock"); + let old_capture = + parse_legacy_camera_capture(&serde_json::to_vec(&value).unwrap()).unwrap(); + assert_eq!(old_capture.sky_uv_clock.unwrap().raw_clock, None); value["sky_uv_clock"]["verified"] = serde_json::json!(false); assert!(parse_legacy_camera_capture(&serde_json::to_vec(&value).unwrap()).is_err()); } diff --git a/apps/fparkan-game/src/terrain_lod.rs b/apps/fparkan-game/src/terrain_lod.rs index 599178c..190f660 100644 --- a/apps/fparkan-game/src/terrain_lod.rs +++ b/apps/fparkan-game/src/terrain_lod.rs @@ -239,6 +239,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, terrain_base_only: true, + terrain_reflection: false, pipeline_state: super::super::world_pipeline_state( fparkan_render::LegacyBlendMode::Opaque, fparkan_render::LegacyDepthMode::TestWrite, |
