Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions crates/tilewright-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,18 +181,22 @@ document exits with code 1. Unrelated snapshot diagnostics remain separate and
can accompany a successful summary.

The command does not emit raw documents or unprojected system fields. Its map
and coordinate values are stored nonnegative scalars, not validated map
references or positions. It does not interpret party members or `versionId`,
compare titles across files, validate editor compatibility, or establish
mutation, round-trip, and write support. The snapshot resource-limit options
are available on `system`.
IDs are stored nonnegative scalars and its player-start coordinates are signed
scalars; neither is a validated map reference or position. It does not
interpret party members or `versionId`, compare titles across files, validate
editor compatibility, or establish mutation, round-trip, and write support.
The snapshot resource-limit options are available on `system`.

The `validate` command loads the same bounded snapshot and delegates the
player-start check to the core library. It reports the observed exact unset
triplet, a zero map ID with unevidenced nonzero coordinates, a missing positive
catalog record, and coordinates outside the selected map dimensions. Findings
are completed validation results and exit with code 0. Acquisition, loading,
and structural system, catalog, or selected-map failures exit with code 1.
triplet, an editor-preserved ambiguous zero map ID with nonzero coordinates, a
missing positive catalog record, and signed coordinates outside the selected
map dimensions. Findings are completed validation results and exit with code
0. Acquisition, loading, and structural system, catalog, or selected-map
failures exit with code 1.

All JSON commands currently emit schema version 2. This executable-wide version
advanced when signed player-start coordinate values were introduced.

A finding-free result means only that this bounded player-start check found no
issue. It does not establish project validity, MZ-version compatibility,
Expand Down
20 changes: 10 additions & 10 deletions crates/tilewright-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use tilewright::rpg_maker_mz::tileset_catalog::{
TilesetCatalog, TilesetCatalogError, TilesetField, tileset_catalog,
};

const OUTPUT_SCHEMA_VERSION: u8 = 1;
const OUTPUT_SCHEMA_VERSION: u8 = 2;

fn parse_max_bytes(s: &str) -> Result<usize, String> {
let val: usize = s.parse().map_err(|_| "must be a valid positive integer")?;
Expand Down Expand Up @@ -761,8 +761,8 @@ struct SystemDetail {
locale: String,
edit_map_id: u32,
start_map_id: u32,
start_x: u32,
start_y: u32,
start_x: i64,
start_y: i64,
}

#[derive(Debug, Serialize)]
Expand Down Expand Up @@ -817,8 +817,8 @@ struct PlayerStartValidationDetail {
scope: ValidationScope,
finding_free: bool,
start_map_id: u32,
start_x: u32,
start_y: u32,
start_x: i64,
start_y: i64,
finding_count: usize,
findings: Vec<PlayerStartFindingReport>,
}
Expand All @@ -834,16 +834,16 @@ enum ValidationScope {
enum PlayerStartFindingReport {
MissingPlayerStart,
ZeroMapIdWithCoordinates {
start_x: u32,
start_y: u32,
start_x: i64,
start_y: i64,
},
MissingMapRecord {
map_id: u32,
},
OutOfBounds {
map_id: u32,
start_x: u32,
start_y: u32,
start_x: i64,
start_y: i64,
width: u32,
height: u32,
},
Expand Down Expand Up @@ -3734,7 +3734,7 @@ fn write_human_player_start_finding(
),
PlayerStartFindingReport::ZeroMapIdWithCoordinates { start_x, start_y } => writeln!(
writer,
" - map ID 0 has unevidenced coordinates ({start_x}, {start_y})"
" - map ID 0 has ambiguous stored coordinates ({start_x}, {start_y})"
),
PlayerStartFindingReport::MissingMapRecord { map_id } => {
writeln!(writer, " - map {map_id} has no map-catalog record")
Expand Down
105 changes: 89 additions & 16 deletions crates/tilewright-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ fn discover_emits_versioned_json_for_scripts() {
assert!(output.status.success());
assert!(stderr(&output).is_empty());
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert_eq!(report["result"], "candidate");
assert_eq!(report["markers"][0]["kind"], "regular_file");
assert_eq!(
Expand Down Expand Up @@ -228,7 +228,7 @@ fn operational_errors_have_human_and_json_forms() {
assert_eq!(json.status.code(), Some(1));
assert!(stderr(&json).is_empty());
let report: Value = serde_json::from_slice(&json.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert!(
report["error"]["message"]
.as_str()
Expand Down Expand Up @@ -287,7 +287,7 @@ fn inventory_emits_versioned_json_for_scripts() {
assert!(output.status.success());
assert!(stderr(&output).is_empty());
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);

let entries = report["entries"].as_array().unwrap();
assert_eq!(entries.len(), 2);
Expand Down Expand Up @@ -333,7 +333,7 @@ fn inventory_refuses_non_utf8_paths_in_json() {

assert_eq!(output.status.code(), Some(1));
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert!(
report["error"]["message"].as_str().unwrap().contains(
"JSON output cannot safely represent non-UTF-8 paths without lossy conversion"
Expand Down Expand Up @@ -426,7 +426,7 @@ fn inventory_operational_errors_have_human_and_json_forms() {
assert_eq!(json.status.code(), Some(1));
assert!(stderr(&json).is_empty());
let report: Value = serde_json::from_slice(&json.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert!(
report["error"]["message"]
.as_str()
Expand Down Expand Up @@ -540,7 +540,7 @@ fn snapshot_emits_deterministic_versioned_json_without_source_contents() {
assert!(output.status.success());
assert!(stderr(&output).is_empty());
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert_eq!(report["completeness"], "complete");
assert_eq!(report["loaded_document_count"], 3);
assert_eq!(report["diagnostic_count"], 0);
Expand Down Expand Up @@ -716,7 +716,7 @@ fn snapshot_operational_errors_have_human_and_json_forms() {
assert_eq!(json.status.code(), Some(1));
assert!(stderr(&json).is_empty());
let report: Value = serde_json::from_slice(&json.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert!(
report["error"]["message"]
.as_str()
Expand Down Expand Up @@ -863,7 +863,7 @@ fn maps_emits_deterministic_versioned_json_without_unprojected_contents() {
assert!(output.status.success());
assert!(stderr(&output).is_empty());
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert_eq!(report["snapshot_completeness"], "complete");
assert_eq!(report["map_count"], 2);
assert_eq!(report["finding_count"], 0);
Expand Down Expand Up @@ -964,7 +964,7 @@ fn maps_projection_errors_have_human_and_json_forms() {
assert_eq!(json.status.code(), Some(1));
assert!(stderr(&json).is_empty());
let report: Value = serde_json::from_slice(&json.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert_eq!(report["error"]["category"], "missing_document");
assert_eq!(report["snapshot_completeness"], "complete");

Expand Down Expand Up @@ -1030,7 +1030,7 @@ fn maps_operational_errors_preserve_stream_separation() {
assert_eq!(json.status.code(), Some(1));
assert!(stderr(&json).is_empty());
let report: Value = serde_json::from_slice(&json.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert!(
report["error"]["message"]
.as_str()
Expand Down Expand Up @@ -1100,7 +1100,7 @@ fn tilesets_emits_deterministic_versioned_json_without_opaque_contents() {
assert!(stderr(&first).is_empty());
assert_eq!(first.stdout, second.stdout);
let report: Value = serde_json::from_slice(&first.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert_eq!(report["snapshot_completeness"], "complete");
assert_eq!(report["tileset_count"], 2);
assert_eq!(report["tilesets"][0]["id"], 1);
Expand Down Expand Up @@ -1211,7 +1211,7 @@ fn map_emits_versioned_json_without_unprojected_contents() {
assert!(stderr(&first).is_empty());
assert_eq!(first.stdout, second.stdout);
let report: Value = serde_json::from_slice(&first.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert_eq!(report["snapshot_completeness"], "complete");
assert_eq!(report["map"]["id"], 1);
assert_eq!(report["map"]["catalog_name"], "First");
Expand Down Expand Up @@ -1427,7 +1427,7 @@ fn events_emits_deterministic_versioned_json_without_opaque_contents() {
assert!(stderr(&first).is_empty());
assert_eq!(first.stdout, second.stdout);
let report: Value = serde_json::from_slice(&first.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert_eq!(report["snapshot_completeness"], "complete");
assert_eq!(report["map"]["id"], 1);
assert_eq!(report["map"]["catalog_name"], "Town");
Expand Down Expand Up @@ -1528,7 +1528,7 @@ fn system_emits_versioned_json_without_unprojected_contents() {
assert!(!output_text.contains("secret"));
assert!(!output_text.contains("not emitted"));
let report: Value = serde_json::from_str(&output_text).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert_eq!(report["snapshot_completeness"], "complete");
assert_eq!(report["snapshot_diagnostic_count"], 0);
assert_eq!(report["system"]["game_title"], "Game");
Expand All @@ -1545,6 +1545,28 @@ fn system_emits_versioned_json_without_unprojected_contents() {
assert_eq!(report["snapshot_diagnostics"], Value::Array(Vec::new()));
}

#[test]
fn system_reports_signed_coordinates_in_human_and_json_output() {
let temp = TempDir::new().unwrap();
let root = write_system_project(
&temp,
br#"{"gameTitle":"Game","currencyUnit":"G","locale":"en_US","editMapId":1,"startMapId":1,"startX":-1,"startY":-2}"#,
);

let human = tilewright(&["system", root.to_str().unwrap()]);
assert!(human.status.success());
assert!(stderr(&human).is_empty());
assert!(stdout(&human).contains("Player start: map 1 at (-1, -2)"));

let json = tilewright(&["system", root.to_str().unwrap(), "--format", "json"]);
assert!(json.status.success());
assert!(stderr(&json).is_empty());
let report: Value = serde_json::from_slice(&json.stdout).unwrap();
assert_eq!(report["schema_version"], 2);
assert_eq!(report["system"]["start_x"], -1);
assert_eq!(report["system"]["start_y"], -2);
}

#[test]
fn system_keeps_unrelated_snapshot_diagnostics_separate() {
let temp = TempDir::new().unwrap();
Expand Down Expand Up @@ -1678,6 +1700,57 @@ fn validate_reports_a_clear_player_start_for_people() {
assert!(output.contains("No player-start findings."));
}

#[test]
fn validate_reports_negative_coordinates_as_out_of_bounds() {
let temp = TempDir::new().unwrap();
let root = write_validation_project(
&temp,
br#"{"gameTitle":"Game","currencyUnit":"G","locale":"en_US","editMapId":1,"startMapId":1,"startX":-1,"startY":-2}"#,
Some(br#"[null,{"id":1,"name":"One","order":1,"parentId":0}]"#),
Some(
br#"{"displayName":"One","width":10,"height":8,"tilesetId":1,"events":[]}"#,
),
);

let human = tilewright(&["validate", root.to_str().unwrap()]);
assert!(human.status.success());
assert!(stderr(&human).is_empty());
assert!(stdout(&human).contains("(-1, -2) is outside map 1 dimensions 10 x 8"));

let json = tilewright(&["validate", root.to_str().unwrap(), "--format", "json"]);
assert!(json.status.success());
assert!(stderr(&json).is_empty());
let report: Value = serde_json::from_slice(&json.stdout).unwrap();
assert_eq!(report["schema_version"], 2);
assert_eq!(report["validation"]["start_x"], -1);
assert_eq!(report["validation"]["start_y"], -2);
assert_eq!(
report["validation"]["findings"][0]["category"],
"out_of_bounds"
);
assert_eq!(report["validation"]["findings"][0]["start_x"], -1);
assert_eq!(report["validation"]["findings"][0]["start_y"], -2);
}

#[test]
fn validate_describes_zero_map_signed_coordinates_as_ambiguous() {
let temp = TempDir::new().unwrap();
let root = write_validation_project(
&temp,
br#"{"gameTitle":"Game","currencyUnit":"G","locale":"en_US","editMapId":1,"startMapId":0,"startX":-1,"startY":2}"#,
None,
None,
);

let output = tilewright(&["validate", root.to_str().unwrap()]);

assert!(output.status.success());
assert!(stderr(&output).is_empty());
let output = stdout(&output);
assert!(output.contains("map ID 0 has ambiguous stored coordinates (-1, 2)"));
assert!(!output.contains("unevidenced"));
}

#[test]
fn validate_keeps_unrelated_snapshot_diagnostics_separate() {
let temp = TempDir::new().unwrap();
Expand Down Expand Up @@ -1750,7 +1823,7 @@ fn validate_emits_versioned_json_for_each_contextual_finding() {
);
assert!(stderr(&output).is_empty());
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert_eq!(report["validation"]["scope"], "player_start");
assert_eq!(report["validation"]["finding_free"], false);
assert_eq!(report["validation"]["finding_count"], 1);
Expand Down Expand Up @@ -1914,7 +1987,7 @@ fn validate_tilesets_emits_deterministic_versioned_json_findings() {
assert!(stderr(&first).is_empty());
assert_eq!(first.stdout, second.stdout);
let report: Value = serde_json::from_slice(&first.stdout).unwrap();
assert_eq!(report["schema_version"], 1);
assert_eq!(report["schema_version"], 2);
assert_eq!(report["validation"]["scope"], "map_tileset_references");
assert_eq!(report["validation"]["finding_free"], false);
assert_eq!(report["validation"]["map_count"], 2);
Expand Down
18 changes: 9 additions & 9 deletions crates/tilewright/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,10 @@ fn print_system_summary(snapshot: &ProjectSnapshot) {
}
```

The map ID and coordinate values are nonnegative scalars, not validated map
references. The operation does not interpret other system settings, compare
titles across files, establish editor compatibility, or expose mutation and
serialization.
The map IDs are nonnegative scalars and the player-start coordinates are signed
scalars; neither is a validated map reference or position. The operation does
not interpret other system settings, compare titles across files, establish
editor compatibility, or expose mutation and serialization.

### Example: Player-Start Validation

Expand All @@ -337,11 +337,11 @@ fn print_player_start_findings(snapshot: &ProjectSnapshot) {
}
```

The operation recognizes the observed exact zero triplet, reports a missing
positive catalog record, and checks coordinates against selected-map
dimensions. A finding-free report is not general project validity or editor
compatibility. The operation does not inspect passability, events, vehicles, or
write behavior.
The operation recognizes the observed exact zero triplet, reports an ambiguous
zero-map/mixed-coordinate state or a missing positive catalog record, and
checks signed coordinates against selected-map dimensions. A finding-free
report is not general project validity or editor compatibility. The operation
does not inspect passability, events, vehicles, or write behavior.

## Responsibilities

Expand Down
Loading