aboutsummaryrefslogtreecommitdiff
path: root/apps/fparkan-game
diff options
context:
space:
mode:
Diffstat (limited to 'apps/fparkan-game')
-rw-r--r--apps/fparkan-game/src/camera_capture.rs1
-rw-r--r--apps/fparkan-game/src/main.rs154
-rw-r--r--apps/fparkan-game/src/terrain_lod.rs1
3 files changed, 137 insertions, 19 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 bbf9ead..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,
});
}
@@ -8001,6 +8040,12 @@ fn static_preview_mesh_and_materials(
let mut terrain_material_loader = StandaloneWearMaterialLoader::new(root);
let source_terrain_component = project_land_msh_to_static_mesh_in_world_space(terrain_mesh)
.map_err(|err| format!("project mission terrain for Vulkan: {err}"))?;
+ let unverified_special_faces = terrain_mesh
+ .faces
+ .iter()
+ .filter(|face| face.flags.0 & 8 != 0 && face.flags.0 & 0x20000 == 0)
+ .count();
+ 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
@@ -8518,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 {
@@ -8539,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,
@@ -9354,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>,
@@ -9482,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,
@@ -9552,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()
@@ -10318,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)
@@ -11202,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 {
@@ -12461,6 +12538,7 @@ mod tests {
LegacyDepthMode::TestReadOnly,
),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
};
let mut mesh = VulkanStaticMesh {
@@ -13085,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)
@@ -13143,6 +13250,7 @@ mod tests {
LegacyDepthMode::TestReadOnly,
),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
};
let mut mesh = VulkanStaticMesh {
@@ -13204,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,
};
@@ -13645,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 {
@@ -13667,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,
@@ -15710,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,