Verify actual native terrain occurrences in HD scene captures
This commit is contained in:
1 parent
1bfe69dc68
commit
64e59fda1f
1 file changed
+209
-8
+209
-8
@@ -7050,6 +7050,187 @@ mod tests {
|
||||
assert_eq!(menu_row_at(Screen::Missions, 0, 27, 145, 60), Some(1));
|
||||
assert_eq!(menu_row_at(Screen::Missions, 17, 27, 40, 60), Some(10));
|
||||
}
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct NativeTerrainSample {
|
||||
target_index: usize,
|
||||
asset: u32,
|
||||
mission: ContentId,
|
||||
row: usize,
|
||||
column: usize,
|
||||
placement: usize,
|
||||
settled: bool,
|
||||
}
|
||||
|
||||
// A catalog shard can begin with tiles that never occur in a native map.
|
||||
// Check the real layout before treating a job's sample as gameplay evidence.
|
||||
fn first_native_terrain_sample(
|
||||
job: &serde_json::Value,
|
||||
registry: &Registry,
|
||||
) -> anyhow::Result<Option<NativeTerrainSample>> {
|
||||
use anyhow::{Context, ensure};
|
||||
let targets = job["targets"]
|
||||
.as_array()
|
||||
.context("terrain job has no targets")?;
|
||||
for (target_index, target) in targets.iter().enumerate() {
|
||||
let context = &target["terrain_context"];
|
||||
let samples = context["samples"]
|
||||
.as_array()
|
||||
.context("terrain target has no sample list")?;
|
||||
if samples.is_empty() {
|
||||
ensure!(
|
||||
context["registry_or_layout_referenced"].as_bool() == Some(false),
|
||||
"terrain target {} has no occurrence samples but is not explicitly unused",
|
||||
target["asset"]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let asset = u32::from_str_radix(
|
||||
target["asset"]
|
||||
.as_str()
|
||||
.context("terrain target has no asset ID")?,
|
||||
16,
|
||||
)?;
|
||||
let sample = &samples[0];
|
||||
let mission = ContentId::from(
|
||||
sample["mission"]
|
||||
.as_str()
|
||||
.context("sample has no mission")?,
|
||||
);
|
||||
let definition = registry
|
||||
.missions
|
||||
.get(&mission)
|
||||
.context("sample mission is absent from the registry")?;
|
||||
let row = usize::try_from(sample["row"].as_u64().context("sample has invalid row")?)?;
|
||||
let column = usize::try_from(
|
||||
sample["column"]
|
||||
.as_u64()
|
||||
.context("sample has invalid column")?,
|
||||
)?;
|
||||
let columns = definition.columns as usize;
|
||||
ensure!(
|
||||
columns > 0 && column < columns,
|
||||
"sample column is outside the mission"
|
||||
);
|
||||
let placement = row
|
||||
.checked_mul(columns)
|
||||
.and_then(|value| value.checked_add(column))
|
||||
.context("sample placement overflows")?;
|
||||
let tile_index = *definition
|
||||
.layout
|
||||
.get(placement)
|
||||
.context("sample row is outside the mission")?;
|
||||
let tile = definition
|
||||
.tiles
|
||||
.get(tile_index)
|
||||
.context("sample references an invalid mission tile")?;
|
||||
let settled = if tile.art.asset == Some(asset) {
|
||||
false
|
||||
} else {
|
||||
ensure!(
|
||||
tile.destroyed.as_ref().and_then(|art| art.asset) == Some(asset),
|
||||
"sample target {asset:x} does not occur at {mission} row {row}, column {column}"
|
||||
);
|
||||
true
|
||||
};
|
||||
return Ok(Some(NativeTerrainSample {
|
||||
target_index,
|
||||
asset,
|
||||
mission,
|
||||
row,
|
||||
column,
|
||||
placement,
|
||||
settled,
|
||||
}));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn terrain_sample_registry() -> Registry {
|
||||
let mut registry = Registry::default();
|
||||
let mission: crate::content::MissionDef = serde_json::from_value(serde_json::json!({
|
||||
"id":"base:sample_fixture", "name":"Sample fixture", "duration":10,
|
||||
"columns":2, "layout":[0, 1], "tiles":[
|
||||
{"art":{"asset":16,"logical_width":32,"logical_height":32},
|
||||
"destroyed":{"asset":17,"logical_width":32,"logical_height":32}},
|
||||
{"art":{"asset":18,"logical_width":32,"logical_height":32}}
|
||||
]
|
||||
}))
|
||||
.unwrap();
|
||||
registry.missions.insert(mission.id.clone(), mission);
|
||||
registry
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_terrain_sample_skips_unused_targets_and_verifies_the_live_layout() {
|
||||
let registry = terrain_sample_registry();
|
||||
let mut job = serde_json::json!({"targets":[
|
||||
{"asset":"0f", "terrain_context":{"samples":[],"registry_or_layout_referenced":false}},
|
||||
{"asset":"10", "terrain_context":{"samples":[
|
||||
{"mission":"base:sample_fixture","row":0,"column":0}
|
||||
]}},
|
||||
{"asset":"12", "terrain_context":{"samples":[
|
||||
{"mission":"base:sample_fixture","row":0,"column":1}
|
||||
]}}
|
||||
]});
|
||||
assert_eq!(
|
||||
first_native_terrain_sample(&job, ®istry).unwrap(),
|
||||
Some(NativeTerrainSample {
|
||||
target_index: 1,
|
||||
asset: 16,
|
||||
mission: "base:sample_fixture".into(),
|
||||
row: 0,
|
||||
column: 0,
|
||||
placement: 0,
|
||||
settled: false,
|
||||
})
|
||||
);
|
||||
job["targets"][1]["asset"] = "11".into();
|
||||
assert!(
|
||||
first_native_terrain_sample(&job, ®istry)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.settled
|
||||
);
|
||||
job["targets"][1]["asset"] = "12".into();
|
||||
assert!(
|
||||
first_native_terrain_sample(&job, ®istry)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("does not occur")
|
||||
);
|
||||
job["targets"][1]["terrain_context"]["samples"][0]["column"] = 2.into();
|
||||
assert!(
|
||||
first_native_terrain_sample(&job, ®istry)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("column")
|
||||
);
|
||||
job["targets"][1]["terrain_context"]["samples"][0]["column"] = 0.into();
|
||||
job["targets"][1]["terrain_context"]["samples"][0]["row"] = 1.into();
|
||||
assert!(
|
||||
first_native_terrain_sample(&job, ®istry)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("row")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_terrain_sample_reports_an_entirely_unused_shard_without_a_fake_occurrence() {
|
||||
let registry = terrain_sample_registry();
|
||||
let mut job = serde_json::json!({"targets":[
|
||||
{"asset":"0f", "terrain_context":{"samples":[],"registry_or_layout_referenced":false}},
|
||||
{"asset":"ff", "terrain_context":{"samples":[],"registry_or_layout_referenced":false}}
|
||||
]});
|
||||
assert_eq!(first_native_terrain_sample(&job, ®istry).unwrap(), None);
|
||||
job["targets"][1]["terrain_context"]["registry_or_layout_referenced"] = true.into();
|
||||
assert!(
|
||||
first_native_terrain_sample(&job, ®istry)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("not explicitly unused")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires original catalog and canonical terrain candidate; captures cached native mission terrain"]
|
||||
fn canonical_terrain_live_scene_matches_fresh_composition_after_scroll_damage_and_mod_changes()
|
||||
@@ -7079,6 +7260,21 @@ mod tests {
|
||||
let job_root = hd_root.join("jobs").join(&job_id);
|
||||
let job: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(job_root.join("job.json")).unwrap()).unwrap();
|
||||
let output = project.join(".reference/terrain-hd").join(&job_id);
|
||||
std::fs::create_dir_all(&output).unwrap();
|
||||
let Some(sample) = first_native_terrain_sample(&job, &host.active.registry).unwrap() else {
|
||||
std::fs::write(
|
||||
output.join("native-scene-cache-proof.json"),
|
||||
serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"job":job_id,"status":"no_native_occurrence",
|
||||
"native_scene_validated":false,"all_32_scroll_phases_equal_fresh":false,
|
||||
"native_destroyed_placement_tested":null,"mod_order_and_hash_invalidate":false,
|
||||
"limits":"Every target is explicitly unused in native maps. Gallery or synthetic review remains necessary; no native gameplay validation was run."
|
||||
})).unwrap(),
|
||||
).unwrap();
|
||||
println!("{job_id}: no native occurrence; native scene validation was not run");
|
||||
return;
|
||||
};
|
||||
let candidate: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(job_root.join("candidate.json")).unwrap())
|
||||
.unwrap();
|
||||
@@ -7103,19 +7299,20 @@ mod tests {
|
||||
host.hd_loaded = true;
|
||||
host.config.scale = 4;
|
||||
host.config.hd_mode = true;
|
||||
let sample = &job["targets"][0]["terrain_context"]["samples"][0];
|
||||
let mission = ContentId::from(sample["mission"].as_str().unwrap());
|
||||
let mission = sample.mission.clone();
|
||||
let definition = &host.active.registry.missions[&mission];
|
||||
let row_origin = 64 - sample["row"].as_i64().unwrap() as i32 * 32;
|
||||
let row_origin = 64 - sample.row as i32 * 32;
|
||||
let scroll =
|
||||
row_origin - 200 + (definition.layout.len() / definition.columns as usize) as i32 * 32;
|
||||
let mut world = World::new(&mission, &host.pilot, &host.active.registry).unwrap();
|
||||
world.tick = 64;
|
||||
world.scroll = scroll;
|
||||
if sample.settled {
|
||||
world.terrain_dead[sample.placement] = true;
|
||||
world.terrain_settled[sample.placement] = true;
|
||||
}
|
||||
host.world = Some(world);
|
||||
host.screen = Screen::Game;
|
||||
let output = project.join(".reference/terrain-hd").join(&job_id);
|
||||
std::fs::create_dir_all(&output).unwrap();
|
||||
let capture = |host: &mut Host, name: &str| {
|
||||
let before = serde_json::json!({"world":host.world,"pilot":host.pilot,"states":host.active.scripts.states,"rng":host.active.scripts.rng});
|
||||
let frame = host.draw().unwrap();
|
||||
@@ -7140,7 +7337,7 @@ mod tests {
|
||||
std::hint::black_box(host.draw().unwrap());
|
||||
}
|
||||
let cached_scene_mean_ms = before.elapsed().as_secs_f64() * 1000.0 / 60.0;
|
||||
for phase in 1..32 {
|
||||
for phase in 0..32 {
|
||||
host.world.as_mut().unwrap().scroll = scroll + phase;
|
||||
let cached = host.draw().unwrap().image;
|
||||
host.terrain_hd = TerrainHd::default();
|
||||
@@ -7259,13 +7456,17 @@ mod tests {
|
||||
host.draw().unwrap();
|
||||
assert!(host.terrain_hd.prepared_bytes() <= 320 * 268 * 16 * 2 + 65536);
|
||||
std::fs::write(output.join("native-scene-cache-proof.json"),serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"mission":mission,"row":sample["row"],"all_32_scroll_phases_equal_fresh":true,
|
||||
"job":job_id,"status":"native_scene_validated","native_scene_validated":true,
|
||||
"asset":format!("{:x}",sample.asset),"target_index":sample.target_index,
|
||||
"mission":mission,"row":sample.row,"column":sample.column,"sample_settled":sample.settled,
|
||||
"all_32_scroll_phases_equal_fresh":true,
|
||||
"native_destroyed_placement_tested":damaged,"mod_order_and_hash_invalidate":true,
|
||||
"cached_scene_mean_ms":cached_scene_mean_ms,"retained_strip_bytes":host.terrain_hd.prepared_bytes(),
|
||||
"transition_lookup_bytes":host.terrain_hd.cache_bytes(),"registration":"Isolated candidate pack; global manifest unchanged"
|
||||
})).unwrap()).unwrap();
|
||||
println!(
|
||||
"cached native scene mean {cached_scene_mean_ms:.3} ms; damaged placement {damaged:?}"
|
||||
"{job_id}: target {:x} at {mission} row {}, column {}; all 32 phases matched; cached native scene mean {cached_scene_mean_ms:.3} ms; damaged placement {damaged:?}",
|
||||
sample.asset, sample.row, sample.column,
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user