fix(simulation): address PR #81 review — critical and high-priority issues

Critical fixes:
- storyteller: replace .expect() with guard + log in activation_pass (Hoshe #1)
- content/loader: validate inverted tile_bounds before iteration (Hoshe #C)
- save_io: persist ActivationState on save/load (Tyre #7)

High-priority fixes:
- storyteller: f64 intermediate for observation_time_ticks scoring (Hoshe #A)
- storyteller: deduplicate copresent entities before scoring (Hoshe #B)
- storyteller: skip activation_pass at tick 0 (Hoshe #G)
- storyteller: explicit .before(advance_tick) ordering (Tyre #9)
- save_state: insert EngagementRecord on NPC deserialize (Tyre #10)
- save_io: reset TriangleActivatedQueue + MovementHistoryBuffer on load (Tyre #8)
- debug: validate teleport target walkability (Hoshe #E)
- debug: reject SkipToContamination when tick past delay (Hoshe #F)
- debug: DebugEnabled defaults to cfg!(debug_assertions) (Hoshe #3)
- debug: log when response overwritten (Hoshe #2)
- content/loader: error on tile dimension mismatch (Hoshe #5)
- content/types: DistrictMeta.description optional (Hoshe #D)
- types: version docs updated to v18 (Tyre #1, #2, #3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 23:40:59 +01:00
co-authored by Claude Opus 4.6
parent eb64f23b77
commit ac68ec6eef
9 changed files with 241 additions and 22 deletions
+18 -4
View File
@@ -377,29 +377,43 @@ fn apply_location_tiles(location: &Location, walkability: &mut WalkabilityMap) -
return false;
};
// Guard: inverted bounds cause (y_max - y_min) to be negative, which wraps to
// ~18 quintillion when cast to usize, silently writing tiles at garbage positions.
if bounds.x_min > bounds.x_max || bounds.y_min > bounds.y_max {
tracing::error!(
"Location '{}': inverted tile_bounds (x: {}..={}, y: {}..={}), skipping",
location.canonical_id,
bounds.x_min, bounds.x_max,
bounds.y_min, bounds.y_max,
);
return false;
}
let expected_height = (bounds.y_max - bounds.y_min + 1) as usize;
let expected_width = (bounds.x_max - bounds.x_min + 1) as usize;
if tiles.len() != expected_height {
tracing::warn!(
"Location '{}': tile row count {} != expected height {} (from tile_bounds)",
tracing::error!(
"Location '{}': tile row count {} != expected height {} (from tile_bounds) — skipping to prevent walkability holes",
location.canonical_id,
tiles.len(),
expected_height,
);
return false;
}
for (row_idx, row) in tiles.iter().enumerate() {
let y = bounds.y_min + row_idx as i32;
if row.len() != expected_width {
tracing::warn!(
"Location '{}' row {}: length {} != expected width {}",
tracing::error!(
"Location '{}' row {}: length {} != expected width {} — skipping row to prevent walkability holes",
location.canonical_id,
row_idx,
row.len(),
expected_width,
);
continue;
}
for (col_idx, ch) in row.chars().enumerate() {