diff --git a/migration/placement-plan/REPORT.md b/migration/placement-plan/REPORT.md index 80e6bb4..19fa525 100644 --- a/migration/placement-plan/REPORT.md +++ b/migration/placement-plan/REPORT.md @@ -18,7 +18,7 @@ Version 2 applies the owner's decisions of 2026-10-18 on top of version 1 (PR #4 - Placement changes from version 1: `ellamin` layer offset -4 -> -5; `rockforge_a` layer offset -4 -> -5; `rockforge_b` layer offset -5 -> -6; `rockforge_c` layer offset -6 -> -7. - Why portals remain: 17 other-entrance-aligned; 1 slope-blocked; 1 stairwell-blocked. See [Portals that remain](#portals-that-remain). - View radius: **16 tiles** (the classic server's limit; the shipped client sees 10). With radius 10 the plan would have 65 stairs, 48 stairwell exits and 19 portals; see [View radius](#view-radius). -- Independent check (`scripts/verify_plan.py`, JSON only): **PASS**: 207 placed member maps, 7 nearby owner pairs measured (closest unrelated pair 22 tiles apart, gap rule needs more than 16), 40 stairwells with 76 intermediate tiles, 1456 exit endpoints re-derived; relayering re-derived from 242,824 overworld tiles and 82,133 tile heights (12 regions, 243 units, 47,594 moved tiles, 904 cut edges in 44 seam pieces). +- Independent check (`scripts/verify_plan.py`, JSON only): **PASS**: 207 placed member maps, 7 nearby owner pairs measured (closest unrelated pair 22 tiles apart, gap rule needs more than 16), 40 stairwells with 76 intermediate tiles, 1,007 exit record copies matched against the inventory of 1,200 exit objects, 1456 exit endpoints re-derived; relayering re-derived from 242,824 overworld tiles and 82,133 tile heights (12 regions, 243 units, 47,594 moved tiles, 904 cut edges in 44 seam pieces). | classification | reason | groups | maps | |---|---|---:|---:| @@ -881,10 +881,12 @@ Each image covers its regions and every storey or cave unit that moved because o ```sh cd migration/placement-plan python3 -I scripts/build_plan.py --content /home/zoey/dev/worktrees/content-plan-v2-input --out . -python3 -I scripts/verify_plan.py placement-plan.json +python3 -I scripts/verify_plan.py placement-plan.json --content /home/zoey/dev/worktrees/content-plan-v2-input +python3 -I scripts/test_placement_plan.py +python3 -I scripts/build_plan.py --content /home/zoey/dev/worktrees/content-plan-v2-input --out . --check ``` -`--content` must be a clean checkout of `atrinik/content` at `3ad20150da76` (the shared checkout was behind `main`, so this run used a separate clone at the current `main`). The scripts need only the Python standard library (the PNGs are written by `raster.py`). The content checkout must be clean under `arch/` and `maps/` (`--allow-dirty` is for experiments and marks the JSON). Artifacts are generated in a staging directory, the staged JSON is verified again, and only then are `placement-plan.json`, `REPORT.md` and the PNGs replaced; a failed run leaves the old artifacts in place. +`--content` must be a clean checkout of `atrinik/content` at `3ad20150da76` (the shared checkout was behind `main`, so this run used a separate clone at the current `main`). The scripts need only the Python standard library (the PNGs are written by `raster.py`). The content checkout must be clean under `arch/` and `maps/` (`--allow-dirty` is for experiments and marks the JSON). Artifacts are generated in a staging directory, the staged JSON is verified again, and only then are `placement-plan.json`, `REPORT.md` and the PNGs replaced; a failed run leaves the old artifacts in place. A group that cannot be placed stays unplaced and fails verification, so such a plan is never published. `test_placement_plan.py` tampers with copies of the committed JSON and checks that the verifier rejects each one; it needs no content checkout. `verify_plan.py` makes two checks. Without `--content` it re-derives the placement rules from the JSON alone. With `--content` it also rebuilds the plan from the content with the generator and requires the published JSON to equal the rebuild field for field (the checkout path aside), so an edit that keeps the JSON consistent with itself (an exit dropped from `exit_inventory` together with its count, say) fails there. `build_plan.py --check` regenerates every artifact in a temporary directory and fails unless each is byte-identical (run it with the `--content` path the plan records). The run is deterministic (sorted inputs, lexicographic tie-breaks) and read-only against the content checkout; rerunning with the same arguments on the same content rewrites `placement-plan.json`, `REPORT.md` and the PNGs byte-for-byte. `content_git_head`, `content_worktree_clean` and `content_inputs_digest` (SHA-256 over every file under `arch/` and `maps/`, checked again after the run) in the JSON identify the input. Scripts: `mapdata.py` (read-only reader, tile links, occupied tiles, floors per sub-layer, `no_pass` tiles), `analysis.py` (components, layout, exit resolution), `relayer.py` (the relayering: regions, units, conflicts, seams), `footprint.py` (gap rule, column and sloped stairwell chains), `build_plan.py` (classification, relayering, placement, stairwells, wiring, JSON, staged publication), `report.py`, `render.py` and `raster.py` (images), and `verify_plan.py` (independent check of the JSON). Tunables (`VIEW_RADIUS`, `CONTACT_MARGIN`, `MAX_EXTRA_LAYERS`, `MAX_SHIFT`, `STAIR_TOLERANCE`, `STAIRWELL_MAX_BEND`, `LAYER_HEIGHT_Z` in `build_plan.py`; `MIN_REGION_TILES`, `WALKABLE_DZ`, `SEAM_CELL` in `relayer.py`) and the hand-written inputs (system maps, owner decisions, the Rockforge mines alignment hint, version 1's placements for the comparison) are constants. diff --git a/migration/placement-plan/placement-plan.json b/migration/placement-plan/placement-plan.json index ce7c767..769a586 100644 --- a/migration/placement-plan/placement-plan.json +++ b/migration/placement-plan/placement-plan.json @@ -1,5 +1,5 @@ { - "schema": "atrinik-overworld-placement-plan/3", + "schema": "atrinik-overworld-placement-plan/4", "generated_by": "scripts/build_plan.py", "content_root": "/home/zoey/dev/worktrees/content-plan-v2-input", "content_git_head": "3ad20150da7656516dae06d4f504d09af4cba344", @@ -73,6 +73,14 @@ "ladder_up" ] ], + "tentative_alignments": [ + { + "group": "/shattered_islands/strakewood_island/rockforge/rockforge_c", + "anchor": "/shattered_islands/strakewood_island/rockforge/rockforge_b", + "layer_delta": -1, + "why": "rockforge_a/_b/_c share the same 5x5 sheet naming (0101..ab05) and rockforge_a<->rockforge_b stairs connect identical local coordinates; rockforge_c ('Rockforge: Mines') is assumed to be the next sheet below rockforge_b at the same x/y." + } + ], "overworld_map_size": 24, "excluded_trees": [ "python", @@ -97,7 +105,8 @@ "stairwell_ids": "SWn = stairwell built for a placed group's aligned exits, SLn = sloped stairwell, SRn = stairwell for an overworld exit whose ends the relayering put 2+ layers apart", "relayering": "overworld tiles are on their final layer everywhere in this plan (see relayering); occupied_rows_by_layer gives each map's tiles per final layer", "wired_exits": "an exit with form 'wired' led nowhere in legacy; it now targets the content on the layer above or below (see its 'wired' evidence)", - "exits_kept_as_decoration": "dangling exits the owner keeps as decoration (no exit): not wired, with the landing they would have had" + "exits_kept_as_decoration": "dangling exits the owner keeps as decoration (no exit): not wired, with the landing they would have had", + "exit_inventory": "every exit object read from the content (inventory exits excluded), keyed by source_map and line: the content fields of its exit record with its legacy form and resolved target (none for an exit that leads nowhere), plus the facts its data problems come from; the exit records and data problems are checked against it" }, "summary": { "maps_total": 3629, @@ -187,7 +196,8 @@ "/shattered_islands/strakewood_island/rockforge/rockforge_c" ], "unplaced_place_groups": [], - "unplaced_uncertain_groups": [] + "unplaced_uncertain_groups": [], + "failed_placements": [] }, "start_map": "/shattered_islands/deserted_tutorial_island/ship_lower_deck", "view_radius_sensitivity": { @@ -200,6 +210,7 @@ }, "stairwells": 38, "stairwell_sites_blocked": 1, + "groups_unplaced": [], "groups_placed_differently": [ "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique_a", "/shattered_islands/incuna/ellamin", @@ -6155,403 +6166,19447 @@ ], "placement_changes_from_v1": [ { - "group": "/shattered_islands/incuna/ellamin", - "v1": [ - 105, - 1991, - -4 + "group": "/shattered_islands/incuna/ellamin", + "v1": [ + 105, + 1991, + -4 + ], + "v2": [ + 105, + 1991, + -5 + ], + "ideal_v2": [ + 105, + 1991, + -1 + ], + "blocked_by": [ + "world_3_82_-1 (gap, layer -2)", + "world_3_82_-1 (gap, layer -3)", + "world_3_83_-1 (gap, layer -1)", + "world_4_82_-1 (gap, layer -4)" + ] + }, + { + "group": "/shattered_islands/strakewood_island/rockforge/rockforge_a", + "v1": [ + -13, + 1347, + -4 + ], + "v2": [ + -13, + 1347, + -5 + ], + "ideal_v2": [ + -13, + 1347, + -2 + ], + "blocked_by": [ + "world_-4_56_-2 (gap, layer -2)", + "world_1_51_-3 (gap, layer -4)", + "world_2_52_-3 (gap, layer -3)" + ] + }, + { + "group": "/shattered_islands/strakewood_island/rockforge/rockforge_b", + "v1": [ + -13, + 1347, + -5 + ], + "v2": [ + -13, + 1347, + -6 + ], + "ideal_v2": [ + -13, + 1347, + -6 + ], + "blocked_by": [] + }, + { + "group": "/shattered_islands/strakewood_island/rockforge/rockforge_c", + "v1": [ + -13, + 1347, + -6 + ], + "v2": [ + -13, + 1347, + -7 + ], + "ideal_v2": [ + -13, + 1347, + -7 + ], + "blocked_by": [] + } + ], + "exits_kept_as_decoration": [ + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 2, + 1 + ], + "arch": "ladder_up", + "line": 415, + "plane_source": [ + 281, + 1597, + -1 + ], + "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", + "migrate_as": "decoration with the ladder_up look: no exit, no destination", + "would_land_on": { + "map": "/shattered_islands/world_11_66", + "tile": [ + 17, + 13 + ], + "target_from": "the tile above (plus the landing offset [0, 0])", + "plane_landing": [ + 281, + 1597, + 0 + ], + "plane_target": [ + 281, + 1597, + 0 + ], + "source_tile_objects": [ + "floor_marble_b1a", + "bed_1b.111", + "bedpost_b", + "bed_1b.111", + "ladder_up" + ], + "landing_tile_objects": [ + "water_lt2" + ], + "landing_floor_terrain": [ + "water" + ], + "landing_blocked": false, + "exits_on_source_tile": [], + "return_exits_near_landing": [], + "verdict": "doubtful", + "why": "the landing floor is water (terrain_type 2), which a walking player cannot enter (classic blocked(), server/src/server/map.c:697); the landing is 18 tile(s) from this group's nearest entrance on that layer" + } + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 2, + 7 + ], + "arch": "ladder_up", + "line": 476, + "plane_source": [ + 281, + 1603, + -1 + ], + "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", + "migrate_as": "decoration with the ladder_up look: no exit, no destination", + "would_land_on": { + "map": "/shattered_islands/world_11_66", + "tile": [ + 17, + 19 + ], + "target_from": "the tile above (plus the landing offset [0, 0])", + "plane_landing": [ + 281, + 1603, + 0 + ], + "plane_target": [ + 281, + 1603, + 0 + ], + "source_tile_objects": [ + "floor_marble_b1a", + "bed_1b.111", + "bedpost_b", + "bed_1b.111", + "ladder_up" + ], + "landing_tile_objects": [ + "water_lt" + ], + "landing_floor_terrain": [ + "water" + ], + "landing_blocked": false, + "exits_on_source_tile": [], + "return_exits_near_landing": [], + "verdict": "doubtful", + "why": "the landing floor is water (terrain_type 2), which a walking player cannot enter (classic blocked(), server/src/server/map.c:697); the landing is 12 tile(s) from this group's nearest entrance on that layer" + } + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 2, + 13 + ], + "arch": "ladder_up", + "line": 541, + "plane_source": [ + 281, + 1609, + -1 + ], + "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", + "migrate_as": "decoration with the ladder_up look: no exit, no destination", + "would_land_on": { + "map": "/shattered_islands/world_11_67", + "tile": [ + 17, + 1 + ], + "target_from": "the tile above (plus the landing offset [0, 0])", + "plane_landing": [ + 281, + 1609, + 0 + ], + "plane_target": [ + 281, + 1609, + 0 + ], + "source_tile_objects": [ + "floor_marble_b1a", + "bed_1b.111", + "bedpost_b", + "bed_1b.111", + "ladder_up" + ], + "landing_tile_objects": [ + "floor_flagm3" + ], + "landing_floor_terrain": [ + "land" + ], + "landing_blocked": false, + "exits_on_source_tile": [], + "return_exits_near_landing": [], + "verdict": "plausible", + "why": "walkable land floor on the landing tile and no exit contradicts it; the landing is 6 tile(s) from this group's nearest entrance on that layer" + } + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 13, + 1 + ], + "arch": "ladder_up", + "line": 2619, + "plane_source": [ + 292, + 1597, + -1 + ], + "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", + "migrate_as": "decoration with the ladder_up look: no exit, no destination", + "would_land_on": { + "map": "/shattered_islands/world_12_66", + "tile": [ + 4, + 13 + ], + "target_from": "the tile above (plus the landing offset [0, 0])", + "plane_landing": [ + 292, + 1597, + 0 + ], + "plane_target": [ + 292, + 1597, + 0 + ], + "source_tile_objects": [ + "floor_marble_b1b", + "bed_1b.111", + "bedpost_b", + "bed_1b.111", + "ladder_up" + ], + "landing_tile_objects": [ + "water_lt" + ], + "landing_floor_terrain": [ + "water" + ], + "landing_blocked": false, + "exits_on_source_tile": [], + "return_exits_near_landing": [], + "verdict": "doubtful", + "why": "the landing floor is water (terrain_type 2), which a walking player cannot enter (classic blocked(), server/src/server/map.c:697); the landing is 18 tile(s) from this group's nearest entrance on that layer" + } + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 13, + 7 + ], + "arch": "ladder_up", + "line": 2688, + "plane_source": [ + 292, + 1603, + -1 + ], + "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", + "migrate_as": "decoration with the ladder_up look: no exit, no destination", + "would_land_on": { + "map": "/shattered_islands/world_12_66", + "tile": [ + 4, + 19 + ], + "target_from": "the tile above (plus the landing offset [0, 0])", + "plane_landing": [ + 292, + 1603, + 0 + ], + "plane_target": [ + 292, + 1603, + 0 + ], + "source_tile_objects": [ + "floor_marble_b1a", + "bed_1b.111", + "bedpost_b", + "bed_1b.111", + "ladder_up" + ], + "landing_tile_objects": [ + "water_lt3" + ], + "landing_floor_terrain": [ + "water" + ], + "landing_blocked": false, + "exits_on_source_tile": [], + "return_exits_near_landing": [], + "verdict": "doubtful", + "why": "the landing floor is water (terrain_type 2), which a walking player cannot enter (classic blocked(), server/src/server/map.c:697); the landing is 12 tile(s) from this group's nearest entrance on that layer" + } + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 13, + 13 + ], + "arch": "ladder_up", + "line": 2757, + "plane_source": [ + 292, + 1609, + -1 + ], + "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", + "migrate_as": "decoration with the ladder_up look: no exit, no destination", + "would_land_on": { + "map": "/shattered_islands/world_12_67", + "tile": [ + 4, + 1 + ], + "target_from": "the tile above (plus the landing offset [0, 0])", + "plane_landing": [ + 292, + 1609, + 0 + ], + "plane_target": [ + 292, + 1609, + 0 + ], + "source_tile_objects": [ + "floor_marble_b1b", + "bed_1b.111", + "bedpost_b", + "bed_1b.111", + "ladder_up" + ], + "landing_tile_objects": [ + "water_lt3" + ], + "landing_floor_terrain": [ + "water" + ], + "landing_blocked": false, + "exits_on_source_tile": [], + "return_exits_near_landing": [], + "verdict": "doubtful", + "why": "the landing floor is water (terrain_type 2), which a walking player cannot enter (classic blocked(), server/src/server/map.c:697); the landing is 12 tile(s) from this group's nearest entrance on that layer" + } + } + ], + "exit_inventory": [ + { + "source_map": "/emergency", + "source_tile": [ + 0, + 0 + ], + "target_map": "/shattered_islands/world_2_68", + "target_tile": [ + 3, + 7 + ], + "arch": "exit", + "line": 17, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/guilds/quick_wolves/guild", + "source_tile": [ + 1, + 12 + ], + "target_map": "/guilds/quick_wolves/storage", + "target_tile": [ + 19, + 23 + ], + "arch": "stairs_wood_up", + "line": 171, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/guilds/quick_wolves/guild", + "source_tile": [ + 1, + 15 + ], + "target_map": "/guilds/quick_wolves/oracle", + "target_tile": [ + 10, + 2 + ], + "arch": "stairs_red_up", + "line": 202, + "form": "explicit", + "vertical": "up", + "event_scripts": [ + "/python/guilds/oracle_entrance.py" + ] + }, + { + "source_map": "/guilds/quick_wolves/guild", + "source_tile": [ + 18, + 20 + ], + "arch": "stairs_wood_up", + "line": 2078, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/guilds/quick_wolves/guild", + "source_tile": [ + 22, + 12 + ], + "arch": "stairs_wood_down", + "line": 2631, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/guilds/quick_wolves/guild", + "source_tile": [ + 22, + 20 + ], + "target_map": "/shattered_islands/world_4_65", + "target_tile": [ + 12, + 6 + ], + "arch": "exit", + "line": 2681, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/guilds/quick_wolves/oracle", + "source_tile": [ + 10, + 2 + ], + "target_map": "/guilds/quick_wolves/guild", + "target_tile": [ + 1, + 15 + ], + "arch": "stairs_red_down", + "line": 830, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/guilds/quick_wolves/storage", + "source_tile": [ + 19, + 23 + ], + "target_map": "/guilds/quick_wolves/guild", + "target_tile": [ + 1, + 12 + ], + "arch": "stairs_wood_down", + "line": 3780, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/hall_of_dms", + "source_tile": [ + 10, + 16 + ], + "target_map": "/guilds/quick_wolves/oracle", + "target_tile": [ + 7, + 7 + ], + "arch": "exit", + "line": 629, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/hall_of_dms", + "source_tile": [ + 12, + 16 + ], + "target_map": "/planes/creation/creation_0808", + "target_tile": [ + 11, + 12 + ], + "arch": "exit", + "line": 748, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/hall_of_dms", + "source_tile": [ + 14, + 16 + ], + "target_map": "/shattered_islands/world_1_68", + "target_tile": [ + 12, + 19 + ], + "arch": "exit", + "line": 876, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/hall_of_dms", + "source_tile": [ + 16, + 16 + ], + "target_map": "/shattered_islands/world_9_68", + "target_tile": [ + 16, + 11 + ], + "arch": "exit", + "line": 995, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/hall_of_dms", + "source_tile": [ + 18, + 16 + ], + "target_map": "/shattered_islands/world_7_78", + "target_tile": [ + 11, + 19 + ], + "arch": "exit", + "line": 1114, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/planes/bone_cairn/bone_cairn_0_0", + "source_tile": [ + 9, + 1 + ], + "arch": "stairs_stone_up", + "line": 1676, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/planes/bone_cairn/bone_cairn_0_0", + "source_tile": [ + 10, + 1 + ], + "arch": "stairs_stone_up", + "line": 1845, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/planes/bone_cairn/bone_cairn_0_0", + "source_tile": [ + 10, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_3_3_-2", + "target_tile": [ + 1, + 2 + ], + "arch": "exit_green", + "line": 1939, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/planes/bone_cairn/bone_cairn_0_0", + "source_tile": [ + 11, + 1 + ], + "arch": "stairs_stone_up", + "line": 2009, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/planes/creation/creation_0501", + "source_tile": [ + 2, + 1 + ], + "target_map": "/planes/creation/creation_0601", + "target_tile": [ + 3, + 1 + ], + "arch": "exit_green", + "line": 427, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0501", + "source_tile": [ + 6, + 1 + ], + "target_map": "/planes/creation/creation_0501", + "target_tile": [ + 13, + 1 + ], + "arch": "exit", + "line": 996, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0501", + "source_tile": [ + 12, + 1 + ], + "target_map": "/planes/creation/creation_0501", + "target_tile": [ + 5, + 1 + ], + "arch": "exit", + "line": 1706, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0501", + "source_tile": [ + 16, + 1 + ], + "target_map": "/planes/creation/creation_0501", + "target_tile": [ + 16, + 1 + ], + "arch": "shop_mat", + "line": 2234, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0501", + "source_tile": [ + 17, + 13 + ], + "target_map": "/planes/creation/creation_0601", + "target_tile": [ + 1, + 11 + ], + "arch": "exit_green", + "line": 2455, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0501", + "source_tile": [ + 19, + 20 + ], + "target_map": "/planes/creation/creation_0601", + "target_tile": [ + 19, + 6 + ], + "arch": "exit", + "line": 2736, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0501", + "source_tile": [ + 21, + 1 + ], + "target_map": "/planes/creation/creation_0501", + "target_tile": [ + 21, + 1 + ], + "arch": "shop_mat", + "line": 2916, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0601", + "source_tile": [ + 1, + 12 + ], + "target_map": "/planes/creation/creation_0501", + "target_tile": [ + 17, + 12 + ], + "arch": "exit_green", + "line": 304, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0601", + "source_tile": [ + 2, + 1 + ], + "target_map": "/planes/creation/creation_0501", + "target_tile": [ + 2, + 2 + ], + "arch": "exit_green", + "line": 376, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0601", + "source_tile": [ + 20, + 6 + ], + "target_map": "/planes/creation/creation_0501", + "target_tile": [ + 19, + 19 + ], + "arch": "exit", + "line": 2825, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0601", + "source_tile": [ + 21, + 1 + ], + "target_map": "/planes/creation/creation_0601", + "target_tile": [ + 16, + 1 + ], + "arch": "portal_azure", + "line": 2949, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0801", + "source_tile": [ + 7, + 1 + ], + "target_map": "/planes/creation/creation_0801", + "target_tile": [ + 1, + 4 + ], + "arch": "teleporter", + "line": 1199, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0801", + "source_tile": [ + 21, + 3 + ], + "target_map": "/planes/creation/creation_0801", + "target_tile": [ + 18, + 5 + ], + "arch": "exit", + "line": 3227, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0801", + "source_tile": [ + 21, + 5 + ], + "target_map": "/planes/creation/creation_0801", + "target_tile": [ + 18, + 5 + ], + "arch": "exit", + "line": 3241, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0801", + "source_tile": [ + 21, + 7 + ], + "target_map": "/planes/creation/creation_0801", + "target_tile": [ + 18, + 5 + ], + "arch": "teleporter", + "line": 3257, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0808", + "source_tile": [ + 11, + 11 + ], + "target_map": "/hall_of_dms", + "target_tile": [ + 12, + 17 + ], + "arch": "exit", + "line": 1488, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/planes/creation/creation_0907", + "source_tile": [ + 4, + 15 + ], + "target_map": "/planes/creation/creation_0907", + "target_tile": [ + 4, + 21 + ], + "arch": "exit", + "line": 999, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/desert_plane_0101", + "source_tile": [ + 23, + 0 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/unusual_garden", + "target_tile": [ + 15, + 10 + ], + "arch": "ladder_down2", + "line": 2345, + "form": "explicit", + "vertical": "down", + "event_scripts": [ + "scripts/ladder.py" + ] + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 8, + 8 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 787, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 8, + 9 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 798, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 8, + 10 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 809, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 8, + 11 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 820, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 8, + 12 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 831, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 8, + 13 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 842, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 8, + 14 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 853, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 9, + 8 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 931, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 9, + 14 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 962, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 10, + 8 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1040, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 10, + 14 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1071, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 11, + 8 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1149, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 11, + 14 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1187, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 12, + 8 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1265, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 12, + 14 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1296, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 13, + 8 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1374, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 13, + 14 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1405, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 14, + 8 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1483, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 14, + 9 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1494, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 14, + 10 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1505, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 14, + 11 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1516, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 14, + 12 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1527, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 14, + 13 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1538, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "source_tile": [ + 14, + 14 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 11, + 5 + ], + "arch": "invis_exit", + "line": 1549, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "source_tile": [ + 1, + 2 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "target_tile": [ + 3, + 2 + ], + "arch": "invis_exit", + "line": 71, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/passage_0101", + "source_tile": [ + 6, + 1 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/desert_plane_0101", + "target_tile": [ + 17, + 22 + ], + "arch": "invis_exit", + "line": 295, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/unusual_garden", + "source_tile": [ + 12, + 15 + ], + "target_map": "/shattered_islands/world_12_59", + "target_tile": [ + 12, + 15 + ], + "arch": "hole", + "line": 1613, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/tol_0101", + "source_tile": [ + 8, + 1 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/tol_0101", + "target_tile": [ + 15, + 10 + ], + "arch": "exit", + "line": 1165, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/tol_0101", + "source_tile": [ + 11, + 12 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/desert_plane/entrance", + "target_tile": [ + 11, + 11 + ], + "arch": "exit", + "line": 1699, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/tol_0101", + "source_tile": [ + 19, + 16 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/tol_0201", + "target_tile": [ + 22, + 22 + ], + "arch": "exit", + "line": 2935, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/tol_0101", + "source_tile": [ + 23, + 5 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/tol_0101", + "target_tile": [ + 22, + 22 + ], + "arch": "exit", + "line": 3517, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/tol_0201", + "source_tile": [ + 4, + 10 + ], + "target_map": "/shattered_islands/world_11_61", + "target_tile": [ + 10, + 10 + ], + "arch": "stairs_stone_up", + "line": 441, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/berri_lur/temple_loki/tol_0201", + "source_tile": [ + 7, + 1 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/tol_0101", + "target_tile": [ + 15, + 10 + ], + "arch": "exit", + "line": 740, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/deserted_tutorial_island/mushroom_cavern", + "source_tile": [ + 11, + 3 + ], + "target_map": "/shattered_islands/world_-5_76", + "target_tile": [ + 13, + 21 + ], + "arch": "invis_exit", + "line": 605, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/deserted_tutorial_island/ship_lower_deck", + "source_tile": [ + 0, + 4 + ], + "arch": "hole2", + "line": 64, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/deserted_tutorial_island/ship_lower_deck", + "source_tile": [ + 1, + 0 + ], + "arch": "hole2", + "line": 148, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/deserted_tutorial_island/ship_lower_deck", + "source_tile": [ + 1, + 6 + ], + "target_map": "/shattered_islands/world_-7_76", + "target_tile": [ + 8, + 14 + ], + "arch": "stairs_wood_up2", + "line": 206, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/deserted_tutorial_island/ship_lower_deck", + "source_tile": [ + 2, + 6 + ], + "target_map": "/shattered_islands/world_-7_76", + "target_tile": [ + 8, + 14 + ], + "arch": "stairs_wood_up", + "line": 345, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/deserted_tutorial_island/ship_lower_deck", + "source_tile": [ + 4, + 4 + ], + "arch": "hole2", + "line": 595, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/deserted_tutorial_island/ship_lower_deck", + "source_tile": [ + 4, + 8 + ], + "arch": "hole2", + "line": 638, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/eld_woods_island/apartments/apartment_cheap", + "source_tile": [ + 1, + 1 + ], + "arch": "exit", + "line": 79, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_out.py" + ] + }, + { + "source_map": "/shattered_islands/eld_woods_island/apartments/apartment_expensive", + "source_tile": [ + 1, + 1 + ], + "arch": "exit", + "line": 97, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_out.py" + ] + }, + { + "source_map": "/shattered_islands/eld_woods_island/apartments/apartment_normal", + "source_tile": [ + 1, + 1 + ], + "arch": "exit", + "line": 85, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_out.py" + ] + }, + { + "source_map": "/shattered_islands/eld_woods_island/cave_morlarg", + "source_tile": [ + 22, + 4 + ], + "target_map": "/shattered_islands/world_11_79", + "target_tile": [ + 8, + 7 + ], + "arch": "invis_exit", + "line": 2637, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/eld_woods_island/cave_morlarg", + "source_tile": [ + 22, + 5 + ], + "target_map": "/shattered_islands/world_11_79", + "target_tile": [ + 8, + 8 + ], + "arch": "invis_exit", + "line": 2652, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance", + "source_tile": [ + 1, + 1 + ], + "arch": "ladder_down", + "line": 64, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance", + "source_tile": [ + 1, + 1 + ], + "target_map": "/shattered_islands/world_8_78", + "target_tile": [ + 6, + 23 + ], + "arch": "ladder_up", + "line": 71, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance", + "source_tile": [ + 3, + 3 + ], + "arch": "ladder_up", + "line": 181, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance", + "source_tile": [ + 8, + 3 + ], + "arch": "ladder_down", + "line": 537, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance", + "source_tile": [ + 10, + 5 + ], + "arch": "ladder_up", + "line": 732, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance", + "source_tile": [ + 15, + 5 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance_a", + "target_tile": [ + 17, + 14 + ], + "arch": "ladder_down", + "line": 1196, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance_a", + "source_tile": [ + 8, + 1 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique", + "target_tile": [ + 37, + 16 + ], + "arch": "invis_exit", + "line": 175, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance_a", + "source_tile": [ + 14, + 13 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique_tipper", + "target_tile": [ + 17, + 4 + ], + "arch": "ladder_down", + "line": 358, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance_a", + "source_tile": [ + 17, + 14 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance", + "target_tile": [ + 15, + 5 + ], + "arch": "ladder_up", + "line": 641, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique", + "source_tile": [ + 8, + 22 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique_a", + "target_tile": [ + 21, + 2 + ], + "arch": "ladder_down", + "line": 1017, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique", + "source_tile": [ + 9, + 11 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance_a", + "target_tile": [ + 14, + 13 + ], + "arch": "ladder_up", + "line": 1092, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique_a", + "source_tile": [ + 21, + 2 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique", + "target_tile": [ + 8, + 22 + ], + "arch": "ladder_up", + "line": 3226, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique_tipper", + "source_tile": [ + 2, + 1 + ], + "target_map": "/shattered_islands/world_8_78", + "target_tile": [ + 6, + 23 + ], + "arch": "invis_exit", + "line": 102, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique_tipper", + "source_tile": [ + 17, + 1 + ], + "target_map": "/shattered_islands/world_8_78", + "target_tile": [ + 6, + 23 + ], + "arch": "invis_exit", + "line": 560, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_unique_tipper", + "source_tile": [ + 17, + 4 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance_a", + "target_tile": [ + 14, + 13 + ], + "arch": "ladder_up", + "line": 590, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_1", + "source_tile": [ + 2, + 2 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_2", + "target_tile": [ + 1, + 1 + ], + "arch": "ladder_up", + "line": 136, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_1", + "source_tile": [ + 4, + 4 + ], + "target_map": "/shattered_islands/world_7_77", + "target_tile": [ + 18, + 21 + ], + "arch": "ladder_down", + "line": 263, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_2", + "source_tile": [ + 1, + 1 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_1", + "target_tile": [ + 2, + 2 + ], + "arch": "ladder_down", + "line": 76, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_2", + "source_tile": [ + 3, + 3 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_3", + "target_tile": [ + 4, + 4 + ], + "arch": "ladder_up", + "line": 199, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_3", + "source_tile": [ + 2, + 2 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_4", + "target_tile": [ + 3, + 3 + ], + "arch": "ladder_up", + "line": 140, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_3", + "source_tile": [ + 4, + 4 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_2", + "target_tile": [ + 3, + 3 + ], + "arch": "ladder_down", + "line": 267, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_4", + "source_tile": [ + 3, + 3 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_3", + "target_tile": [ + 2, + 2 + ], + "arch": "ladder_down", + "line": 238, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_4", + "source_tile": [ + 4, + 7 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_4", + "target_tile": [ + 4, + 6 + ], + "arch": "exit", + "line": 327, + "form": "same-map", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_teleport.py" + ] + }, + { + "source_map": "/shattered_islands/eld_woods_island/hemlock_cave/hemlock_cave_0101", + "source_tile": [ + 2, + 22 + ], + "target_map": "/shattered_islands/world_14_71", + "target_tile": [ + 14, + 4 + ], + "arch": "stairs_stone_up", + "line": 298, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/hemlock_cave/hemlock_cave_0101", + "source_tile": [ + 3, + 22 + ], + "target_map": "/shattered_islands/world_14_71", + "target_tile": [ + 15, + 4 + ], + "arch": "stairs_stone_up", + "line": 444, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/vielumin_ruins/ruins_0201", + "source_tile": [ + 9, + 22 + ], + "target_map": "/shattered_islands/world_19_72", + "target_tile": [ + 0, + 4 + ], + "arch": "stairs_stone_up", + "line": 1503, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/eld_woods_island/vielumin_ruins/ruins_0201", + "source_tile": [ + 10, + 22 + ], + "target_map": "/shattered_islands/world_19_72", + "target_tile": [ + 1, + 4 + ], + "arch": "stairs_stone_up", + "line": 1666, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/everlink/everlink_0102", + "source_tile": [ + 5, + 7 + ], + "arch": "exit", + "line": 564, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/exit.py" + ] + }, + { + "source_map": "/shattered_islands/everlink/everlink_0301", + "source_tile": [ + 20, + 12 + ], + "target_map": "/shattered_islands/everlink/everlink_0301", + "target_tile": [ + 20, + 12 + ], + "arch": "shop_mat", + "line": 3009, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/everlink/everlink_0301", + "source_tile": [ + 20, + 14 + ], + "target_map": "/shattered_islands/everlink/everlink_0301", + "target_tile": [ + 20, + 14 + ], + "arch": "shop_mat", + "line": 3027, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/everlink/everlink_0302", + "source_tile": [ + 15, + 11 + ], + "target_map": "/shattered_islands/everlink/everlink_0302", + "target_tile": [ + 15, + 11 + ], + "arch": "shop_mat", + "line": 1734, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/everlink/everlink_0302", + "source_tile": [ + 17, + 11 + ], + "target_map": "/shattered_islands/everlink/everlink_0302", + "target_tile": [ + 17, + 11 + ], + "arch": "shop_mat", + "line": 2052, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/incuna/apartments/beach_nook", + "source_tile": [ + 10, + 13 + ], + "arch": "exit", + "line": 1295, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_out.py" + ] + }, + { + "source_map": "/shattered_islands/incuna/ellamin", + "source_tile": [ + 3, + 0 + ], + "target_map": "/shattered_islands/world_4_83", + "target_tile": [ + 15, + 7 + ], + "arch": "invis_exit", + "line": 460, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/incuna/ellamin", + "source_tile": [ + 4, + 13 + ], + "target_map": "/shattered_islands/world_4_83", + "target_tile": [ + 13, + 12 + ], + "arch": "stairs_wood_up", + "line": 737, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/incuna/ship_lower_deck", + "source_tile": [ + 6, + 2 + ], + "target_map": "/shattered_islands/world_4_85", + "target_tile": [ + 7, + 13 + ], + "arch": "stairs_wood_up", + "line": 284, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/incuna/ship_lower_deck", + "source_tile": [ + 6, + 3 + ], + "target_map": "/shattered_islands/world_4_85", + "target_tile": [ + 7, + 13 + ], + "arch": "stairs_wood_up2", + "line": 301, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/incuna/ship_lower_deck_to_incuna", + "source_tile": [ + 6, + 2 + ], + "target_map": "/shattered_islands/world_4_85", + "target_tile": [ + 7, + 13 + ], + "arch": "stairs_wood_up", + "line": 272, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/incuna/ship_lower_deck_to_incuna", + "source_tile": [ + 6, + 3 + ], + "target_map": "/shattered_islands/world_4_85", + "target_tile": [ + 7, + 13 + ], + "arch": "stairs_wood_up2", + "line": 284, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/incuna/ship_lower_deck_to_strakewood", + "source_tile": [ + 2, + 7 + ], + "target_map": "/shattered_islands/world_0_70", + "target_tile": [ + 1, + 13 + ], + "arch": "stairs_wood_up", + "line": 333, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/incuna/ship_lower_deck_to_strakewood", + "source_tile": [ + 3, + 7 + ], + "target_map": "/shattered_islands/world_0_70", + "target_tile": [ + 1, + 13 + ], + "arch": "stairs_wood_up2", + "line": 465, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/lyondale_archipelago/morliana/hierro/hierro_lair", + "source_tile": [ + 8, + 25 + ], + "target_map": "/shattered_islands/world_13_7", + "target_tile": [ + 12, + 8 + ], + "arch": "stairs_stone_up", + "line": 1477, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/lyondale_archipelago/morliana/hierro/hierro_lair", + "source_tile": [ + 9, + 25 + ], + "target_map": "/shattered_islands/world_13_7", + "target_tile": [ + 13, + 8 + ], + "arch": "stairs_stone_up", + "line": 1654, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/lyondale_archipelago/morliana/hierro/hierro_lair", + "source_tile": [ + 10, + 25 + ], + "target_map": "/shattered_islands/world_13_7", + "target_tile": [ + 14, + 8 + ], + "arch": "stairs_stone_up", + "line": 1904, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 1, + 2 + ], + "target_map": "/emergency", + "target_tile": [ + 0, + 0 + ], + "arch": "stairs_wood_up", + "line": 122, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 1, + 10 + ], + "target_map": "/shattered_islands/world_0_70", + "target_tile": [ + 20, + 16 + ], + "arch": "stairs_wood_up", + "line": 165, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 1, + 18 + ], + "target_map": "/shattered_islands/world_9_56", + "target_tile": [ + 17, + 17 + ], + "arch": "stairs_wood_up", + "line": 208, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 6, + 2 + ], + "target_map": "/emergency", + "target_tile": [ + 0, + 0 + ], + "arch": "stairs_wood_up", + "line": 624, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 6, + 10 + ], + "target_map": "/shattered_islands/world_6_78", + "target_tile": [ + 19, + 21 + ], + "arch": "stairs_wood_up", + "line": 668, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 6, + 18 + ], + "target_map": "/shattered_islands/world_2_74", + "target_tile": [ + 9, + 21 + ], + "arch": "stairs_wood_up", + "line": 712, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 12, + 2 + ], + "target_map": "/shattered_islands/world_9_42", + "target_tile": [ + 12, + 2 + ], + "arch": "stairs_wood_up", + "line": 990, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 12, + 10 + ], + "target_map": "/shattered_islands/world_12_68", + "target_tile": [ + 20, + 12 + ], + "arch": "stairs_wood_up", + "line": 1033, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 12, + 18 + ], + "target_map": "/shattered_islands/world_9_41", + "target_tile": [ + 12, + 3 + ], + "arch": "stairs_wood_up", + "line": 1076, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 17, + 2 + ], + "target_map": "/shattered_islands/world_17_3", + "target_tile": [ + 11, + 18 + ], + "arch": "stairs_wood_up", + "line": 1492, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 17, + 10 + ], + "target_map": "/shattered_islands/world_14_64", + "target_tile": [ + 20, + 11 + ], + "arch": "stairs_wood_up", + "line": 1536, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_1", + "source_tile": [ + 17, + 18 + ], + "target_map": "/shattered_islands/world_12_68", + "target_tile": [ + 10, + 22 + ], + "arch": "stairs_wood_up", + "line": 1580, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_2", + "source_tile": [ + 1, + 2 + ], + "target_map": "/shattered_islands/world_2_70", + "target_tile": [ + 5, + 12 + ], + "arch": "stairs_wood_up", + "line": 96, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_2", + "source_tile": [ + 1, + 10 + ], + "target_map": "/shattered_islands/world_15_4", + "target_tile": [ + 15, + 12 + ], + "arch": "stairs_wood_up", + "line": 139, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_2", + "source_tile": [ + 6, + 2 + ], + "target_map": "/shattered_islands/world_6_51", + "target_tile": [ + 16, + 6 + ], + "arch": "stairs_wood_up", + "line": 481, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/ships/ship_2", + "source_tile": [ + 6, + 10 + ], + "target_map": "/shattered_islands/world_18_3", + "target_tile": [ + 6, + 17 + ], + "arch": "stairs_wood_up", + "line": 527, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/apartments/apartment_cheap", + "source_tile": [ + 1, + 1 + ], + "arch": "exit", + "line": 59, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_out.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/apartments/apartment_expensive", + "source_tile": [ + 1, + 1 + ], + "arch": "exit", + "line": 83, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_out.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/apartments/apartment_luxurious", + "source_tile": [ + 1, + 1 + ], + "arch": "exit", + "line": 117, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_out.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/apartments/apartment_normal", + "source_tile": [ + 1, + 1 + ], + "arch": "exit", + "line": 75, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_out.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 2, + 26 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 191, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 2, + 27 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 212, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 3, + 26 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 309, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 3, + 27 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 330, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 3, + 31 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 367, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 3, + 32 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 388, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 3, + 33 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 409, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 4, + 26 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 502, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 4, + 27 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 523, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 4, + 30 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 552, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 4, + 31 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 569, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 4, + 32 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 586, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 4, + 33 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 607, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 5, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 693, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 5, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 714, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 5, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 735, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 5, + 26 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 780, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 5, + 30 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 823, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 5, + 31 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 840, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 5, + 32 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 861, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 5, + 33 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 882, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 6, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 985, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 6, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1006, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 6, + 30 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1157, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 6, + 31 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1178, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 6, + 32 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1199, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 7, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1292, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 7, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1374, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 7, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1391, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 7, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1408, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 8, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1618, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 8, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1635, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 8, + 26 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1674, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 8, + 34 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1731, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 8, + 35 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1752, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 9, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1835, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 9, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1856, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 9, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1877, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 9, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1918, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 9, + 26 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1953, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 9, + 27 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 1974, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 9, + 33 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2015, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 9, + 34 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2032, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 9, + 35 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2049, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 10, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2132, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 10, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2149, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 10, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2166, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 10, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2183, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 10, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2204, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 10, + 26 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2255, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 10, + 27 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2276, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 10, + 33 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2317, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 10, + 34 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2334, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 10, + 35 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2351, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 11, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2442, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 11, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2459, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 11, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2476, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 11, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2497, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 11, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2518, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 11, + 26 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2581, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 11, + 30 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2623, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 12, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2799, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 12, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2820, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 12, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2841, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 12, + 30 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2965, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 12, + 31 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 2986, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 14, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 3332, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 14, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 3353, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 15, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 3506, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 15, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 3523, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 15, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 3544, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 16, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 3714, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 16, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 3731, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 16, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 3752, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 16, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 3799, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 16, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 3820, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 17, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 4046, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 17, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 4067, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 17, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 4108, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 17, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 4125, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 17, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 4142, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 18, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 4358, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 19, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 20, + 43 + ], + "arch": "invis_exit", + "line": 4615, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "source_tile": [ + 21, + 43 + ], + "target_map": "/shattered_islands/world_0_66", + "target_tile": [ + 18, + 19 + ], + "arch": "rope_up", + "line": 5252, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/sewers/lab", + "source_tile": [ + 11, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_0_1", + "target_tile": [ + 23, + 13 + ], + "arch": "exit", + "line": 2090, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/sewers/lab", + "source_tile": [ + 19, + 12 + ], + "target_map": "/shattered_islands/world_4_66_-2", + "target_tile": [ + 0, + 12 + ], + "arch": "exit", + "line": 3596, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/sewers/lab_nyhelobo", + "source_tile": [ + 11, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/sewers/lab_nyhelobo", + "target_tile": [ + 11, + 12 + ], + "arch": "portal_green", + "line": 1777, + "form": "automatic-link", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/brynknot/sewers/lab_nyhelobo", + "source_tile": [ + 19, + 12 + ], + "target_map": "/shattered_islands/world_4_66_-2", + "target_tile": [ + 0, + 12 + ], + "arch": "exit", + "line": 2890, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/dark_cave/dark_cave_0102", + "source_tile": [ + 1, + 0 + ], + "target_map": "/shattered_islands/world_3_70", + "target_tile": [ + 6, + 0 + ], + "arch": "invis_exit", + "line": 165, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/giant_stronghold/giant_stronghold_0201", + "source_tile": [ + 21, + 23 + ], + "target_map": "/shattered_islands/world_0_65", + "target_tile": [ + 17, + 10 + ], + "arch": "invis_exit", + "line": 2999, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/giant_stronghold/giant_stronghold_0201", + "source_tile": [ + 22, + 23 + ], + "target_map": "/shattered_islands/world_0_65", + "target_tile": [ + 18, + 10 + ], + "arch": "invis_exit", + "line": 3112, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/house/luxury_house_0_0", + "source_tile": [ + 8, + 12 + ], + "arch": "invis_exit", + "line": 1549, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/fees_checker.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/house/luxury_house_0_0", + "source_tile": [ + 9, + 12 + ], + "arch": "invis_exit", + "line": 1792, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/fees_checker.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/house/luxury_house_0_0", + "source_tile": [ + 9, + 17 + ], + "arch": "invis_exit", + "line": 1833, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/fees_checker.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/house/luxury_house_0_0", + "source_tile": [ + 9, + 22 + ], + "arch": "invis_exit", + "line": 1895, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/fees_checker.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/house/luxury_house_0_0", + "source_tile": [ + 10, + 22 + ], + "arch": "invis_exit", + "line": 2113, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/fees_checker.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/house/luxury_house_0_0", + "source_tile": [ + 11, + 17 + ], + "arch": "invis_exit", + "line": 2284, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/fees_checker.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/house/luxury_house_0_0", + "source_tile": [ + 11, + 27 + ], + "arch": "exit_green", + "line": 2365, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/exit.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/house/luxury_house_0_0", + "source_tile": [ + 12, + 22 + ], + "arch": "invis_exit", + "line": 2634, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/fees_checker.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/house/luxury_house_0_0", + "source_tile": [ + 13, + 22 + ], + "arch": "invis_exit", + "line": 2887, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/fees_checker.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/house/luxury_house_0_0", + "source_tile": [ + 46, + 26 + ], + "arch": "exit", + "line": 11690, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/exit.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 1, + 19 + ], + "target_map": "/shattered_islands/world_11_67", + "target_tile": [ + 16, + 7 + ], + "arch": "stairs_stone_up", + "line": 351, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 1, + 20 + ], + "target_map": "/shattered_islands/world_11_67", + "target_tile": [ + 16, + 8 + ], + "arch": "stairs_stone_up", + "line": 362, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 2, + 1 + ], + "arch": "ladder_up", + "line": 415, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 2, + 7 + ], + "arch": "ladder_up", + "line": 476, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 2, + 13 + ], + "arch": "ladder_up", + "line": 541, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 12, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/greyton/jail/jail_a", + "target_tile": [ + 5, + 11 + ], + "arch": "stairs_red_down", + "line": 2557, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 13, + 1 + ], + "arch": "ladder_up", + "line": 2619, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 13, + 7 + ], + "arch": "ladder_up", + "line": 2688, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 13, + 13 + ], + "arch": "ladder_up", + "line": 2757, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_tile": [ + 13, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/greyton/jail/jail_a", + "target_tile": [ + 6, + 11 + ], + "arch": "stairs_red_down", + "line": 2806, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail_a", + "source_tile": [ + 1, + 11 + ], + "arch": "exit", + "line": 149, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/generic/jail_exit.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail_a", + "source_tile": [ + 5, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "target_tile": [ + 12, + 19 + ], + "arch": "stairs_red_up", + "line": 518, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail_a", + "source_tile": [ + 6, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "target_tile": [ + 13, + 19 + ], + "arch": "stairs_red_up", + "line": 649, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_0101", + "source_tile": [ + 1, + 1 + ], + "target_map": "/shattered_islands/world_5_57", + "target_tile": [ + 7, + 3 + ], + "arch": "stairs_stone_up", + "line": 181, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_0101", + "source_tile": [ + 2, + 1 + ], + "target_map": "/shattered_islands/world_5_57", + "target_tile": [ + 8, + 3 + ], + "arch": "stairs_stone_up", + "line": 372, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_0101", + "source_tile": [ + 3, + 1 + ], + "target_map": "/shattered_islands/world_5_57", + "target_tile": [ + 9, + 3 + ], + "arch": "stairs_stone_up", + "line": 503, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_0202", + "source_tile": [ + 10, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_a_0202", + "target_tile": [ + 1, + 18 + ], + "arch": "stairs_stone_down", + "line": 1505, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_0202", + "source_tile": [ + 10, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_a_0202", + "target_tile": [ + 1, + 19 + ], + "arch": "stairs_stone_down", + "line": 1517, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_a_0202", + "source_tile": [ + 1, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_0202", + "target_tile": [ + 10, + 18 + ], + "arch": "stairs_stone_up", + "line": 296, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_a_0202", + "source_tile": [ + 1, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_0202", + "target_tile": [ + 10, + 19 + ], + "arch": "stairs_stone_up", + "line": 307, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_a_0203", + "source_tile": [ + 17, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_a_0203", + "target_tile": [ + 21, + 2 + ], + "arch": "exit", + "line": 2595, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_a_0203", + "source_tile": [ + 20, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_a_0203", + "target_tile": [ + 18, + 11 + ], + "arch": "exit", + "line": 3018, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_a_0204", + "source_tile": [ + 8, + 19 + ], + "target_map": "/shattered_islands/world_5_57_-2", + "target_tile": [ + 14, + 14 + ], + "arch": "well_stone", + "line": 778, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "reciprocal well_bucket at /shattered_islands/world_5_57_-2 (14,14)" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 6, + 7 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 6, + 8 + ], + "arch": "stairs_stone_down", + "line": 135, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 6, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 6, + 8 + ], + "arch": "invis_exit", + "line": 154, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 7, + 7 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 7, + 8 + ], + "arch": "stairs_stone_down", + "line": 229, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 7, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 7, + 8 + ], + "arch": "invis_exit", + "line": 242, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 11, + 21 + ], + "target_map": "/shattered_islands/world_-1_57", + "target_tile": [ + 22, + 2 + ], + "arch": "stairs_stone_up", + "line": 941, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 11, + 22 + ], + "arch": "stairs_stone_up", + "line": 954, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 11, + 22 + ], + "target_map": "/shattered_islands/world_-1_57", + "target_tile": [ + 22, + 2 + ], + "arch": "invis_exit", + "line": 959, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 12, + 21 + ], + "target_map": "/shattered_islands/world_-1_57", + "target_tile": [ + 23, + 2 + ], + "arch": "stairs_stone_up", + "line": 1148, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 12, + 22 + ], + "arch": "stairs_stone_up", + "line": 1161, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 12, + 22 + ], + "target_map": "/shattered_islands/world_-1_57", + "target_tile": [ + 23, + 2 + ], + "arch": "invis_exit", + "line": 1166, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 16, + 7 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 16, + 8 + ], + "arch": "stairs_stone_down", + "line": 1727, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 16, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 16, + 8 + ], + "arch": "invis_exit", + "line": 1746, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 17, + 7 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 17, + 8 + ], + "arch": "stairs_stone_down", + "line": 1792, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "source_tile": [ + 17, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 17, + 8 + ], + "arch": "invis_exit", + "line": 1805, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 6, + 7 + ], + "arch": "stairs_stone_up", + "line": 343, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 6, + 7 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 6, + 7 + ], + "arch": "invis_exit", + "line": 348, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 6, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 6, + 7 + ], + "arch": "stairs_stone_up", + "line": 363, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 7, + 7 + ], + "arch": "stairs_stone_up", + "line": 506, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 7, + 7 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 7, + 7 + ], + "arch": "invis_exit", + "line": 511, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 7, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 7, + 7 + ], + "arch": "stairs_stone_up", + "line": 526, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 7, + 19 + ], + "target_map": "/shattered_islands/world_-1_57", + "target_tile": [ + 18, + 2 + ], + "arch": "stairs_stone_up", + "line": 578, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 7, + 20 + ], + "arch": "stairs_stone_up", + "line": 591, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 7, + 20 + ], + "target_map": "/shattered_islands/world_-1_57", + "target_tile": [ + 18, + 2 + ], + "arch": "invis_exit", + "line": 596, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 7, + 21 + ], + "arch": "stairs_stone_up", + "line": 608, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 7, + 22 + ], + "arch": "stairs_stone_up", + "line": 629, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 8, + 19 + ], + "target_map": "/shattered_islands/world_-1_57", + "target_tile": [ + 19, + 2 + ], + "arch": "stairs_stone_up", + "line": 722, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 8, + 20 + ], + "arch": "stairs_stone_up", + "line": 735, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 8, + 20 + ], + "target_map": "/shattered_islands/world_-1_57", + "target_tile": [ + 19, + 2 + ], + "arch": "invis_exit", + "line": 740, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 8, + 21 + ], + "arch": "stairs_stone_up", + "line": 759, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 8, + 22 + ], + "arch": "stairs_stone_up", + "line": 787, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 15, + 19 + ], + "target_map": "/shattered_islands/world_0_57", + "target_tile": [ + 2, + 2 + ], + "arch": "stairs_stone_up", + "line": 1412, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 15, + 20 + ], + "arch": "stairs_stone_up", + "line": 1425, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 15, + 20 + ], + "target_map": "/shattered_islands/world_0_57", + "target_tile": [ + 2, + 2 + ], + "arch": "invis_exit", + "line": 1430, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 15, + 21 + ], + "arch": "stairs_stone_up", + "line": 1442, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 15, + 22 + ], + "arch": "stairs_stone_up", + "line": 1470, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 16, + 7 + ], + "arch": "stairs_stone_up", + "line": 1504, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 16, + 7 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 16, + 7 + ], + "arch": "invis_exit", + "line": 1509, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 16, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 16, + 7 + ], + "arch": "stairs_stone_up", + "line": 1524, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 16, + 19 + ], + "target_map": "/shattered_islands/world_0_57", + "target_tile": [ + 3, + 2 + ], + "arch": "stairs_stone_up", + "line": 1576, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 16, + 20 + ], + "arch": "stairs_stone_up", + "line": 1589, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 16, + 20 + ], + "target_map": "/shattered_islands/world_0_57", + "target_tile": [ + 3, + 2 + ], + "arch": "invis_exit", + "line": 1594, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 16, + 21 + ], + "arch": "stairs_stone_up", + "line": 1613, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 16, + 22 + ], + "arch": "stairs_stone_up", + "line": 1641, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 17, + 7 + ], + "arch": "stairs_stone_up", + "line": 1675, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 17, + 7 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 17, + 7 + ], + "arch": "invis_exit", + "line": 1680, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "source_tile": [ + 17, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 17, + 7 + ], + "arch": "stairs_stone_up", + "line": 1695, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0104", + "source_tile": [ + 12, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_0104", + "target_tile": [ + 12, + 15 + ], + "arch": "invis_exit", + "line": 1397, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0104", + "source_tile": [ + 12, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_0104", + "target_tile": [ + 12, + 15 + ], + "arch": "stairs_stone_down", + "line": 1408, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0104", + "source_tile": [ + 13, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_0104", + "target_tile": [ + 13, + 15 + ], + "arch": "invis_exit", + "line": 1527, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0104", + "source_tile": [ + 13, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_0104", + "target_tile": [ + 13, + 15 + ], + "arch": "stairs_stone_down", + "line": 1538, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_aa04", + "source_tile": [ + 2, + 5 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_aa04", + "target_tile": [ + 2, + 6 + ], + "arch": "stairs_stone_down", + "line": 204, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_aa04", + "source_tile": [ + 2, + 6 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_aa04", + "target_tile": [ + 2, + 6 + ], + "arch": "invis_exit", + "line": 217, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_aa04", + "source_tile": [ + 17, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_aa04", + "target_tile": [ + 17, + 16 + ], + "arch": "exit", + "line": 2704, + "form": "same-map", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_teleport.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_0104", + "source_tile": [ + 12, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0104", + "target_tile": [ + 12, + 16 + ], + "arch": "stairs_stone_up", + "line": 1104, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_0104", + "source_tile": [ + 12, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0104", + "target_tile": [ + 12, + 16 + ], + "arch": "stairs_stone_up", + "line": 1117, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_0104", + "source_tile": [ + 13, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0104", + "target_tile": [ + 13, + 16 + ], + "arch": "stairs_stone_up", + "line": 1209, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_0104", + "source_tile": [ + 13, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0104", + "target_tile": [ + 13, + 16 + ], + "arch": "stairs_stone_up", + "line": 1222, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_aa04", + "source_tile": [ + 2, + 5 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_aa04", + "target_tile": [ + 2, + 5 + ], + "arch": "stairs_stone_up", + "line": 214, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/rockforge/rockforge_b_aa04", + "source_tile": [ + 2, + 6 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_aa04", + "target_tile": [ + 2, + 5 + ], + "arch": "stairs_stone_up", + "line": 228, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_0_0", + "source_tile": [ + 1, + 1 + ], + "target_map": "/shattered_islands/world_5_65", + "target_tile": [ + 2, + 2 + ], + "arch": "stairs_stone_up", + "line": 193, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_0_1", + "source_tile": [ + 22, + 12 + ], + "target_map": "/shattered_islands/world_2_69", + "target_tile": [ + 0, + 7 + ], + "arch": "portal_green", + "line": 2972, + "form": "explicit", + "vertical": "none", + "event_scripts": [ + "scripts/lake_portal.py" + ] + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_0_1_-2", + "source_tile": [ + 22, + 21 + ], + "target_map": "/shattered_islands/world_6_67_-1", + "target_tile": [ + 7, + 9 + ], + "arch": "portal_yellow", + "line": 3718, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-2", + "source_tile": [ + 4, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-3", + "target_tile": [ + 4, + 1 + ], + "arch": "stairs_down", + "line": 267, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-2", + "source_tile": [ + 4, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-3", + "target_tile": [ + 4, + 2 + ], + "arch": "stairs_down", + "line": 275, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-2", + "source_tile": [ + 5, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-3", + "target_tile": [ + 4, + 1 + ], + "arch": "stairs_stone_down", + "line": 336, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-2", + "source_tile": [ + 5, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-3", + "target_tile": [ + 4, + 2 + ], + "arch": "stairs_stone_down", + "line": 345, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-3", + "source_tile": [ + 4, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-2", + "target_tile": [ + 5, + 1 + ], + "arch": "stairs_stone_up", + "line": 414, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-3", + "source_tile": [ + 4, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-2", + "target_tile": [ + 5, + 2 + ], + "arch": "stairs_stone_up", + "line": 423, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-3", + "source_tile": [ + 5, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-2", + "target_tile": [ + 5, + 1 + ], + "arch": "stairs_stone_up2", + "line": 518, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-3", + "source_tile": [ + 5, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_1_-2_-2", + "target_tile": [ + 5, + 2 + ], + "arch": "stairs_stone_up2", + "line": 527, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-1", + "source_tile": [ + 17, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-2", + "target_tile": [ + 18, + 2 + ], + "arch": "stairs_stone_down", + "line": 2656, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-1", + "source_tile": [ + 17, + 3 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-2", + "target_tile": [ + 18, + 3 + ], + "arch": "stairs_stone_down", + "line": 2664, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-1", + "source_tile": [ + 18, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-2", + "target_tile": [ + 18, + 2 + ], + "arch": "stairs_down", + "line": 2803, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-1", + "source_tile": [ + 18, + 3 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-2", + "target_tile": [ + 18, + 3 + ], + "arch": "stairs_down", + "line": 2811, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-2", + "source_tile": [ + 2, + 20 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-2", + "target_tile": [ + 11, + 20 + ], + "arch": "exit_green", + "line": 443, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-2", + "source_tile": [ + 17, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-1", + "target_tile": [ + 17, + 2 + ], + "arch": "stairs_stone_up2", + "line": 3605, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-2", + "source_tile": [ + 17, + 3 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-1", + "target_tile": [ + 17, + 3 + ], + "arch": "stairs_stone_up2", + "line": 3613, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-2", + "source_tile": [ + 18, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-1", + "target_tile": [ + 17, + 2 + ], + "arch": "stairs_stone_up", + "line": 3751, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-2", + "source_tile": [ + 18, + 3 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_2_3_-1", + "target_tile": [ + 17, + 3 + ], + "arch": "stairs_stone_up", + "line": 3759, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_3_3_-2", + "source_tile": [ + 0, + 2 + ], + "target_map": "/planes/bone_cairn/bone_cairn_0_0", + "target_tile": [ + 10, + 15 + ], + "arch": "exit", + "line": 33, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4", + "source_tile": [ + 13, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4_-1", + "target_tile": [ + 13, + 16 + ], + "arch": "stairs_down", + "line": 1679, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4", + "source_tile": [ + 13, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4_-1", + "target_tile": [ + 13, + 17 + ], + "arch": "stairs_down", + "line": 1693, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4", + "source_tile": [ + 14, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4_-1", + "target_tile": [ + 13, + 16 + ], + "arch": "stairs_stone_down", + "line": 1815, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4", + "source_tile": [ + 14, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4_-1", + "target_tile": [ + 13, + 17 + ], + "arch": "stairs_stone_down", + "line": 1824, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4_-1", + "source_tile": [ + 13, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4", + "target_tile": [ + 14, + 16 + ], + "arch": "stairs_stone_up", + "line": 1604, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4_-1", + "source_tile": [ + 13, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4", + "target_tile": [ + 14, + 17 + ], + "arch": "stairs_stone_up", + "line": 1613, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4_-1", + "source_tile": [ + 14, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4", + "target_tile": [ + 14, + 16 + ], + "arch": "stairs_stone_up2", + "line": 1721, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4_-1", + "source_tile": [ + 14, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_4_4", + "target_tile": [ + 14, + 17 + ], + "arch": "stairs_stone_up2", + "line": 1734, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_5_2_-1", + "source_tile": [ + 2, + 3 + ], + "target_map": "/shattered_islands/world_4_67_-1", + "target_tile": [ + 7, + 17 + ], + "arch": "stairs_stone_up", + "line": 192, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/underground_city/underground_city_5_3_-1", + "source_tile": [ + 3, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_5_3_-1", + "target_tile": [ + 4, + 8 + ], + "arch": "exit_green", + "line": 247, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_0", + "source_tile": [ + 7, + 7 + ], + "target_map": "/shattered_islands/world_7_67_-1", + "target_tile": [ + 16, + 9 + ], + "arch": "exit_green", + "line": 1011, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_0_-3", + "source_tile": [ + 4, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-3", + "target_tile": [ + 17, + 22 + ], + "arch": "exit_green", + "line": 433, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "source_tile": [ + 3, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "target_tile": [ + 3, + 0 + ], + "arch": "stairs_down", + "line": 311, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "source_tile": [ + 3, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "target_tile": [ + 3, + 0 + ], + "arch": "stairs_stone_down", + "line": 318, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "source_tile": [ + 4, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "target_tile": [ + 4, + 0 + ], + "arch": "stairs_down", + "line": 379, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "source_tile": [ + 4, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "target_tile": [ + 4, + 0 + ], + "arch": "stairs_stone_down", + "line": 386, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "source_tile": [ + 5, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "target_tile": [ + 5, + 0 + ], + "arch": "stairs_down", + "line": 446, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "source_tile": [ + 5, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "target_tile": [ + 5, + 0 + ], + "arch": "stairs_stone_down", + "line": 453, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "source_tile": [ + 3, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "target_tile": [ + 3, + 1 + ], + "arch": "stairs_stone_up", + "line": 245, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "source_tile": [ + 3, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "target_tile": [ + 3, + 1 + ], + "arch": "stairs_stone_up2", + "line": 253, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "source_tile": [ + 4, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "target_tile": [ + 4, + 1 + ], + "arch": "stairs_stone_up", + "line": 401, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "source_tile": [ + 4, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "target_tile": [ + 4, + 1 + ], + "arch": "stairs_stone_up2", + "line": 412, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "source_tile": [ + 5, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "target_tile": [ + 5, + 1 + ], + "arch": "stairs_stone_up", + "line": 544, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-3", + "source_tile": [ + 5, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_1_-2", + "target_tile": [ + 5, + 1 + ], + "arch": "stairs_stone_up2", + "line": 552, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-1", + "source_tile": [ + 2, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-2", + "target_tile": [ + 2, + 1 + ], + "arch": "stairs_down", + "line": 293, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-1", + "source_tile": [ + 3, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-2", + "target_tile": [ + 2, + 1 + ], + "arch": "stairs_stone_down", + "line": 437, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-2", + "source_tile": [ + 2, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-1", + "target_tile": [ + 3, + 1 + ], + "arch": "stairs_stone_up", + "line": 377, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-2", + "source_tile": [ + 3, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-1", + "target_tile": [ + 3, + 1 + ], + "arch": "stairs_stone_up2", + "line": 509, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-3", + "source_tile": [ + 17, + 21 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_0_-3", + "target_tile": [ + 4, + 3 + ], + "arch": "grave_cross2.111", + "line": 2634, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1", + "source_tile": [ + 0, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1_-1", + "target_tile": [ + 0, + 19 + ], + "arch": "stairs_stone_down", + "line": 66, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1", + "source_tile": [ + 0, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1_-1", + "target_tile": [ + 0, + 19 + ], + "arch": "stairs_down", + "line": 73, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1", + "source_tile": [ + 1, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1_-1", + "target_tile": [ + 1, + 19 + ], + "arch": "stairs_stone_down", + "line": 172, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1", + "source_tile": [ + 1, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1_-1", + "target_tile": [ + 1, + 19 + ], + "arch": "stairs_down", + "line": 181, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1_-1", + "source_tile": [ + 0, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1", + "target_tile": [ + 0, + 18 + ], + "arch": "stairs_stone_up2", + "line": 88, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1_-1", + "source_tile": [ + 0, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1", + "target_tile": [ + 0, + 18 + ], + "arch": "stairs_stone_up", + "line": 95, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1_-1", + "source_tile": [ + 1, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1", + "target_tile": [ + 1, + 18 + ], + "arch": "stairs_stone_up2", + "line": 196, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1_-1", + "source_tile": [ + 1, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1", + "target_tile": [ + 1, + 18 + ], + "arch": "stairs_stone_up", + "line": 205, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_-10_54", + "source_tile": [ + 14, + 12 + ], + "arch": "hole", + "line": 2097, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_-10_54", + "source_tile": [ + 15, + 6 + ], + "arch": "stairs_stone_down", + "line": 2207, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_-1_44", + "source_tile": [ + 10, + 6 + ], + "arch": "stairs_stone_up", + "line": 1592, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/world_-1_44", + "source_tile": [ + 10, + 7 + ], + "arch": "stairs_stone_up", + "line": 1607, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/world_-1_44", + "source_tile": [ + 11, + 6 + ], + "arch": "stairs_stone_up", + "line": 1733, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/world_-1_44", + "source_tile": [ + 11, + 7 + ], + "arch": "stairs_stone_up", + "line": 1748, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/world_-1_57", + "source_tile": [ + 18, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 7, + 19 + ], + "arch": "invis_exit", + "line": 2800, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/world_-1_57", + "source_tile": [ + 18, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 7, + 19 + ], + "arch": "stairs_stone_down", + "line": 2811, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_-1_57", + "source_tile": [ + 19, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 8, + 19 + ], + "arch": "invis_exit", + "line": 2950, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/world_-1_57", + "source_tile": [ + 19, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 8, + 19 + ], + "arch": "stairs_stone_down", + "line": 2961, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_-1_57", + "source_tile": [ + 22, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 11, + 21 + ], + "arch": "invis_exit", + "line": 3405, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/world_-1_57", + "source_tile": [ + 22, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 11, + 21 + ], + "arch": "stairs_stone_down", + "line": 3416, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_-1_57", + "source_tile": [ + 23, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 12, + 21 + ], + "arch": "invis_exit", + "line": 3547, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/world_-1_57", + "source_tile": [ + 23, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_0101", + "target_tile": [ + 12, + 21 + ], + "arch": "stairs_stone_down", + "line": 3558, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_-4_56_-1", + "source_tile": [ + 14, + 8 + ], + "target_map": "/shattered_islands/world_-4_56_-2", + "target_tile": [ + 14, + 8 + ], + "arch": "hole", + "line": 2495, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_-4_56_-2", + "source_tile": [ + 17, + 23 + ], + "target_map": "/shattered_islands/world_-5_56_-1", + "target_tile": [ + 12, + 7 + ], + "arch": "exit", + "line": 1537, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-5_56_-1", + "source_tile": [ + 9, + 11 + ], + "target_map": "/shattered_islands/world_-5_56_-2", + "target_tile": [ + 9, + 11 + ], + "arch": "invis_exit", + "line": 1648, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-5_56_-1", + "source_tile": [ + 9, + 12 + ], + "target_map": "/shattered_islands/world_-5_56_-2", + "target_tile": [ + 9, + 12 + ], + "arch": "invis_exit", + "line": 1666, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-5_56_-1", + "source_tile": [ + 20, + 11 + ], + "target_map": "/shattered_islands/world_-5_56_-2", + "target_tile": [ + 20, + 11 + ], + "arch": "invis_exit", + "line": 3712, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-5_56_-1", + "source_tile": [ + 20, + 12 + ], + "target_map": "/shattered_islands/world_-5_56_-2", + "target_tile": [ + 20, + 12 + ], + "arch": "invis_exit", + "line": 3730, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-5_56_-2", + "source_tile": [ + 8, + 12 + ], + "target_map": "/shattered_islands/world_-5_56_-1", + "target_tile": [ + 8, + 12 + ], + "arch": "invis_exit", + "line": 56, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-5_56_-2", + "source_tile": [ + 8, + 13 + ], + "target_map": "/shattered_islands/world_-5_56_-1", + "target_tile": [ + 8, + 13 + ], + "arch": "invis_exit", + "line": 74, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-5_56_-2", + "source_tile": [ + 21, + 11 + ], + "target_map": "/shattered_islands/world_-5_56_-1", + "target_tile": [ + 21, + 11 + ], + "arch": "invis_exit", + "line": 1058, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-5_56_-2", + "source_tile": [ + 21, + 12 + ], + "target_map": "/shattered_islands/world_-5_56_-1", + "target_tile": [ + 21, + 12 + ], + "arch": "invis_exit", + "line": 1076, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-5_76", + "source_tile": [ + 12, + 20 + ], + "target_map": "/shattered_islands/deserted_tutorial_island/mushroom_cavern", + "target_tile": [ + 10, + 3 + ], + "arch": "invis_exit", + "line": 2436, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_-5_76", + "source_tile": [ + 12, + 21 + ], + "target_map": "/shattered_islands/deserted_tutorial_island/mushroom_cavern", + "target_tile": [ + 10, + 3 + ], + "arch": "invis_exit", + "line": 2454, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_-5_76", + "source_tile": [ + 12, + 22 + ], + "target_map": "/shattered_islands/deserted_tutorial_island/mushroom_cavern", + "target_tile": [ + 10, + 3 + ], + "arch": "invis_exit", + "line": 2472, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_-6_56_-1", + "source_tile": [ + 7, + 17 + ], + "arch": "hole", + "line": 1066, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_-6_58", + "source_tile": [ + 7, + 23 + ], + "target_map": "/shattered_islands/world_-6_58_-1", + "target_tile": [ + 19, + 22 + ], + "arch": "exit", + "line": 1201, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-6_58_-1", + "source_tile": [ + 19, + 23 + ], + "target_map": "/shattered_islands/world_-6_58", + "target_tile": [ + 5, + 20 + ], + "arch": "invis_exit", + "line": 3170, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-6_58_-1", + "source_tile": [ + 20, + 23 + ], + "target_map": "/shattered_islands/world_-6_58", + "target_tile": [ + 5, + 20 + ], + "arch": "invis_exit", + "line": 3377, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-6_79", + "source_tile": [ + 7, + 7 + ], + "target_map": "/shattered_islands/incuna/ship_lower_deck", + "target_tile": [ + 2, + 2 + ], + "arch": "stairs_wood_down", + "line": 1139, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_-7_54", + "source_tile": [ + 7, + 10 + ], + "target_map": "/shattered_islands/world_-7_54_-1", + "target_tile": [ + 8, + 10 + ], + "arch": "invis_exit", + "line": 1105, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-7_54_-1", + "source_tile": [ + 7, + 10 + ], + "target_map": "/shattered_islands/world_-7_54", + "target_tile": [ + 8, + 10 + ], + "arch": "invis_exit", + "line": 921, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_-7_76", + "source_tile": [ + 8, + 14 + ], + "arch": "stairs_wood_down", + "line": 1192, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_-7_76", + "source_tile": [ + 9, + 18 + ], + "arch": "hole2", + "line": 1456, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_-7_76", + "source_tile": [ + 10, + 11 + ], + "arch": "hole2", + "line": 1563, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_-7_76", + "source_tile": [ + 11, + 12 + ], + "arch": "hole2", + "line": 1747, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_51", + "source_tile": [ + 10, + 21 + ], + "target_map": "/shattered_islands/world_0_51_1", + "target_tile": [ + 10, + 21 + ], + "arch": "ladder_tall_up", + "line": 1411, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_51", + "source_tile": [ + 18, + 23 + ], + "target_map": "/shattered_islands/world_0_51_1", + "target_tile": [ + 18, + 23 + ], + "arch": "ladder_tall_up", + "line": 2485, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_51_1", + "source_tile": [ + 10, + 21 + ], + "target_map": "/shattered_islands/world_0_51", + "target_tile": [ + 10, + 21 + ], + "arch": "ladder_down2", + "line": 83, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_51_1", + "source_tile": [ + 12, + 20 + ], + "arch": "ladder_up", + "line": 174, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_51_1", + "source_tile": [ + 18, + 23 + ], + "target_map": "/shattered_islands/world_0_51", + "target_tile": [ + 18, + 23 + ], + "arch": "ladder_down2", + "line": 286, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_51_1", + "source_tile": [ + 20, + 22 + ], + "arch": "ladder_up", + "line": 353, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_52", + "source_tile": [ + 4, + 16 + ], + "target_map": "/shattered_islands/world_0_52", + "target_tile": [ + 4, + 16 + ], + "arch": "shop_mat", + "line": 790, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_0_52", + "source_tile": [ + 6, + 16 + ], + "target_map": "/shattered_islands/world_0_52", + "target_tile": [ + 6, + 16 + ], + "arch": "shop_mat", + "line": 1231, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_0_52", + "source_tile": [ + 10, + 3 + ], + "target_map": "/shattered_islands/world_0_52_-1", + "target_tile": [ + 11, + 4 + ], + "arch": "stairs_wood_down", + "line": 1788, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_52", + "source_tile": [ + 10, + 12 + ], + "target_map": "/shattered_islands/world_0_52_1", + "target_tile": [ + 11, + 11 + ], + "arch": "stairs_wood_up", + "line": 1839, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_52", + "source_tile": [ + 11, + 3 + ], + "target_map": "/shattered_islands/world_0_52_-1", + "target_tile": [ + 11, + 4 + ], + "arch": "stairs_down", + "line": 1948, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_52", + "source_tile": [ + 11, + 12 + ], + "target_map": "/shattered_islands/world_0_52_1", + "target_tile": [ + 11, + 11 + ], + "arch": "stairs_wood_up2", + "line": 1999, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_52_-1", + "source_tile": [ + 11, + 3 + ], + "target_map": "/shattered_islands/world_0_52", + "target_tile": [ + 10, + 3 + ], + "arch": "stairs_wood_up2", + "line": 1212, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_52_-1", + "source_tile": [ + 11, + 4 + ], + "target_map": "/shattered_islands/world_0_52", + "target_tile": [ + 10, + 3 + ], + "arch": "stairs_wood_up", + "line": 1228, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_52_1", + "source_tile": [ + 11, + 11 + ], + "target_map": "/shattered_islands/world_0_52", + "target_tile": [ + 10, + 12 + ], + "arch": "stairs_wood_down", + "line": 1473, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_52_1", + "source_tile": [ + 11, + 12 + ], + "target_map": "/shattered_islands/world_0_52", + "target_tile": [ + 10, + 12 + ], + "arch": "stairs_down", + "line": 1485, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_57", + "source_tile": [ + 2, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 15, + 19 + ], + "arch": "invis_exit", + "line": 319, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/world_0_57", + "source_tile": [ + 2, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 15, + 19 + ], + "arch": "stairs_stone_down", + "line": 330, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_57", + "source_tile": [ + 3, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 16, + 19 + ], + "arch": "invis_exit", + "line": 474, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/world_0_57", + "source_tile": [ + 3, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/rockforge/rockforge_a_0101", + "target_tile": [ + 16, + 19 + ], + "arch": "stairs_stone_down", + "line": 485, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_65", + "source_tile": [ + 17, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/giant_stronghold/giant_stronghold_0201", + "target_tile": [ + 21, + 22 + ], + "arch": "invis_exit", + "line": 2258, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 6, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 902, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 6, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 919, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 6, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 936, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 7, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1055, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 7, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1072, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 7, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1085, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 7, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1102, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 7, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1119, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 8, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1227, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 8, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1240, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 8, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1253, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 8, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1266, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 8, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1283, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 8, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1300, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 9, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1427, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 9, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1444, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 9, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1457, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 9, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1470, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 9, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1483, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 9, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1500, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 9, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1517, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 10, + 10 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1640, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 10, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1657, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 10, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1670, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 10, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1683, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 10, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1696, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 10, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1713, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 10, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1730, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 11, + 11 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1838, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 11, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1855, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 11, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1868, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 11, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1881, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 11, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1894, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 11, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1911, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 11, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 1928, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 12, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2040, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 12, + 13 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2057, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 12, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2074, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 12, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2087, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 12, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2100, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 12, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2117, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 12, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2134, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 13, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2249, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 13, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2266, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 13, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2279, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 13, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2292, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 13, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2309, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 13, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2326, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 14, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2455, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 14, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2472, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 14, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2485, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 14, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2498, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 14, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2515, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 14, + 20 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2532, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 15, + 16 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2656, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 15, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2673, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 15, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2686, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 15, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2699, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 15, + 20 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2716, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 15, + 21 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2733, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 16, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2863, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 16, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2880, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 16, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2893, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 16, + 20 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2906, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 16, + 21 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2919, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 16, + 22 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2936, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 17, + 18 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3048, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 17, + 19 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3065, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 17, + 20 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3082, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 17, + 21 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3095, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 17, + 22 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3108, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 17, + 23 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3130, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 18, + 20 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3247, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 18, + 21 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3264, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 18, + 22 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3277, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 18, + 23 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3294, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 19, + 21 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3435, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "vertical_inferred_from": "reciprocal rope_up at /shattered_islands/strakewood_island/brynknot/abyss/abyss (21,43)" + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 19, + 22 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3452, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 19, + 23 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3465, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 20, + 22 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3615, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 20, + 23 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3632, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_66", + "source_tile": [ + 21, + 23 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3764, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 8, + 16 + ], + "target_map": "/shattered_islands/world_0_67_1", + "target_tile": [ + 8, + 16 + ], + "arch": "stairs_wood_up2", + "line": 1386, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 8, + 17 + ], + "target_map": "/shattered_islands/world_0_67_1", + "target_tile": [ + 8, + 16 + ], + "arch": "stairs_wood_up", + "line": 1400, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 18, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 2866, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 19, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3032, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 19, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3048, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 20, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3205, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 20, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3221, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 20, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3238, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 20, + 3 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3255, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 21, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3421, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 21, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3433, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 21, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3450, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 21, + 3 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3467, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 22, + 0 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3616, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 22, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3632, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67", + "source_tile": [ + 22, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/abyss/abyss", + "target_tile": [ + 21, + 43 + ], + "arch": "invis_exit", + "line": 3649, + "form": "explicit", + "vertical": "none", + "per_player_last_eat_1": true + }, + { + "source_map": "/shattered_islands/world_0_67_1", + "source_tile": [ + 8, + 16 + ], + "target_map": "/shattered_islands/world_0_67", + "target_tile": [ + 8, + 17 + ], + "arch": "stairs_wood_down", + "line": 98, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_67_1", + "source_tile": [ + 8, + 17 + ], + "target_map": "/shattered_islands/world_0_67", + "target_tile": [ + 8, + 17 + ], + "arch": "stairs_down", + "line": 107, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_67_1", + "source_tile": [ + 20, + 16 + ], + "target_map": "/shattered_islands/world_0_67_2", + "target_tile": [ + 20, + 16 + ], + "arch": "ladder_up", + "line": 821, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_67_2", + "source_tile": [ + 20, + 16 + ], + "target_map": "/shattered_islands/world_0_67_1", + "target_tile": [ + 20, + 16 + ], + "arch": "ladder_down2", + "line": 827, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_68", + "source_tile": [ + 4, + 9 + ], + "target_map": "/shattered_islands/world_0_68", + "target_tile": [ + 4, + 8 + ], + "arch": "exit", + "line": 629, + "form": "same-map", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_teleport.py" + ] + }, + { + "source_map": "/shattered_islands/world_0_68", + "source_tile": [ + 10, + 21 + ], + "target_map": "/shattered_islands/world_0_68_-1", + "target_tile": [ + 10, + 21 + ], + "arch": "ladder_down3", + "line": 1510, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_68", + "source_tile": [ + 16, + 9 + ], + "target_map": "/shattered_islands/world_0_68_1", + "target_tile": [ + 16, + 9 + ], + "arch": "stairs_stone_up2", + "line": 2348, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_68", + "source_tile": [ + 16, + 10 + ], + "target_map": "/shattered_islands/world_0_68_1", + "target_tile": [ + 16, + 9 + ], + "arch": "stairs_stone_up", + "line": 2357, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_68", + "source_tile": [ + 17, + 9 + ], + "target_map": "/shattered_islands/world_0_68_1", + "target_tile": [ + 17, + 9 + ], + "arch": "stairs_stone_up2", + "line": 2521, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_68", + "source_tile": [ + 17, + 10 + ], + "target_map": "/shattered_islands/world_0_68_1", + "target_tile": [ + 17, + 9 + ], + "arch": "stairs_stone_up", + "line": 2530, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_68", + "source_tile": [ + 21, + 7 + ], + "target_map": "/shattered_islands/world_0_68_1", + "target_tile": [ + 21, + 7 + ], + "arch": "ladder_up", + "line": 3257, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_68", + "source_tile": [ + 21, + 9 + ], + "target_map": "/shattered_islands/world_0_68_1", + "target_tile": [ + 21, + 9 + ], + "arch": "ladder_up", + "line": 3303, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_68", + "source_tile": [ + 21, + 11 + ], + "target_map": "/shattered_islands/world_0_68_1", + "target_tile": [ + 21, + 11 + ], + "arch": "ladder_up", + "line": 3339, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_68", + "source_tile": [ + 21, + 13 + ], + "target_map": "/shattered_islands/world_0_68_1", + "target_tile": [ + 21, + 13 + ], + "arch": "ladder_up", + "line": 3375, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_68_-1", + "source_tile": [ + 10, + 21 + ], + "target_map": "/shattered_islands/world_0_68", + "target_tile": [ + 10, + 21 + ], + "arch": "ladder_up", + "line": 676, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 15, + 13 + ], + "arch": "ladder_up", + "line": 372, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 15, + 16 + ], + "arch": "ladder_up", + "line": 438, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 16, + 9 + ], + "target_map": "/shattered_islands/world_0_68", + "target_tile": [ + 16, + 10 + ], + "arch": "stairs_stone_down", + "line": 480, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 16, + 10 + ], + "target_map": "/shattered_islands/world_0_68", + "target_tile": [ + 16, + 10 + ], + "arch": "stairs_down", + "line": 489, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 17, + 9 + ], + "target_map": "/shattered_islands/world_0_68", + "target_tile": [ + 17, + 10 + ], + "arch": "stairs_stone_down", + "line": 567, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 17, + 10 + ], + "target_map": "/shattered_islands/world_0_68", + "target_tile": [ + 17, + 10 + ], + "arch": "stairs_down", + "line": 576, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 17, + 13 + ], + "arch": "ladder_up", + "line": 636, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 17, + 16 + ], + "arch": "ladder_up", + "line": 697, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 19, + 16 + ], + "arch": "ladder_up", + "line": 907, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 21, + 16 + ], + "arch": "ladder_up", + "line": 1108, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 23, + 13 + ], + "arch": "ladder_up", + "line": 1321, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_68_1", + "source_tile": [ + 23, + 16 + ], + "arch": "ladder_up", + "line": 1382, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_69", + "source_tile": [ + 15, + 9 + ], + "target_map": "/shattered_islands/world_0_69", + "target_tile": [ + 15, + 9 + ], + "arch": "shop_mat", + "line": 2534, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_0_69", + "source_tile": [ + 15, + 11 + ], + "target_map": "/shattered_islands/world_0_69", + "target_tile": [ + 15, + 11 + ], + "arch": "shop_mat", + "line": 2550, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_0_69_-1", + "source_tile": [ + 15, + 1 + ], + "target_map": "/shattered_islands/world_0_69_-1", + "target_tile": [ + 15, + 1 + ], + "arch": "sewer_hole2", + "line": 1158, + "form": "same-map", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_69_-1", + "source_tile": [ + 15, + 11 + ], + "target_map": "/shattered_islands/world_0_69_-2", + "target_tile": [ + 15, + 11 + ], + "arch": "sewer_hole2", + "line": 1263, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_69_-1", + "source_tile": [ + 23, + 1 + ], + "target_map": "/shattered_islands/world_0_69_-1", + "target_tile": [ + 23, + 1 + ], + "arch": "sewer_hole2", + "line": 2449, + "form": "same-map", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_69_-1", + "source_tile": [ + 23, + 11 + ], + "target_map": "/shattered_islands/world_0_69_-1", + "target_tile": [ + 23, + 11 + ], + "arch": "sewer_hole2", + "line": 2550, + "form": "same-map", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_69_-2", + "source_tile": [ + 15, + 1 + ], + "target_map": "/shattered_islands/world_0_69_-1", + "target_tile": [ + 15, + 1 + ], + "arch": "ladder_up", + "line": 1598, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_69_-2", + "source_tile": [ + 15, + 11 + ], + "target_map": "/shattered_islands/world_0_69_-1", + "target_tile": [ + 15, + 11 + ], + "arch": "ladder_up", + "line": 1656, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_69_-2", + "source_tile": [ + 23, + 1 + ], + "target_map": "/shattered_islands/world_0_69_-1", + "target_tile": [ + 23, + 1 + ], + "arch": "ladder_up", + "line": 2650, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_69_-2", + "source_tile": [ + 23, + 11 + ], + "target_map": "/shattered_islands/world_0_69_-1", + "target_tile": [ + 23, + 11 + ], + "arch": "ladder_up", + "line": 2716, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_70", + "source_tile": [ + 1, + 13 + ], + "target_map": "/shattered_islands/world_0_70", + "target_tile": [ + 1, + 13 + ], + "arch": "stairs_wood_down", + "line": 255, + "form": "same-map", + "vertical": "down", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_0_70", + "source_tile": [ + 20, + 16 + ], + "target_map": "/shattered_islands/ships/ship_1", + "target_tile": [ + 1, + 10 + ], + "arch": "stairs_wood_down", + "line": 2835, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_71_-2", + "source_tile": [ + 1, + 21 + ], + "target_map": "/shattered_islands/world_0_71_-3", + "target_tile": [ + 1, + 22 + ], + "arch": "stairs_stone_down", + "line": 330, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_71_-2", + "source_tile": [ + 1, + 22 + ], + "target_map": "/shattered_islands/world_0_71_-3", + "target_tile": [ + 1, + 22 + ], + "arch": "stairs_down", + "line": 339, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_0_71_-3", + "source_tile": [ + 1, + 21 + ], + "target_map": "/shattered_islands/world_0_71_-2", + "target_tile": [ + 1, + 21 + ], + "arch": "stairs_stone_up2", + "line": 58, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_71_-3", + "source_tile": [ + 1, + 22 + ], + "target_map": "/shattered_islands/world_0_71_-2", + "target_tile": [ + 1, + 21 + ], + "arch": "stairs_stone_up", + "line": 73, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_0_83", + "source_tile": [ + 8, + 16 + ], + "target_map": "/shattered_islands/incuna/ship_lower_deck_to_strakewood", + "target_tile": [ + 2, + 11 + ], + "arch": "stairs_wood_down", + "line": 1452, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_10_42_1", + "source_tile": [ + 2, + 19 + ], + "target_map": "/shattered_islands/world_10_42_2", + "target_tile": [ + 2, + 20 + ], + "arch": "stairs_wood_up", + "line": 163, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_10_42_1", + "source_tile": [ + 2, + 20 + ], + "target_map": "/shattered_islands/world_10_42_2", + "target_tile": [ + 2, + 20 + ], + "arch": "stairs_wood_up2", + "line": 172, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_10_42_2", + "source_tile": [ + 2, + 19 + ], + "target_map": "/shattered_islands/world_10_42_1", + "target_tile": [ + 2, + 19 + ], + "arch": "stairs_down", + "line": 168, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_10_42_2", + "source_tile": [ + 2, + 20 + ], + "target_map": "/shattered_islands/world_10_42_1", + "target_tile": [ + 2, + 19 + ], + "arch": "stairs_wood_down", + "line": 176, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_10_43", + "source_tile": [ + 19, + 2 + ], + "target_map": "/shattered_islands/world_10_43_-2", + "target_tile": [ + 19, + 2 + ], + "arch": "invis_exit", + "line": 2878, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "co-located stairs_stone_down" + }, + { + "source_map": "/shattered_islands/world_10_43", + "source_tile": [ + 20, + 2 + ], + "target_map": "/shattered_islands/world_10_43_-2", + "target_tile": [ + 19, + 2 + ], + "arch": "stairs_stone_down", + "line": 3024, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_10_43_-2", + "source_tile": [ + 19, + 2 + ], + "target_map": "/shattered_islands/world_10_43", + "target_tile": [ + 20, + 2 + ], + "arch": "stairs_stone_up", + "line": 696, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_10_43_-2", + "source_tile": [ + 20, + 2 + ], + "arch": "stairs_stone_up", + "line": 737, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/world_10_43_-2", + "source_tile": [ + 20, + 2 + ], + "target_map": "/shattered_islands/world_10_43", + "target_tile": [ + 20, + 2 + ], + "arch": "invis_exit", + "line": 742, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "co-located stairs_stone_up" + }, + { + "source_map": "/shattered_islands/world_10_44", + "source_tile": [ + 1, + 20 + ], + "target_map": "/shattered_islands/world_10_44", + "target_tile": [ + 1, + 20 + ], + "arch": "shop_mat", + "line": 275, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_10_44", + "source_tile": [ + 3, + 20 + ], + "target_map": "/shattered_islands/world_10_44", + "target_tile": [ + 3, + 20 + ], + "arch": "shop_mat", + "line": 617, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_10_67", + "source_tile": [ + 1, + 9 + ], + "target_map": "/shattered_islands/world_10_67", + "target_tile": [ + 1, + 9 + ], + "arch": "shop_mat", + "line": 221, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_10_67", + "source_tile": [ + 1, + 11 + ], + "target_map": "/shattered_islands/world_10_67", + "target_tile": [ + 1, + 11 + ], + "arch": "shop_mat", + "line": 241, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_10_68", + "source_tile": [ + 0, + 0 + ], + "arch": "exit", + "line": 19, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "scripts/strakewood_island/greyton/arena_exit.py" + ] + }, + { + "source_map": "/shattered_islands/world_10_68", + "source_tile": [ + 2, + 0 + ], + "target_map": "/shattered_islands/world_11_67", + "target_tile": [ + 1, + 11 + ], + "arch": "exit", + "line": 232, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_10_68", + "source_tile": [ + 11, + 11 + ], + "target_map": "/shattered_islands/world_11_67", + "target_tile": [ + 5, + 5 + ], + "arch": "exit", + "line": 1447, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_10_69", + "source_tile": [ + 12, + 15 + ], + "arch": "exit", + "line": 2016, + "form": "unresolved", + "vertical": "none", + "event_scripts": [ + "/python/houses/exit.py" + ] + }, + { + "source_map": "/shattered_islands/world_11_61", + "source_tile": [ + 4, + 10 + ], + "target_map": "/shattered_islands/berri_lur/temple_loki/tol_0201", + "target_tile": [ + 4, + 10 + ], + "arch": "stairs_stone_down", + "line": 668, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_11_67", + "source_tile": [ + 1, + 12 + ], + "target_map": "/shattered_islands/world_11_67", + "target_tile": [ + 1, + 11 + ], + "arch": "exit", + "line": 232, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_11_67", + "source_tile": [ + 2, + 7 + ], + "target_map": "/shattered_islands/world_11_67", + "target_tile": [ + 3, + 7 + ], + "arch": "exit", + "line": 363, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_11_67", + "source_tile": [ + 3, + 9 + ], + "arch": "stairs_stone_down", + "line": 540, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_11_67", + "source_tile": [ + 4, + 5 + ], + "target_map": "/shattered_islands/world_10_68", + "target_tile": [ + 12, + 12 + ], + "arch": "exit", + "line": 673, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_11_67", + "source_tile": [ + 10, + 12 + ], + "target_map": "/shattered_islands/world_10_68", + "target_tile": [ + 12, + 16 + ], + "arch": "exit", + "line": 1633, + "form": "explicit", + "vertical": "none", + "event_scripts": [ + "scripts/strakewood_island/greyton/greyton.arena" + ] + }, + { + "source_map": "/shattered_islands/world_11_67", + "source_tile": [ + 16, + 7 + ], + "target_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "target_tile": [ + 1, + 19 + ], + "arch": "stairs_stone_down", + "line": 2589, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_11_67", + "source_tile": [ + 16, + 8 + ], + "target_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "target_tile": [ + 1, + 20 + ], + "arch": "stairs_stone_down", + "line": 2600, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_11_68", + "source_tile": [ + 20, + 3 + ], + "target_map": "/shattered_islands/world_11_68_1", + "target_tile": [ + 20, + 3 + ], + "arch": "ladder_up", + "line": 2745, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_11_68", + "source_tile": [ + 20, + 20 + ], + "target_map": "/shattered_islands/world_11_68_1", + "target_tile": [ + 20, + 20 + ], + "arch": "ladder_up", + "line": 2940, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_11_68_1", + "source_tile": [ + 20, + 3 + ], + "target_map": "/shattered_islands/world_11_68", + "target_tile": [ + 20, + 3 + ], + "arch": "ladder_down2", + "line": 205, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_11_68_1", + "source_tile": [ + 20, + 7 + ], + "target_map": "/shattered_islands/world_11_68_2", + "target_tile": [ + 20, + 7 + ], + "arch": "ladder_up", + "line": 236, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_11_68_1", + "source_tile": [ + 20, + 16 + ], + "target_map": "/shattered_islands/world_11_68_2", + "target_tile": [ + 20, + 16 + ], + "arch": "ladder_up", + "line": 289, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_11_68_1", + "source_tile": [ + 20, + 20 + ], + "target_map": "/shattered_islands/world_11_68", + "target_tile": [ + 20, + 20 + ], + "arch": "ladder_down2", + "line": 320, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_11_68_2", + "source_tile": [ + 20, + 7 + ], + "target_map": "/shattered_islands/world_11_68_1", + "target_tile": [ + 20, + 7 + ], + "arch": "ladder_down2", + "line": 273, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_11_68_2", + "source_tile": [ + 20, + 16 + ], + "target_map": "/shattered_islands/world_11_68_1", + "target_tile": [ + 20, + 16 + ], + "arch": "ladder_down2", + "line": 334, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_11_79", + "source_tile": [ + 7, + 7 + ], + "target_map": "/shattered_islands/eld_woods_island/cave_morlarg", + "target_tile": [ + 21, + 4 + ], + "arch": "invis_exit", + "line": 1130, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_11_79", + "source_tile": [ + 7, + 8 + ], + "target_map": "/shattered_islands/eld_woods_island/cave_morlarg", + "target_tile": [ + 21, + 5 + ], + "arch": "invis_exit", + "line": 1147, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_12_4", + "source_tile": [ + 12, + 10 + ], + "arch": "stairs_wood_down", + "line": 2058, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_12_4", + "source_tile": [ + 15, + 22 + ], + "target_map": "/shattered_islands/world_12_4_1", + "target_tile": [ + 15, + 22 + ], + "arch": "stairs_wood_up2", + "line": 2742, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_12_4", + "source_tile": [ + 16, + 22 + ], + "target_map": "/shattered_islands/world_12_4_1", + "target_tile": [ + 15, + 22 + ], + "arch": "stairs_wood_up", + "line": 2903, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_12_4_1", + "source_tile": [ + 15, + 22 + ], + "target_map": "/shattered_islands/world_12_4", + "target_tile": [ + 16, + 22 + ], + "arch": "stairs_wood_down", + "line": 694, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_12_4_1", + "source_tile": [ + 16, + 22 + ], + "target_map": "/shattered_islands/world_12_4", + "target_tile": [ + 16, + 22 + ], + "arch": "stairs_down", + "line": 843, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_12_63", + "source_tile": [ + 15, + 13 + ], + "target_map": "/shattered_islands/world_12_63", + "target_tile": [ + 15, + 13 + ], + "arch": "shop_mat", + "line": 1920, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_12_63", + "source_tile": [ + 15, + 15 + ], + "target_map": "/shattered_islands/world_12_63", + "target_tile": [ + 15, + 15 + ], + "arch": "shop_mat", + "line": 1940, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_12_64", + "source_tile": [ + 19, + 7 + ], + "arch": "stairs_stone_down", + "line": 2382, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_12_68", + "source_tile": [ + 10, + 22 + ], + "target_map": "/shattered_islands/ships/ship_1", + "target_tile": [ + 17, + 18 + ], + "arch": "stairs_wood_down", + "line": 1327, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_12_68", + "source_tile": [ + 20, + 12 + ], + "target_map": "/shattered_islands/ships/ship_1", + "target_tile": [ + 12, + 10 + ], + "arch": "stairs_wood_down", + "line": 2679, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_13_6", + "source_tile": [ + 5, + 0 + ], + "arch": "stairs_wood_up", + "line": 904, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_13_6", + "source_tile": [ + 5, + 1 + ], + "arch": "stairs_wood_up", + "line": 912, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_13_62", + "source_tile": [ + 7, + 10 + ], + "arch": "stairs_stone_down", + "line": 1148, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_13_64", + "source_tile": [ + 6, + 14 + ], + "arch": "hole", + "line": 885, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_13_7", + "source_tile": [ + 12, + 8 + ], + "target_map": "/shattered_islands/lyondale_archipelago/morliana/hierro/hierro_lair", + "target_tile": [ + 8, + 25 + ], + "arch": "stairs_stone_down", + "line": 1443, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "event_scripts": [ + "scripts/lyondale_archipelago/morliana/hierro_lair.py" + ] + }, + { + "source_map": "/shattered_islands/world_13_7", + "source_tile": [ + 13, + 8 + ], + "target_map": "/shattered_islands/lyondale_archipelago/morliana/hierro/hierro_lair", + "target_tile": [ + 9, + 25 + ], + "arch": "stairs_stone_down", + "line": 1563, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "event_scripts": [ + "scripts/lyondale_archipelago/morliana/hierro_lair.py" + ] + }, + { + "source_map": "/shattered_islands/world_13_7", + "source_tile": [ + 14, + 8 + ], + "target_map": "/shattered_islands/lyondale_archipelago/morliana/hierro/hierro_lair", + "target_tile": [ + 10, + 25 + ], + "arch": "stairs_stone_down", + "line": 1698, + "form": "explicit", + "vertical": "down", + "per_player_last_eat_1": true, + "event_scripts": [ + "scripts/lyondale_archipelago/morliana/hierro_lair.py" + ] + }, + { + "source_map": "/shattered_islands/world_14_5", + "source_tile": [ + 14, + 13 + ], + "target_map": "/shattered_islands/world_14_5_1", + "target_tile": [ + 14, + 13 + ], + "arch": "stairs_wood_up2", + "line": 2530, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_14_5", + "source_tile": [ + 14, + 14 + ], + "target_map": "/shattered_islands/world_14_5_1", + "target_tile": [ + 14, + 13 + ], + "arch": "stairs_wood_up", + "line": 2539, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_14_5_1", + "source_tile": [ + 14, + 10 + ], + "target_map": "/shattered_islands/world_14_5_1", + "target_tile": [ + 14, + 10 + ], + "arch": "shop_mat", + "line": 586, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_14_5_1", + "source_tile": [ + 14, + 13 + ], + "target_map": "/shattered_islands/world_14_5", + "target_tile": [ + 14, + 14 + ], + "arch": "stairs_wood_down", + "line": 602, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_14_5_1", + "source_tile": [ + 14, + 14 + ], + "target_map": "/shattered_islands/world_14_5", + "target_tile": [ + 14, + 14 + ], + "arch": "stairs_down", + "line": 611, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_14_5_1", + "source_tile": [ + 16, + 10 + ], + "target_map": "/shattered_islands/world_14_5_1", + "target_tile": [ + 16, + 10 + ], + "arch": "shop_mat", + "line": 769, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_14_64", + "source_tile": [ + 20, + 11 + ], + "target_map": "/shattered_islands/ships/ship_1", + "target_tile": [ + 17, + 10 + ], + "arch": "stairs_wood_down", + "line": 2380, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_14_71", + "source_tile": [ + 14, + 4 + ], + "target_map": "/shattered_islands/eld_woods_island/hemlock_cave/hemlock_cave_0101", + "target_tile": [ + 2, + 22 + ], + "arch": "stairs_stone_down", + "line": 1908, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_14_71", + "source_tile": [ + 15, + 4 + ], + "target_map": "/shattered_islands/eld_woods_island/hemlock_cave/hemlock_cave_0101", + "target_tile": [ + 3, + 22 + ], + "arch": "stairs_stone_down", + "line": 2040, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_15_4", + "source_tile": [ + 15, + 12 + ], + "target_map": "/shattered_islands/ships/ship_2", + "target_tile": [ + 1, + 10 + ], + "arch": "stairs_wood_down", + "line": 2320, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_17_2", + "source_tile": [ + 5, + 5 + ], + "target_map": "/shattered_islands/world_17_2_2", + "target_tile": [ + 4, + 4 + ], + "arch": "exit_green", + "line": 1083, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2", + "source_tile": [ + 5, + 17 + ], + "target_map": "/shattered_islands/world_17_2_5", + "target_tile": [ + 4, + 19 + ], + "arch": "exit_green", + "line": 1191, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2", + "source_tile": [ + 17, + 5 + ], + "target_map": "/shattered_islands/world_17_2_3", + "target_tile": [ + 19, + 4 + ], + "arch": "exit_green", + "line": 3384, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2", + "source_tile": [ + 17, + 17 + ], + "target_map": "/shattered_islands/world_17_2_4", + "target_tile": [ + 19, + 19 + ], + "arch": "exit_green", + "line": 3492, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_2", + "source_tile": [ + 3, + 3 + ], + "target_map": "/shattered_islands/world_17_2", + "target_tile": [ + 6, + 6 + ], + "arch": "exit_green", + "line": 700, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_2", + "source_tile": [ + 3, + 20 + ], + "target_map": "/shattered_islands/world_17_2_5", + "target_tile": [ + 4, + 19 + ], + "arch": "exit_green", + "line": 872, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_2", + "source_tile": [ + 20, + 3 + ], + "target_map": "/shattered_islands/world_17_2_3", + "target_tile": [ + 19, + 4 + ], + "arch": "exit_green", + "line": 4061, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_2", + "source_tile": [ + 20, + 20 + ], + "target_map": "/shattered_islands/world_17_2_4", + "target_tile": [ + 19, + 19 + ], + "arch": "exit_green", + "line": 4233, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_3", + "source_tile": [ + 3, + 3 + ], + "target_map": "/shattered_islands/world_17_2_2", + "target_tile": [ + 4, + 4 + ], + "arch": "exit_green", + "line": 696, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_3", + "source_tile": [ + 3, + 20 + ], + "target_map": "/shattered_islands/world_17_2_5", + "target_tile": [ + 4, + 19 + ], + "arch": "exit_green", + "line": 878, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_3", + "source_tile": [ + 20, + 3 + ], + "target_map": "/shattered_islands/world_17_2", + "target_tile": [ + 16, + 6 + ], + "arch": "exit_green", + "line": 4306, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_3", + "source_tile": [ + 20, + 20 + ], + "target_map": "/shattered_islands/world_17_2_4", + "target_tile": [ + 19, + 19 + ], + "arch": "exit_green", + "line": 4488, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_4", + "source_tile": [ + 3, + 3 + ], + "target_map": "/shattered_islands/world_17_2_2", + "target_tile": [ + 4, + 4 + ], + "arch": "exit_green", + "line": 612, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_4", + "source_tile": [ + 3, + 20 + ], + "target_map": "/shattered_islands/world_17_2_5", + "target_tile": [ + 4, + 19 + ], + "arch": "exit_green", + "line": 784, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_4", + "source_tile": [ + 13, + 5 + ], + "target_map": "/shattered_islands/world_19_4", + "target_tile": [ + 5, + 22 + ], + "arch": "exit", + "line": 2554, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_4", + "source_tile": [ + 20, + 3 + ], + "target_map": "/shattered_islands/world_17_2_3", + "target_tile": [ + 19, + 4 + ], + "arch": "exit_green", + "line": 4191, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_4", + "source_tile": [ + 20, + 20 + ], + "target_map": "/shattered_islands/world_17_2", + "target_tile": [ + 16, + 16 + ], + "arch": "exit_green", + "line": 4363, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_5", + "source_tile": [ + 3, + 3 + ], + "target_map": "/shattered_islands/world_17_2_2", + "target_tile": [ + 4, + 4 + ], + "arch": "exit_green", + "line": 746, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_5", + "source_tile": [ + 3, + 20 + ], + "target_map": "/shattered_islands/world_17_2", + "target_tile": [ + 6, + 16 + ], + "arch": "exit_green", + "line": 968, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_5", + "source_tile": [ + 20, + 3 + ], + "target_map": "/shattered_islands/world_17_2_3", + "target_tile": [ + 19, + 4 + ], + "arch": "exit_green", + "line": 3969, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_2_5", + "source_tile": [ + 20, + 20 + ], + "target_map": "/shattered_islands/world_17_2_4", + "target_tile": [ + 19, + 19 + ], + "arch": "exit_green", + "line": 4119, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_17_3", + "source_tile": [ + 11, + 18 + ], + "target_map": "/shattered_islands/ships/ship_1", + "target_tile": [ + 17, + 2 + ], + "arch": "stairs_wood_down", + "line": 1601, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_18_3", + "source_tile": [ + 6, + 17 + ], + "target_map": "/shattered_islands/ships/ship_2", + "target_tile": [ + 6, + 10 + ], + "arch": "stairs_wood_down", + "line": 899, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_18_4", + "source_tile": [ + 19, + 19 + ], + "arch": "stairs_stone_down", + "line": 1958, + "form": "dynamic", + "vertical": "down", + "dynamic_target": "/random/" + }, + { + "source_map": "/shattered_islands/world_19_4", + "source_tile": [ + 6, + 22 + ], + "target_map": "/shattered_islands/world_17_2_4", + "target_tile": [ + 13, + 4 + ], + "arch": "exit_green", + "line": 773, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_19_72", + "source_tile": [ + 0, + 4 + ], + "target_map": "/shattered_islands/eld_woods_island/vielumin_ruins/ruins_0201", + "target_tile": [ + 9, + 22 + ], + "arch": "stairs_stone_down", + "line": 40, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_19_72", + "source_tile": [ + 1, + 4 + ], + "target_map": "/shattered_islands/eld_woods_island/vielumin_ruins/ruins_0201", + "target_tile": [ + 10, + 22 + ], + "arch": "stairs_stone_down", + "line": 142, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_47", + "source_tile": [ + 23, + 15 + ], + "target_map": "/shattered_islands/world_1_47_-1", + "target_tile": [ + 23, + 15 + ], + "arch": "stairs_down", + "line": 2740, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_47_-1", + "source_tile": [ + 23, + 15 + ], + "target_map": "/shattered_islands/world_2_47", + "target_tile": [ + 0, + 15 + ], + "arch": "stairs_stone_up", + "line": 2427, + "form": "tiled", + "vertical": "up", + "authored_target": [ + "/shattered_islands/world_1_47", + 24, + 15 + ] + }, + { + "source_map": "/shattered_islands/world_1_50", + "source_tile": [ + 19, + 23 + ], + "target_map": "/shattered_islands/world_1_51_-1", + "target_tile": [ + 19, + 0 + ], + "arch": "stairs_stone_down", + "line": 2559, + "form": "tiled", + "vertical": "down", + "authored_target": [ + "/shattered_islands/world_1_50_-1", + 19, + 24 + ] + }, + { + "source_map": "/shattered_islands/world_1_50_-1", + "source_tile": [ + 19, + 23 + ], + "target_map": "/shattered_islands/world_1_50", + "target_tile": [ + 19, + 23 + ], + "arch": "stairs_stone_up2", + "line": 2305, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_51", + "source_tile": [ + 19, + 0 + ], + "target_map": "/shattered_islands/world_1_51_-1", + "target_tile": [ + 19, + 0 + ], + "arch": "stairs_down", + "line": 2218, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_51_-1", + "source_tile": [ + 19, + 0 + ], + "target_map": "/shattered_islands/world_1_50", + "target_tile": [ + 19, + 23 + ], + "arch": "stairs_stone_up", + "line": 2310, + "form": "tiled", + "vertical": "up", + "authored_target": [ + "/shattered_islands/world_1_51", + 19, + -1 + ] + }, + { + "source_map": "/shattered_islands/world_1_67", + "source_tile": [ + 16, + 16 + ], + "target_map": "/shattered_islands/world_1_67_1", + "target_tile": [ + 16, + 16 + ], + "arch": "stairs_wood_up2", + "line": 2774, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_67", + "source_tile": [ + 16, + 17 + ], + "target_map": "/shattered_islands/world_1_67_1", + "target_tile": [ + 16, + 16 + ], + "arch": "stairs_wood_up", + "line": 2783, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_67_1", + "source_tile": [ + 16, + 16 + ], + "target_map": "/shattered_islands/world_1_67", + "target_tile": [ + 16, + 17 + ], + "arch": "stairs_wood_down", + "line": 1057, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_67_1", + "source_tile": [ + 16, + 17 + ], + "target_map": "/shattered_islands/world_1_67", + "target_tile": [ + 16, + 17 + ], + "arch": "stairs_down", + "line": 1066, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_68", + "source_tile": [ + 0, + 7 + ], + "target_map": "/shattered_islands/world_1_68_1", + "target_tile": [ + 0, + 7 + ], + "arch": "ladder_up", + "line": 73, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_68", + "source_tile": [ + 0, + 9 + ], + "target_map": "/shattered_islands/world_1_68_1", + "target_tile": [ + 0, + 9 + ], + "arch": "ladder_up", + "line": 103, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_68", + "source_tile": [ + 0, + 11 + ], + "target_map": "/shattered_islands/world_1_68_1", + "target_tile": [ + 0, + 11 + ], + "arch": "ladder_up", + "line": 133, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_68", + "source_tile": [ + 0, + 13 + ], + "target_map": "/shattered_islands/world_1_68_1", + "target_tile": [ + 0, + 13 + ], + "arch": "ladder_up", + "line": 163, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_68", + "source_tile": [ + 0, + 15 + ], + "target_map": "/shattered_islands/world_1_68_1", + "target_tile": [ + 0, + 15 + ], + "arch": "ladder_up", + "line": 193, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_68", + "source_tile": [ + 5, + 0 + ], + "target_map": "/shattered_islands/world_1_68_-1", + "target_tile": [ + 5, + 0 + ], + "arch": "ladder_down3", + "line": 766, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_68", + "source_tile": [ + 5, + 21 + ], + "target_map": "/shattered_islands/world_1_68_-1", + "target_tile": [ + 5, + 21 + ], + "arch": "ladder_down3", + "line": 916, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_68", + "source_tile": [ + 22, + 7 + ], + "target_map": "/hall_of_dms", + "target_tile": [ + 5, + 19 + ], + "arch": "exit", + "line": 3015, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_68_-1", + "source_tile": [ + 5, + 0 + ], + "target_map": "/shattered_islands/world_1_68", + "target_tile": [ + 5, + 0 + ], + "arch": "ladder_up", + "line": 491, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_68_-1", + "source_tile": [ + 23, + 5 + ], + "target_map": "/shattered_islands/world_1_68_-2", + "target_tile": [ + 23, + 5 + ], + "arch": "hole", + "line": 2785, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_68_-2", + "source_tile": [ + 23, + 5 + ], + "target_map": "/shattered_islands/world_1_68_-1", + "target_tile": [ + 23, + 5 + ], + "arch": "ladder_up", + "line": 994, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_69", + "source_tile": [ + 5, + 17 + ], + "target_map": "/shattered_islands/world_1_69_-1", + "target_tile": [ + 5, + 17 + ], + "arch": "ladder_down3", + "line": 801, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_69_-1", + "source_tile": [ + 5, + 17 + ], + "target_map": "/shattered_islands/world_1_69", + "target_tile": [ + 5, + 17 + ], + "arch": "ladder_up", + "line": 965, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_69_-1", + "source_tile": [ + 15, + 16 + ], + "arch": "sewer_hole2", + "line": 1683, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_69_-2", + "source_tile": [ + 15, + 16 + ], + "target_map": "/shattered_islands/world_1_69_-1", + "target_tile": [ + 15, + 16 + ], + "arch": "ladder_up", + "line": 2029, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_70_-2", + "source_tile": [ + 13, + 6 + ], + "target_map": "/shattered_islands/world_1_70_-3", + "target_tile": [ + 13, + 6 + ], + "arch": "hole", + "line": 1674, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_70_-3", + "source_tile": [ + 13, + 6 + ], + "target_map": "/shattered_islands/world_1_70_-2", + "target_tile": [ + 13, + 7 + ], + "arch": "ladder_up", + "line": 799, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_71_-2", + "source_tile": [ + 7, + 11 + ], + "target_map": "/shattered_islands/world_1_71_-2", + "target_tile": [ + 7, + 13 + ], + "arch": "exit", + "line": 1061, + "form": "same-map", + "vertical": "none", + "event_scripts": [ + "strakewood_island/brynknot/sewers/scripts/rocks_puzzle.py" + ] + }, + { + "source_map": "/shattered_islands/world_1_73", + "source_tile": [ + 10, + 7 + ], + "arch": "stairs_stone_up", + "line": 2367, + "form": "tiled-unresolved", + "vertical": "up", + "tiled_slot": 9 + }, + { + "source_map": "/shattered_islands/world_1_73", + "source_tile": [ + 10, + 8 + ], + "arch": "stairs_stone_down", + "line": 2384, + "form": "dynamic", + "vertical": "down", + "dynamic_target": "/random/" + }, + { + "source_map": "/shattered_islands/world_1_73", + "source_tile": [ + 22, + 13 + ], + "target_map": "/shattered_islands/world_1_73_-1", + "target_tile": [ + 22, + 13 + ], + "arch": "ladder_down", + "line": 4830, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_73_-1", + "source_tile": [ + 1, + 20 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "ladder_down", + "line": 122, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_1_73_-1", + "source_tile": [ + 22, + 13 + ], + "target_map": "/shattered_islands/world_1_73", + "target_tile": [ + 22, + 13 + ], + "arch": "ladder_up", + "line": 1274, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 1, + 8 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 146, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 1, + 9 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 156, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 1, + 20 + ], + "target_map": "/shattered_islands/world_1_73_-1", + "target_tile": [ + 1, + 20 + ], + "arch": "ladder_up", + "line": 202, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 2, + 14 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 309, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 6, + 10 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 877, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 6, + 14 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 903, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 6, + 15 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 913, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 6, + 20 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 943, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 6, + 21 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 953, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 7, + 3 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 996, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 8, + 3 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 1156, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 9, + 6 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 1370, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 9, + 7 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 1380, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 9, + 14 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 1422, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 9, + 21 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 1456, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 10, + 21 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 1638, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 11, + 1 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 1666, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 11, + 10 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 1716, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 13, + 18 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 2108, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 13, + 21 + ], + "target_map": "/shattered_islands/world_4_73", + "target_tile": [ + 20, + 13 + ], + "arch": "exit", + "line": 2130, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 14, + 8 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 2207, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 14, + 11 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 2233, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 14, + 18 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 2271, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 15, + 4 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 2339, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 15, + 8 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 2369, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 15, + 11 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 2395, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 16, + 4 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 2518, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 19, + 14 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 3058, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 19, + 15 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 1, + 20 + ], + "arch": "invis_exit", + "line": 3068, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_1_73_-2", + "source_tile": [ + 22, + 15 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 22, + 11 + ], + "arch": "invis_exit", + "line": 3541, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_47", + "source_tile": [ + 0, + 15 + ], + "target_map": "/shattered_islands/world_1_47_-1", + "target_tile": [ + 23, + 15 + ], + "arch": "stairs_stone_down", + "line": 93, + "form": "tiled", + "vertical": "down", + "authored_target": [ + "/shattered_islands/world_2_47_-1", + -1, + 15 + ] + }, + { + "source_map": "/shattered_islands/world_2_47", + "source_tile": [ + 17, + 4 + ], + "arch": "ladder_up", + "line": 2605, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_2_47", + "source_tile": [ + 17, + 6 + ], + "arch": "ladder_up", + "line": 2642, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_2_47", + "source_tile": [ + 20, + 4 + ], + "arch": "ladder_up", + "line": 3103, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_2_47", + "source_tile": [ + 20, + 6 + ], + "arch": "ladder_up", + "line": 3140, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_2_47_-1", + "source_tile": [ + 0, + 15 + ], + "target_map": "/shattered_islands/world_2_47", + "target_tile": [ + 0, + 15 + ], + "arch": "stairs_stone_up2", + "line": 90, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_50_-1", + "source_tile": [ + 11, + 16 + ], + "target_map": "/shattered_islands/world_2_50_-2", + "target_tile": [ + 11, + 17 + ], + "arch": "stairs_stone_down", + "line": 1262, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_50_-1", + "source_tile": [ + 11, + 17 + ], + "target_map": "/shattered_islands/world_2_50_-2", + "target_tile": [ + 11, + 17 + ], + "arch": "stairs_down", + "line": 1271, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_50_-2", + "source_tile": [ + 11, + 16 + ], + "target_map": "/shattered_islands/world_2_50_-1", + "target_tile": [ + 11, + 16 + ], + "arch": "stairs_stone_up2", + "line": 104, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_50_-2", + "source_tile": [ + 11, + 17 + ], + "target_map": "/shattered_islands/world_2_50_-1", + "target_tile": [ + 11, + 16 + ], + "arch": "stairs_stone_up", + "line": 117, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_51_-1", + "source_tile": [ + 6, + 6 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 7, + 6 + ], + "arch": "invis_exit", + "line": 742, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_51_-1", + "source_tile": [ + 7, + 5 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 7, + 6 + ], + "arch": "invis_exit", + "line": 864, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_51_-1", + "source_tile": [ + 7, + 6 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 7, + 6 + ], + "arch": "invis_exit", + "line": 885, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_51_-2", + "source_tile": [ + 21, + 4 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 22, + 12 + ], + "arch": "bollard_tied_s_2", + "line": 1829, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_51_-2", + "source_tile": [ + 21, + 5 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 22, + 12 + ], + "arch": "bollard_tied_s_2", + "line": 1852, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_51_-2", + "source_tile": [ + 21, + 6 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 20, + 4 + ], + "arch": "bollard_tied_s_2", + "line": 1870, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_51_-2", + "source_tile": [ + 21, + 7 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 20, + 4 + ], + "arch": "bollard_tied_s_2", + "line": 1900, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_51_-2", + "source_tile": [ + 21, + 8 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 20, + 4 + ], + "arch": "bollard_tied_s_2", + "line": 1922, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_51_-2", + "source_tile": [ + 21, + 9 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 22, + 12 + ], + "arch": "bollard_tied_s_2", + "line": 1950, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_51_-2", + "source_tile": [ + 21, + 10 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 22, + 12 + ], + "arch": "bollard_tied_s_2", + "line": 1973, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_51_-2", + "source_tile": [ + 22, + 11 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 20, + 4 + ], + "arch": "bollard_tied_s_2", + "line": 2179, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_51_-2", + "source_tile": [ + 22, + 12 + ], + "target_map": "/shattered_islands/world_2_51_-2", + "target_tile": [ + 20, + 4 + ], + "arch": "bollard_tied_s_2", + "line": 2201, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_52_-2", + "source_tile": [ + 17, + 7 + ], + "target_map": "/shattered_islands/world_2_52_-3", + "target_tile": [ + 12, + 8 + ], + "arch": "invis_exit", + "line": 250, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_52_-2", + "source_tile": [ + 17, + 8 + ], + "target_map": "/shattered_islands/world_2_52_-3", + "target_tile": [ + 12, + 9 + ], + "arch": "invis_exit", + "line": 268, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_52_-2", + "source_tile": [ + 17, + 9 + ], + "target_map": "/shattered_islands/world_2_52_-3", + "target_tile": [ + 12, + 10 + ], + "arch": "invis_exit", + "line": 286, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_52_-3", + "source_tile": [ + 13, + 8 + ], + "target_map": "/shattered_islands/world_2_52_-2", + "target_tile": [ + 18, + 7 + ], + "arch": "invis_exit", + "line": 1234, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_52_-3", + "source_tile": [ + 13, + 9 + ], + "target_map": "/shattered_islands/world_2_52_-2", + "target_tile": [ + 18, + 8 + ], + "arch": "invis_exit", + "line": 1253, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_52_-3", + "source_tile": [ + 13, + 10 + ], + "target_map": "/shattered_islands/world_2_52_-2", + "target_tile": [ + 18, + 9 + ], + "arch": "invis_exit", + "line": 1272, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_53", + "source_tile": [ + 2, + 3 + ], + "arch": "ladder_down", + "line": 295, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_2_60", + "source_tile": [ + 7, + 22 + ], + "target_map": "/shattered_islands/world_2_60_-1", + "target_tile": [ + 7, + 22 + ], + "arch": "ladder_down2", + "line": 955, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_60", + "source_tile": [ + 12, + 22 + ], + "target_map": "/shattered_islands/world_2_60_-1", + "target_tile": [ + 12, + 22 + ], + "arch": "ladder_down3", + "line": 1638, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_60", + "source_tile": [ + 17, + 22 + ], + "target_map": "/shattered_islands/world_2_60_-1", + "target_tile": [ + 17, + 22 + ], + "arch": "ladder_down2", + "line": 2334, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_60_-1", + "source_tile": [ + 7, + 22 + ], + "target_map": "/shattered_islands/world_2_60", + "target_tile": [ + 7, + 22 + ], + "arch": "ladder_up", + "line": 110, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_60_-1", + "source_tile": [ + 12, + 22 + ], + "target_map": "/shattered_islands/world_2_60", + "target_tile": [ + 12, + 22 + ], + "arch": "ladder_up", + "line": 342, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_60_-1", + "source_tile": [ + 17, + 22 + ], + "target_map": "/shattered_islands/world_2_60", + "target_tile": [ + 17, + 22 + ], + "arch": "ladder_up", + "line": 530, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_61", + "source_tile": [ + 6, + 15 + ], + "target_map": "/shattered_islands/world_2_61_-1", + "target_tile": [ + 6, + 15 + ], + "arch": "ladder_down2", + "line": 919, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_61", + "source_tile": [ + 11, + 13 + ], + "target_map": "/shattered_islands/world_2_61_-1", + "target_tile": [ + 11, + 13 + ], + "arch": "ladder_down3", + "line": 1761, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_61", + "source_tile": [ + 16, + 14 + ], + "target_map": "/shattered_islands/world_2_61_-1", + "target_tile": [ + 16, + 14 + ], + "arch": "ladder_down2", + "line": 2458, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_61_-1", + "source_tile": [ + 6, + 15 + ], + "target_map": "/shattered_islands/world_2_61", + "target_tile": [ + 6, + 15 + ], + "arch": "ladder_up", + "line": 78, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_61_-1", + "source_tile": [ + 11, + 13 + ], + "target_map": "/shattered_islands/world_2_61", + "target_tile": [ + 11, + 13 + ], + "arch": "ladder_up", + "line": 387, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_61_-1", + "source_tile": [ + 16, + 14 + ], + "target_map": "/shattered_islands/world_2_61", + "target_tile": [ + 16, + 14 + ], + "arch": "ladder_up", + "line": 621, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_68", + "source_tile": [ + 1, + 15 + ], + "target_map": "/shattered_islands/world_2_68_1", + "target_tile": [ + 1, + 15 + ], + "arch": "stairs_wood_up2", + "line": 280, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_68", + "source_tile": [ + 1, + 16 + ], + "target_map": "/shattered_islands/world_2_68_1", + "target_tile": [ + 1, + 15 + ], + "arch": "stairs_wood_up", + "line": 289, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_68", + "source_tile": [ + 5, + 9 + ], + "target_map": "/shattered_islands/world_2_68_1", + "target_tile": [ + 6, + 9 + ], + "arch": "stairs_stone_up", + "line": 1011, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_68", + "source_tile": [ + 6, + 9 + ], + "target_map": "/shattered_islands/world_2_68_1", + "target_tile": [ + 6, + 9 + ], + "arch": "stairs_stone_up2", + "line": 1199, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_68", + "source_tile": [ + 12, + 21 + ], + "target_map": "/shattered_islands/world_2_68_-1", + "target_tile": [ + 12, + 21 + ], + "arch": "ladder_down3", + "line": 2167, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_68_-1", + "source_tile": [ + 12, + 21 + ], + "target_map": "/shattered_islands/world_2_68", + "target_tile": [ + 12, + 21 + ], + "arch": "ladder_up", + "line": 1127, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_68_1", + "source_tile": [ + 1, + 15 + ], + "target_map": "/shattered_islands/world_2_68", + "target_tile": [ + 1, + 16 + ], + "arch": "stairs_wood_down", + "line": 159, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_68_1", + "source_tile": [ + 1, + 16 + ], + "target_map": "/shattered_islands/world_2_68", + "target_tile": [ + 1, + 16 + ], + "arch": "stairs_down", + "line": 168, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_68_1", + "source_tile": [ + 5, + 5 + ], + "target_map": "/shattered_islands/world_2_68_2", + "target_tile": [ + 5, + 5 + ], + "arch": "ladder_up", + "line": 607, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_68_1", + "source_tile": [ + 5, + 9 + ], + "target_map": "/shattered_islands/world_2_68", + "target_tile": [ + 5, + 9 + ], + "arch": "stairs_down", + "line": 627, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_68_1", + "source_tile": [ + 5, + 21 + ], + "target_map": "/shattered_islands/world_2_68_2", + "target_tile": [ + 5, + 21 + ], + "arch": "stairs_wood_up2", + "line": 712, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_68_1", + "source_tile": [ + 6, + 9 + ], + "target_map": "/shattered_islands/world_2_68", + "target_tile": [ + 5, + 9 + ], + "arch": "stairs_stone_down", + "line": 760, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_68_1", + "source_tile": [ + 6, + 21 + ], + "target_map": "/shattered_islands/world_2_68_2", + "target_tile": [ + 5, + 21 + ], + "arch": "stairs_wood_up", + "line": 825, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_68_2", + "source_tile": [ + 4, + 5 + ], + "target_map": "/shattered_islands/world_2_68_3", + "target_tile": [ + 4, + 5 + ], + "arch": "ladder_up", + "line": 499, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_68_2", + "source_tile": [ + 5, + 5 + ], + "target_map": "/shattered_islands/world_2_68_1", + "target_tile": [ + 5, + 5 + ], + "arch": "ladder_down2", + "line": 609, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_68_2", + "source_tile": [ + 5, + 21 + ], + "target_map": "/shattered_islands/world_2_68_1", + "target_tile": [ + 6, + 21 + ], + "arch": "stairs_wood_down", + "line": 701, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_68_2", + "source_tile": [ + 6, + 21 + ], + "target_map": "/shattered_islands/world_2_68_1", + "target_tile": [ + 6, + 21 + ], + "arch": "stairs_down", + "line": 850, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_68_3", + "source_tile": [ + 4, + 5 + ], + "target_map": "/shattered_islands/world_2_68_2", + "target_tile": [ + 4, + 5 + ], + "arch": "ladder_down2", + "line": 282, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_69", + "source_tile": [ + 0, + 6 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_0_1", + "target_tile": [ + 23, + 13 + ], + "arch": "exit_green", + "line": 48, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_2_69", + "source_tile": [ + 0, + 7 + ], + "target_map": "/shattered_islands/world_2_69_-1", + "target_tile": [ + 0, + 7 + ], + "arch": "hole", + "line": 58, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_69_-1", + "source_tile": [ + 0, + 7 + ], + "target_map": "/shattered_islands/world_2_69", + "target_tile": [ + 0, + 7 + ], + "arch": "ladder_up", + "line": 61, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_2_70", + "source_tile": [ + 5, + 12 + ], + "target_map": "/shattered_islands/ships/ship_2", + "target_tile": [ + 1, + 2 + ], + "arch": "stairs_wood_down", + "line": 789, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_74", + "source_tile": [ + 9, + 21 + ], + "target_map": "/shattered_islands/ships/ship_1", + "target_tile": [ + 6, + 18 + ], + "arch": "stairs_wood_down", + "line": 1365, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_2_83", + "source_tile": [ + 16, + 12 + ], + "arch": "stairs_stone_down", + "line": 2231, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_2_87", + "source_tile": [ + 7, + 7 + ], + "target_map": "/shattered_islands/incuna/ship_lower_deck_to_incuna", + "target_tile": [ + 2, + 2 + ], + "arch": "stairs_wood_down", + "line": 1138, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_43", + "source_tile": [ + 2, + 20 + ], + "arch": "ladder_down3", + "line": 374, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_3_44", + "source_tile": [ + 9, + 18 + ], + "target_map": "/shattered_islands/world_3_44_-1", + "target_tile": [ + 9, + 18 + ], + "arch": "stairs_down", + "line": 1926, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_44", + "source_tile": [ + 9, + 19 + ], + "target_map": "/shattered_islands/world_3_44_-1", + "target_tile": [ + 9, + 18 + ], + "arch": "stairs_stone_down", + "line": 1937, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_44_-1", + "source_tile": [ + 9, + 18 + ], + "target_map": "/shattered_islands/world_3_44", + "target_tile": [ + 9, + 19 + ], + "arch": "stairs_stone_up", + "line": 486, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_3_44_-1", + "source_tile": [ + 9, + 19 + ], + "target_map": "/shattered_islands/world_3_44", + "target_tile": [ + 9, + 19 + ], + "arch": "stairs_stone_up2", + "line": 495, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_3_46", + "source_tile": [ + 16, + 14 + ], + "arch": "ladder_down3", + "line": 2212, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_3_56", + "source_tile": [ + 13, + 22 + ], + "target_map": "/shattered_islands/world_3_56_-2", + "target_tile": [ + 13, + 22 + ], + "arch": "well_stone", + "line": 2326, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "reciprocal well_bucket at /shattered_islands/world_3_56_-2 (13,22)" + }, + { + "source_map": "/shattered_islands/world_3_56_-2", + "source_tile": [ + 13, + 22 + ], + "target_map": "/shattered_islands/world_3_56", + "target_tile": [ + 13, + 22 + ], + "arch": "well_bucket", + "line": 698, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_59", + "source_tile": [ + 10, + 21 + ], + "arch": "stairs_stone_down", + "line": 1504, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_3_60", + "source_tile": [ + 17, + 5 + ], + "arch": "hole", + "line": 2207, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_66_-2", + "source_tile": [ + 23, + 12 + ], + "target_map": "/shattered_islands/strakewood_island/brynknot/sewers/lab", + "target_tile": [ + 19, + 13 + ], + "arch": "exit", + "line": 4164, + "form": "explicit", + "vertical": "none", + "event_scripts": [ + "strakewood_island/brynknot/sewers/scripts/nyhelobo_portal.py" + ] + }, + { + "source_map": "/shattered_islands/world_3_70", + "source_tile": [ + 6, + 1 + ], + "target_map": "/shattered_islands/strakewood_island/dark_cave/dark_cave_0102", + "target_tile": [ + 1, + 1 + ], + "arch": "invis_exit", + "line": 648, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_3_82", + "source_tile": [ + 13, + 3 + ], + "target_map": "/shattered_islands/world_3_82_-1", + "target_tile": [ + 13, + 3 + ], + "arch": "stairs_down", + "line": 2076, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_82", + "source_tile": [ + 13, + 4 + ], + "target_map": "/shattered_islands/world_3_82_-1", + "target_tile": [ + 13, + 3 + ], + "arch": "stairs_stone_down", + "line": 2092, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_82", + "source_tile": [ + 14, + 3 + ], + "target_map": "/shattered_islands/world_3_82_-1", + "target_tile": [ + 14, + 3 + ], + "arch": "stairs_down", + "line": 2265, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_82", + "source_tile": [ + 14, + 4 + ], + "target_map": "/shattered_islands/world_3_82_-1", + "target_tile": [ + 14, + 3 + ], + "arch": "stairs_stone_down", + "line": 2277, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_82", + "source_tile": [ + 15, + 3 + ], + "target_map": "/shattered_islands/world_3_82_-1", + "target_tile": [ + 15, + 3 + ], + "arch": "stairs_down", + "line": 2454, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_82", + "source_tile": [ + 15, + 4 + ], + "target_map": "/shattered_islands/world_3_82_-1", + "target_tile": [ + 15, + 3 + ], + "arch": "stairs_stone_down", + "line": 2466, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_82_-1", + "source_tile": [ + 13, + 3 + ], + "target_map": "/shattered_islands/world_3_82", + "target_tile": [ + 13, + 4 + ], + "arch": "stairs_stone_up", + "line": 663, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_3_82_-1", + "source_tile": [ + 13, + 4 + ], + "target_map": "/shattered_islands/world_3_82", + "target_tile": [ + 13, + 4 + ], + "arch": "stairs_stone_up2", + "line": 673, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_3_82_-1", + "source_tile": [ + 14, + 3 + ], + "target_map": "/shattered_islands/world_3_82", + "target_tile": [ + 14, + 4 + ], + "arch": "stairs_stone_up", + "line": 773, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_3_82_-1", + "source_tile": [ + 14, + 4 + ], + "target_map": "/shattered_islands/world_3_82", + "target_tile": [ + 14, + 4 + ], + "arch": "stairs_stone_up2", + "line": 783, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_3_82_-1", + "source_tile": [ + 15, + 3 + ], + "target_map": "/shattered_islands/world_3_82", + "target_tile": [ + 15, + 4 + ], + "arch": "stairs_stone_up", + "line": 862, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_3_82_-1", + "source_tile": [ + 15, + 4 + ], + "target_map": "/shattered_islands/world_3_82", + "target_tile": [ + 15, + 4 + ], + "arch": "stairs_stone_up2", + "line": 872, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_3_83", + "source_tile": [ + 15, + 2 + ], + "target_map": "/shattered_islands/world_3_83_-2", + "target_tile": [ + 15, + 2 + ], + "arch": "invis_exit", + "line": 2555, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_3_83", + "source_tile": [ + 18, + 2 + ], + "target_map": "/shattered_islands/world_3_83_-2", + "target_tile": [ + 18, + 2 + ], + "arch": "invis_exit", + "line": 3068, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_3_83_-2", + "source_tile": [ + 14, + 1 + ], + "target_map": "/shattered_islands/world_3_83", + "target_tile": [ + 14, + 1 + ], + "arch": "invis_exit", + "line": 516, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_3_83_-2", + "source_tile": [ + 14, + 9 + ], + "arch": "ladder_down", + "line": 591, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_3_83_-2", + "source_tile": [ + 18, + 1 + ], + "target_map": "/shattered_islands/world_3_83", + "target_tile": [ + 18, + 1 + ], + "arch": "invis_exit", + "line": 1289, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_3_83_-2", + "source_tile": [ + 19, + 1 + ], + "target_map": "/shattered_islands/world_3_83", + "target_tile": [ + 19, + 1 + ], + "arch": "invis_exit", + "line": 1467, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_3_85_-1", + "source_tile": [ + 8, + 8 + ], + "target_map": "/shattered_islands/world_3_85_-2", + "target_tile": [ + 8, + 9 + ], + "arch": "stairs_wood_down", + "line": 201, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_85_-1", + "source_tile": [ + 8, + 9 + ], + "target_map": "/shattered_islands/world_3_85_-2", + "target_tile": [ + 8, + 9 + ], + "arch": "stairs_down", + "line": 210, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_3_85_-2", + "source_tile": [ + 8, + 8 + ], + "target_map": "/shattered_islands/world_3_85_-1", + "target_tile": [ + 8, + 8 + ], + "arch": "stairs_wood_up2", + "line": 1279, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_3_85_-2", + "source_tile": [ + 8, + 9 + ], + "target_map": "/shattered_islands/world_3_85_-1", + "target_tile": [ + 8, + 8 + ], + "arch": "stairs_wood_up", + "line": 1288, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_45", + "source_tile": [ + 18, + 4 + ], + "target_map": "/shattered_islands/world_4_45", + "target_tile": [ + 19, + 4 + ], + "arch": "exit", + "line": 3213, + "form": "same-map", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_teleport.py" + ] + }, + { + "source_map": "/shattered_islands/world_4_48", + "source_tile": [ + 22, + 11 + ], + "target_map": "/shattered_islands/world_4_48_1", + "target_tile": [ + 22, + 11 + ], + "arch": "stairs_wood_up2", + "line": 3054, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_48", + "source_tile": [ + 22, + 12 + ], + "target_map": "/shattered_islands/world_4_48_1", + "target_tile": [ + 22, + 12 + ], + "arch": "stairs_wood_up", + "line": 3063, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_48_1", + "source_tile": [ + 22, + 11 + ], + "target_map": "/shattered_islands/world_4_48", + "target_tile": [ + 22, + 12 + ], + "arch": "stairs_wood_down", + "line": 96, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_48_1", + "source_tile": [ + 22, + 12 + ], + "target_map": "/shattered_islands/world_4_48", + "target_tile": [ + 22, + 12 + ], + "arch": "stairs_down", + "line": 105, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_50", + "source_tile": [ + 5, + 4 + ], + "arch": "ladder_up", + "line": 800, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_4_50", + "source_tile": [ + 5, + 6 + ], + "arch": "ladder_up", + "line": 837, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_4_50", + "source_tile": [ + 8, + 4 + ], + "arch": "ladder_up", + "line": 1318, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_4_50", + "source_tile": [ + 8, + 6 + ], + "arch": "ladder_up", + "line": 1355, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_4_52", + "source_tile": [ + 8, + 12 + ], + "arch": "stairs_stone_down", + "line": 1250, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_4_52", + "source_tile": [ + 10, + 16 + ], + "target_map": "/shattered_islands/world_4_52_1", + "target_tile": [ + 9, + 16 + ], + "arch": "ladder_tall_up", + "line": 1613, + "form": "explicit", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_4_52", + "source_tile": [ + 10, + 17 + ], + "target_map": "/shattered_islands/world_4_52_1", + "target_tile": [ + 9, + 17 + ], + "arch": "ladder_tall_up", + "line": 1626, + "form": "explicit", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_4_52_1", + "source_tile": [ + 10, + 15 + ], + "target_map": "/shattered_islands/world_4_52", + "target_tile": [ + 10, + 15 + ], + "arch": "invis_exit", + "line": 947, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_52_1", + "source_tile": [ + 10, + 16 + ], + "target_map": "/shattered_islands/world_4_52", + "target_tile": [ + 10, + 16 + ], + "arch": "invis_exit", + "line": 956, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_52_1", + "source_tile": [ + 10, + 17 + ], + "target_map": "/shattered_islands/world_4_52", + "target_tile": [ + 10, + 17 + ], + "arch": "invis_exit", + "line": 965, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_52_1", + "source_tile": [ + 10, + 18 + ], + "target_map": "/shattered_islands/world_4_52", + "target_tile": [ + 10, + 18 + ], + "arch": "invis_exit", + "line": 974, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53", + "source_tile": [ + 3, + 18 + ], + "target_map": "/shattered_islands/world_4_53_-2", + "target_tile": [ + 1, + 12 + ], + "arch": "well_stone", + "line": 634, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "reciprocal well_bucket at /shattered_islands/world_4_53_-2 (1,12)" + }, + { + "source_map": "/shattered_islands/world_4_53", + "source_tile": [ + 10, + 3 + ], + "target_map": "/shattered_islands/world_4_53_-1", + "target_tile": [ + 10, + 3 + ], + "arch": "stairs_down", + "line": 1879, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53", + "source_tile": [ + 10, + 4 + ], + "target_map": "/shattered_islands/world_4_53_-1", + "target_tile": [ + 10, + 3 + ], + "arch": "stairs_stone_down", + "line": 1891, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53", + "source_tile": [ + 11, + 3 + ], + "target_map": "/shattered_islands/world_4_53_-1", + "target_tile": [ + 11, + 3 + ], + "arch": "stairs_down", + "line": 2065, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53", + "source_tile": [ + 11, + 4 + ], + "target_map": "/shattered_islands/world_4_53_-1", + "target_tile": [ + 11, + 3 + ], + "arch": "stairs_stone_down", + "line": 2077, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53", + "source_tile": [ + 12, + 3 + ], + "target_map": "/shattered_islands/world_4_53_-1", + "target_tile": [ + 12, + 3 + ], + "arch": "stairs_down", + "line": 2289, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53", + "source_tile": [ + 12, + 4 + ], + "target_map": "/shattered_islands/world_4_53_-1", + "target_tile": [ + 12, + 3 + ], + "arch": "stairs_stone_down", + "line": 2301, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53_-1", + "source_tile": [ + 4, + 21 + ], + "target_map": "/shattered_islands/world_4_53_-2", + "target_tile": [ + 5, + 21 + ], + "arch": "hole", + "line": 462, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53_-1", + "source_tile": [ + 10, + 3 + ], + "target_map": "/shattered_islands/world_4_53", + "target_tile": [ + 10, + 4 + ], + "arch": "stairs_stone_up", + "line": 1202, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_53_-1", + "source_tile": [ + 10, + 4 + ], + "target_map": "/shattered_islands/world_4_53", + "target_tile": [ + 10, + 4 + ], + "arch": "stairs_stone_up2", + "line": 1215, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_53_-1", + "source_tile": [ + 11, + 3 + ], + "target_map": "/shattered_islands/world_4_53", + "target_tile": [ + 11, + 4 + ], + "arch": "stairs_stone_up", + "line": 1350, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_53_-1", + "source_tile": [ + 11, + 4 + ], + "target_map": "/shattered_islands/world_4_53", + "target_tile": [ + 11, + 4 + ], + "arch": "stairs_stone_up2", + "line": 1363, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_53_-1", + "source_tile": [ + 12, + 3 + ], + "target_map": "/shattered_islands/world_4_53", + "target_tile": [ + 12, + 4 + ], + "arch": "stairs_stone_up", + "line": 1507, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_53_-1", + "source_tile": [ + 12, + 4 + ], + "target_map": "/shattered_islands/world_4_53", + "target_tile": [ + 12, + 4 + ], + "arch": "stairs_stone_up2", + "line": 1520, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_53_-2", + "source_tile": [ + 1, + 12 + ], + "target_map": "/shattered_islands/world_4_53", + "target_tile": [ + 3, + 18 + ], + "arch": "well_bucket", + "line": 144, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53_-2", + "source_tile": [ + 4, + 21 + ], + "target_map": "/shattered_islands/world_4_53_-1", + "target_tile": [ + 4, + 21 + ], + "arch": "hole", + "line": 558, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53_-2", + "source_tile": [ + 7, + 4 + ], + "target_map": "/shattered_islands/world_4_53_-2", + "target_tile": [ + 9, + 3 + ], + "arch": "hole", + "line": 951, + "form": "same-map", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53_-2", + "source_tile": [ + 7, + 14 + ], + "target_map": "/shattered_islands/world_4_53_-2", + "target_tile": [ + 7, + 20 + ], + "arch": "hole", + "line": 1045, + "form": "same-map", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53_-2", + "source_tile": [ + 7, + 20 + ], + "target_map": "/shattered_islands/world_4_53_-2", + "target_tile": [ + 7, + 14 + ], + "arch": "hole", + "line": 1090, + "form": "same-map", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_53_-2", + "source_tile": [ + 8, + 3 + ], + "target_map": "/shattered_islands/world_4_53_-2", + "target_tile": [ + 7, + 5 + ], + "arch": "hole", + "line": 1132, + "form": "same-map", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_58", + "source_tile": [ + 13, + 10 + ], + "arch": "stairs_stone_down", + "line": 2033, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_4_58", + "source_tile": [ + 21, + 12 + ], + "target_map": "/shattered_islands/world_4_58", + "target_tile": [ + 20, + 13 + ], + "arch": "exit", + "line": 3402, + "form": "same-map", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_teleport.py" + ] + }, + { + "source_map": "/shattered_islands/world_4_59", + "source_tile": [ + 20, + 9 + ], + "arch": "stairs_stone_down", + "line": 3269, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_4_67", + "source_tile": [ + 12, + 4 + ], + "target_map": "/shattered_islands/world_4_67_-1", + "target_tile": [ + 12, + 4 + ], + "arch": "ladder_down", + "line": 1682, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_67", + "source_tile": [ + 12, + 17 + ], + "target_map": "/shattered_islands/world_4_67_-1", + "target_tile": [ + 12, + 17 + ], + "arch": "ladder_down", + "line": 1758, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_67_-1", + "source_tile": [ + 7, + 17 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_5_2_-1", + "target_tile": [ + 2, + 3 + ], + "arch": "stairs_stone_down", + "line": 1090, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_67_-1", + "source_tile": [ + 12, + 4 + ], + "target_map": "/shattered_islands/world_4_67", + "target_tile": [ + 12, + 4 + ], + "arch": "ladder_up", + "line": 1729, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_67_-1", + "source_tile": [ + 12, + 17 + ], + "target_map": "/shattered_islands/world_4_67", + "target_tile": [ + 12, + 17 + ], + "arch": "ladder_up", + "line": 1810, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_73", + "source_tile": [ + 19, + 13 + ], + "target_map": "/shattered_islands/world_1_73_-2", + "target_tile": [ + 14, + 21 + ], + "arch": "exit", + "line": 2257, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_4_83", + "source_tile": [ + 0, + 7 + ], + "target_map": "/shattered_islands/world_4_83_-1", + "target_tile": [ + 1, + 7 + ], + "arch": "stairs_stone_down", + "line": 56, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_83", + "source_tile": [ + 1, + 7 + ], + "target_map": "/shattered_islands/world_4_83_-1", + "target_tile": [ + 1, + 7 + ], + "arch": "stairs_down", + "line": 212, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_83", + "source_tile": [ + 6, + 17 + ], + "target_map": "/shattered_islands/world_4_83_-2", + "target_tile": [ + 6, + 17 + ], + "arch": "well_stone", + "line": 1148, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "reciprocal well_bucket at /shattered_islands/world_4_83_-2 (7,18)" + }, + { + "source_map": "/shattered_islands/world_4_83", + "source_tile": [ + 13, + 12 + ], + "target_map": "/shattered_islands/incuna/ellamin", + "target_tile": [ + 4, + 13 + ], + "arch": "stairs_wood_down", + "line": 2229, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_83", + "source_tile": [ + 15, + 8 + ], + "target_map": "/shattered_islands/incuna/ellamin", + "target_tile": [ + 3, + 1 + ], + "arch": "invis_exit", + "line": 2550, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_4_83_-1", + "source_tile": [ + 0, + 7 + ], + "target_map": "/shattered_islands/world_4_83", + "target_tile": [ + 0, + 7 + ], + "arch": "stairs_stone_up2", + "line": 37, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_83_-1", + "source_tile": [ + 1, + 7 + ], + "target_map": "/shattered_islands/world_4_83", + "target_tile": [ + 0, + 7 + ], + "arch": "stairs_stone_up", + "line": 126, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_83_-2", + "source_tile": [ + 7, + 18 + ], + "target_map": "/shattered_islands/world_4_83", + "target_tile": [ + 6, + 17 + ], + "arch": "well_bucket", + "line": 365, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 1, + 15 + ], + "target_map": "/shattered_islands/world_4_84_1", + "target_tile": [ + 1, + 15 + ], + "arch": "ladder_up", + "line": 292, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 6, + 14 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 7, + 14 + ], + "arch": "stairs_wood_down", + "line": 1229, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 7, + 14 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 7, + 14 + ], + "arch": "stairs_down", + "line": 1457, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 9, + 7 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 9, + 8 + ], + "arch": "stairs_stone_down", + "line": 1812, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 9, + 8 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 9, + 8 + ], + "arch": "stairs_down", + "line": 1821, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 10, + 7 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 10, + 8 + ], + "arch": "stairs_stone_down", + "line": 1999, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 10, + 8 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 10, + 8 + ], + "arch": "stairs_down", + "line": 2008, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 17, + 18 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 10, + 1 + ], + "arch": "exit", + "line": 3382, + "form": "same-map", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_teleport.py" + ] + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 22, + 10 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 23, + 10 + ], + "arch": "stairs_stone_down", + "line": 4329, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 22, + 11 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 23, + 11 + ], + "arch": "stairs_stone_down", + "line": 4337, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 23, + 10 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 23, + 10 + ], + "arch": "stairs_down", + "line": 4479, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84", + "source_tile": [ + 23, + 11 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 23, + 11 + ], + "arch": "stairs_down", + "line": 4487, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 6, + 14 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 6, + 14 + ], + "arch": "stairs_wood_up2", + "line": 916, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 7, + 14 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 6, + 14 + ], + "arch": "stairs_wood_up", + "line": 1065, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 9, + 7 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 9, + 7 + ], + "arch": "stairs_stone_up2", + "line": 1270, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 9, + 8 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 9, + 7 + ], + "arch": "stairs_stone_up", + "line": 1279, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 10, + 7 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 10, + 7 + ], + "arch": "stairs_stone_up2", + "line": 1391, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 10, + 8 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 10, + 7 + ], + "arch": "stairs_stone_up", + "line": 1400, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 20, + 5 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 20, + 5 + ], + "arch": "shop_mat", + "line": 2844, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 22, + 5 + ], + "target_map": "/shattered_islands/world_4_84_-1", + "target_tile": [ + 22, + 5 + ], + "arch": "shop_mat", + "line": 3144, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 22, + 10 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 22, + 10 + ], + "arch": "stairs_stone_up2", + "line": 3177, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 22, + 11 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 22, + 11 + ], + "arch": "stairs_stone_up2", + "line": 3185, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 23, + 10 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 22, + 10 + ], + "arch": "stairs_stone_up", + "line": 3233, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84_-1", + "source_tile": [ + 23, + 11 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 22, + 11 + ], + "arch": "stairs_stone_up", + "line": 3241, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_4_84_-2", + "source_tile": [ + 16, + 6 + ], + "arch": "exit_green", + "line": 766, + "form": "unresolved", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_4_84_1", + "source_tile": [ + 1, + 15 + ], + "target_map": "/shattered_islands/world_4_84", + "target_tile": [ + 1, + 15 + ], + "arch": "ladder_down", + "line": 115, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_85", + "source_tile": [ + 7, + 13 + ], + "target_map": "/shattered_islands/world_4_85", + "target_tile": [ + 7, + 13 + ], + "arch": "stairs_wood_down", + "line": 1130, + "form": "same-map", + "vertical": "down", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_4_85", + "source_tile": [ + 20, + 13 + ], + "target_map": "/shattered_islands/world_4_85_-1", + "target_tile": [ + 20, + 13 + ], + "arch": "hole", + "line": 3052, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_4_85_-1", + "source_tile": [ + 20, + 13 + ], + "target_map": "/shattered_islands/world_4_85", + "target_tile": [ + 20, + 13 + ], + "arch": "rope_up", + "line": 578, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_42", + "source_tile": [ + 18, + 14 + ], + "arch": "ladder_up", + "line": 3015, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_5_42", + "source_tile": [ + 18, + 16 + ], + "arch": "ladder_up", + "line": 3054, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_5_42", + "source_tile": [ + 21, + 14 + ], + "arch": "ladder_up", + "line": 3588, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_5_42", + "source_tile": [ + 21, + 16 + ], + "arch": "ladder_up", + "line": 3627, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_5_43_1", + "source_tile": [ + 19, + 15 + ], + "target_map": "/shattered_islands/world_5_43_2", + "target_tile": [ + 19, + 15 + ], + "arch": "stairs_spiral_stone_up", + "line": 146, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_43_2", + "source_tile": [ + 19, + 15 + ], + "target_map": "/shattered_islands/world_5_43_1", + "target_tile": [ + 19, + 15 + ], + "arch": "stairs_spiral_stone_down", + "line": 228, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_44_1", + "source_tile": [ + 19, + 18 + ], + "target_map": "/shattered_islands/world_5_44_2", + "target_tile": [ + 19, + 18 + ], + "arch": "stairs_spiral_stone_up", + "line": 337, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_44_2", + "source_tile": [ + 19, + 18 + ], + "target_map": "/shattered_islands/world_5_44_1", + "target_tile": [ + 19, + 18 + ], + "arch": "stairs_spiral_stone_down", + "line": 354, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_45", + "source_tile": [ + 14, + 9 + ], + "target_map": "/shattered_islands/world_5_45_1", + "target_tile": [ + 15, + 9 + ], + "arch": "stairs_wood_up", + "line": 2351, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_45", + "source_tile": [ + 15, + 9 + ], + "target_map": "/shattered_islands/world_5_45_1", + "target_tile": [ + 15, + 9 + ], + "arch": "stairs_wood_up2", + "line": 2544, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_45_1", + "source_tile": [ + 14, + 9 + ], + "target_map": "/shattered_islands/world_5_45", + "target_tile": [ + 14, + 9 + ], + "arch": "stairs_down", + "line": 158, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_45_1", + "source_tile": [ + 15, + 9 + ], + "target_map": "/shattered_islands/world_5_45", + "target_tile": [ + 14, + 9 + ], + "arch": "stairs_wood_down", + "line": 237, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_46", + "source_tile": [ + 17, + 10 + ], + "target_map": "/shattered_islands/world_5_46", + "target_tile": [ + 17, + 10 + ], + "arch": "shop_mat", + "line": 2999, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_5_46", + "source_tile": [ + 19, + 10 + ], + "target_map": "/shattered_islands/world_5_46", + "target_tile": [ + 19, + 10 + ], + "arch": "shop_mat", + "line": 3388, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_5_48", + "source_tile": [ + 6, + 16 + ], + "target_map": "/shattered_islands/world_5_48_-1", + "target_tile": [ + 6, + 18 + ], + "arch": "stairs_stone_down", + "line": 1119, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_48", + "source_tile": [ + 6, + 17 + ], + "target_map": "/shattered_islands/world_5_48_-1", + "target_tile": [ + 6, + 18 + ], + "arch": "stairs_down", + "line": 1131, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_48", + "source_tile": [ + 22, + 22 + ], + "target_map": "/shattered_islands/world_5_48_-1", + "target_tile": [ + 22, + 22 + ], + "arch": "stairs_wood_down", + "line": 3718, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_48_-1", + "source_tile": [ + 6, + 16 + ], + "target_map": "/shattered_islands/world_5_48", + "target_tile": [ + 6, + 15 + ], + "arch": "stairs_stone_up2", + "line": 144, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_48_-1", + "source_tile": [ + 6, + 17 + ], + "target_map": "/shattered_islands/world_5_48", + "target_tile": [ + 6, + 15 + ], + "arch": "stairs_stone_up", + "line": 156, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_48_-1", + "source_tile": [ + 22, + 22 + ], + "target_map": "/shattered_islands/world_5_48", + "target_tile": [ + 22, + 22 + ], + "arch": "stairs_wood_up", + "line": 906, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_57", + "source_tile": [ + 7, + 3 + ], + "target_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_0101", + "target_tile": [ + 1, + 1 + ], + "arch": "stairs_stone_down", + "line": 1429, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_57", + "source_tile": [ + 8, + 3 + ], + "target_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_0101", + "target_tile": [ + 2, + 1 + ], + "arch": "stairs_stone_down", + "line": 1619, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_57", + "source_tile": [ + 9, + 3 + ], + "target_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_0101", + "target_tile": [ + 3, + 1 + ], + "arch": "stairs_stone_down", + "line": 1805, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_57_-2", + "source_tile": [ + 14, + 14 + ], + "target_map": "/shattered_islands/strakewood_island/old_outpost/old_outpost_a_0204", + "target_tile": [ + 8, + 19 + ], + "arch": "well_bucket", + "line": 708, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_58", + "source_tile": [ + 3, + 17 + ], + "target_map": "/shattered_islands/world_5_58", + "target_tile": [ + 3, + 17 + ], + "arch": "shop_mat", + "line": 570, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_5_58", + "source_tile": [ + 5, + 17 + ], + "target_map": "/shattered_islands/world_5_58", + "target_tile": [ + 5, + 17 + ], + "arch": "shop_mat", + "line": 869, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_5_65", + "source_tile": [ + 2, + 2 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_0_0", + "target_tile": [ + 1, + 1 + ], + "arch": "stairs_stone_down", + "line": 308, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_65", + "source_tile": [ + 11, + 2 + ], + "target_map": "/shattered_islands/world_5_65_1", + "target_tile": [ + 11, + 2 + ], + "arch": "ladder_up", + "line": 1745, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_65_1", + "source_tile": [ + 11, + 2 + ], + "target_map": "/shattered_islands/world_5_65", + "target_tile": [ + 11, + 2 + ], + "arch": "ladder_down", + "line": 1019, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_65_1", + "source_tile": [ + 13, + 2 + ], + "target_map": "/shattered_islands/world_5_65_2", + "target_tile": [ + 13, + 2 + ], + "arch": "ladder_up", + "line": 1224, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_65_2", + "source_tile": [ + 13, + 2 + ], + "target_map": "/shattered_islands/world_5_65_1", + "target_tile": [ + 13, + 2 + ], + "arch": "ladder_down", + "line": 869, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_67", + "source_tile": [ + 6, + 9 + ], + "target_map": "/shattered_islands/world_5_67_1", + "target_tile": [ + 6, + 9 + ], + "arch": "stairs_wood_up2", + "line": 1083, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_67", + "source_tile": [ + 7, + 9 + ], + "target_map": "/shattered_islands/world_5_67_1", + "target_tile": [ + 6, + 9 + ], + "arch": "stairs_wood_up", + "line": 1258, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_67_1", + "source_tile": [ + 6, + 9 + ], + "target_map": "/shattered_islands/world_5_67", + "target_tile": [ + 7, + 9 + ], + "arch": "stairs_wood_down", + "line": 250, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_67_1", + "source_tile": [ + 7, + 9 + ], + "target_map": "/shattered_islands/world_5_67", + "target_tile": [ + 7, + 9 + ], + "arch": "stairs_down", + "line": 309, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_68", + "source_tile": [ + 9, + 5 + ], + "target_map": "/shattered_islands/world_5_68_-1", + "target_tile": [ + 9, + 4 + ], + "arch": "stairs_stone_down", + "line": 1378, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_68", + "source_tile": [ + 10, + 5 + ], + "target_map": "/shattered_islands/world_5_68_-1", + "target_tile": [ + 10, + 4 + ], + "arch": "stairs_stone_down", + "line": 1550, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_68", + "source_tile": [ + 22, + 9 + ], + "arch": "well_stone", + "line": 3377, + "form": "unresolved", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_5_68_-1", + "source_tile": [ + 9, + 4 + ], + "target_map": "/shattered_islands/world_5_68", + "target_tile": [ + 9, + 5 + ], + "arch": "stairs_stone_up", + "line": 87, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_68_-1", + "source_tile": [ + 10, + 4 + ], + "target_map": "/shattered_islands/world_5_68", + "target_tile": [ + 10, + 5 + ], + "arch": "stairs_stone_up", + "line": 120, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_71", + "source_tile": [ + 8, + 15 + ], + "target_map": "/shattered_islands/world_6_72", + "target_tile": [ + 17, + 11 + ], + "arch": "exit_green", + "line": 1157, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_5_84_-2", + "source_tile": [ + 2, + 7 + ], + "target_map": "/shattered_islands/world_5_84_-3", + "target_tile": [ + 2, + 8 + ], + "arch": "stairs_wood_down", + "line": 479, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_84_-2", + "source_tile": [ + 2, + 8 + ], + "target_map": "/shattered_islands/world_5_84_-3", + "target_tile": [ + 2, + 8 + ], + "arch": "stairs_down", + "line": 488, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_84_-2", + "source_tile": [ + 2, + 13 + ], + "target_map": "/shattered_islands/world_5_84_-3", + "target_tile": [ + 2, + 13 + ], + "arch": "stairs_down", + "line": 524, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_84_-2", + "source_tile": [ + 2, + 14 + ], + "target_map": "/shattered_islands/world_5_84_-3", + "target_tile": [ + 2, + 13 + ], + "arch": "stairs_wood_down", + "line": 532, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_84_-2", + "source_tile": [ + 3, + 7 + ], + "target_map": "/shattered_islands/world_5_84_-3", + "target_tile": [ + 3, + 8 + ], + "arch": "stairs_wood_down", + "line": 806, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_84_-2", + "source_tile": [ + 3, + 8 + ], + "target_map": "/shattered_islands/world_5_84_-3", + "target_tile": [ + 3, + 8 + ], + "arch": "stairs_down", + "line": 815, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_84_-2", + "source_tile": [ + 3, + 13 + ], + "target_map": "/shattered_islands/world_5_84_-3", + "target_tile": [ + 3, + 13 + ], + "arch": "stairs_down", + "line": 851, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_84_-2", + "source_tile": [ + 3, + 14 + ], + "target_map": "/shattered_islands/world_5_84_-3", + "target_tile": [ + 3, + 13 + ], + "arch": "stairs_wood_down", + "line": 859, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_84_-2", + "source_tile": [ + 9, + 8 + ], + "target_map": "/shattered_islands/world_5_84", + "target_tile": [ + 9, + 8 + ], + "arch": "ladder_tall_up", + "line": 2093, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_84_-3", + "source_tile": [ + 2, + 7 + ], + "target_map": "/shattered_islands/world_5_84_-2", + "target_tile": [ + 2, + 7 + ], + "arch": "stairs_wood_up2", + "line": 113, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_84_-3", + "source_tile": [ + 2, + 8 + ], + "target_map": "/shattered_islands/world_5_84_-2", + "target_tile": [ + 2, + 7 + ], + "arch": "stairs_wood_up", + "line": 148, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_84_-3", + "source_tile": [ + 2, + 13 + ], + "target_map": "/shattered_islands/world_5_84_-2", + "target_tile": [ + 2, + 14 + ], + "arch": "stairs_wood_up", + "line": 303, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_84_-3", + "source_tile": [ + 2, + 14 + ], + "target_map": "/shattered_islands/world_5_84_-2", + "target_tile": [ + 2, + 14 + ], + "arch": "stairs_wood_up2", + "line": 338, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_84_-3", + "source_tile": [ + 3, + 7 + ], + "target_map": "/shattered_islands/world_5_84_-2", + "target_tile": [ + 3, + 7 + ], + "arch": "stairs_wood_up2", + "line": 389, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_84_-3", + "source_tile": [ + 3, + 8 + ], + "target_map": "/shattered_islands/world_5_84_-2", + "target_tile": [ + 3, + 7 + ], + "arch": "stairs_wood_up", + "line": 424, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_84_-3", + "source_tile": [ + 3, + 13 + ], + "target_map": "/shattered_islands/world_5_84_-2", + "target_tile": [ + 3, + 14 + ], + "arch": "stairs_wood_up", + "line": 579, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_84_-3", + "source_tile": [ + 3, + 14 + ], + "target_map": "/shattered_islands/world_5_84_-2", + "target_tile": [ + 3, + 14 + ], + "arch": "stairs_wood_up2", + "line": 614, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_5_85_-1", + "source_tile": [ + 18, + 20 + ], + "target_map": "/shattered_islands/world_5_85_-2", + "target_tile": [ + 18, + 20 + ], + "arch": "hole", + "line": 1523, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_5_85_-2", + "source_tile": [ + 18, + 20 + ], + "target_map": "/shattered_islands/world_5_85_-1", + "target_tile": [ + 18, + 20 + ], + "arch": "rope_up", + "line": 965, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_42", + "source_tile": [ + 2, + 17 + ], + "arch": "ladder_down", + "line": 439, + "form": "tiled-unresolved", + "vertical": "down", + "tiled_slot": 10 + }, + { + "source_map": "/shattered_islands/world_6_43_-1", + "source_tile": [ + 17, + 22 + ], + "target_map": "/shattered_islands/world_6_43_-2", + "target_tile": [ + 17, + 22 + ], + "arch": "stairs_stone_down", + "line": 1168, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_43_-2", + "source_tile": [ + 17, + 22 + ], + "target_map": "/shattered_islands/world_6_43_-1", + "target_tile": [ + 17, + 22 + ], + "arch": "stairs_stone_up", + "line": 396, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_44", + "source_tile": [ + 7, + 15 + ], + "target_map": "/shattered_islands/world_6_44_1", + "target_tile": [ + 7, + 15 + ], + "arch": "stairs_spiral_stone_up", + "line": 1205, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_44", + "source_tile": [ + 8, + 12 + ], + "target_map": "/shattered_islands/world_6_44_1", + "target_tile": [ + 8, + 12 + ], + "arch": "stairs_spiral_stone_up", + "line": 1351, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_44", + "source_tile": [ + 14, + 12 + ], + "target_map": "/shattered_islands/world_6_44_1", + "target_tile": [ + 14, + 12 + ], + "arch": "stairs_spiral_stone_up", + "line": 2452, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_44", + "source_tile": [ + 15, + 15 + ], + "target_map": "/shattered_islands/world_6_44_1", + "target_tile": [ + 15, + 15 + ], + "arch": "stairs_spiral_stone_up", + "line": 2649, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_44", + "source_tile": [ + 18, + 13 + ], + "target_map": "/shattered_islands/world_6_44_-1", + "target_tile": [ + 18, + 13 + ], + "arch": "stairs_stone_down", + "line": 3183, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_44", + "source_tile": [ + 21, + 2 + ], + "target_map": "/shattered_islands/world_6_44_-1", + "target_tile": [ + 21, + 2 + ], + "arch": "stairs_stone_down", + "line": 3602, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_44_-1", + "source_tile": [ + 18, + 13 + ], + "target_map": "/shattered_islands/world_6_44", + "target_tile": [ + 18, + 13 + ], + "arch": "stairs_stone_up", + "line": 2831, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_44_-1", + "source_tile": [ + 21, + 2 + ], + "target_map": "/shattered_islands/world_6_44", + "target_tile": [ + 21, + 2 + ], + "arch": "stairs_stone_up", + "line": 3220, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_44_1", + "source_tile": [ + 7, + 15 + ], + "target_map": "/shattered_islands/world_6_44", + "target_tile": [ + 7, + 15 + ], + "arch": "stairs_spiral_stone_down", + "line": 793, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_44_1", + "source_tile": [ + 8, + 12 + ], + "target_map": "/shattered_islands/world_6_44", + "target_tile": [ + 8, + 12 + ], + "arch": "stairs_spiral_stone_down", + "line": 859, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_44_1", + "source_tile": [ + 11, + 15 + ], + "target_map": "/shattered_islands/world_6_44_2", + "target_tile": [ + 11, + 15 + ], + "arch": "stairs_stone_up", + "line": 1174, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_44_1", + "source_tile": [ + 14, + 12 + ], + "target_map": "/shattered_islands/world_6_44", + "target_tile": [ + 14, + 12 + ], + "arch": "stairs_spiral_stone_down", + "line": 1610, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_44_1", + "source_tile": [ + 15, + 15 + ], + "target_map": "/shattered_islands/world_6_44", + "target_tile": [ + 15, + 15 + ], + "arch": "stairs_spiral_stone_down", + "line": 1750, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_44_2", + "source_tile": [ + 11, + 15 + ], + "target_map": "/shattered_islands/world_6_44_1", + "target_tile": [ + 11, + 15 + ], + "arch": "stairs_stone_down", + "line": 1024, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_45", + "source_tile": [ + 2, + 9 + ], + "target_map": "/shattered_islands/world_6_45_1", + "target_tile": [ + 2, + 9 + ], + "arch": "stairs_stone_up2", + "line": 446, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_45", + "source_tile": [ + 3, + 9 + ], + "target_map": "/shattered_islands/world_6_45_1", + "target_tile": [ + 2, + 9 + ], + "arch": "stairs_stone_up", + "line": 604, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_45_1", + "source_tile": [ + 2, + 9 + ], + "target_map": "/shattered_islands/world_6_45", + "target_tile": [ + 3, + 9 + ], + "arch": "stairs_stone_down", + "line": 171, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_45_1", + "source_tile": [ + 3, + 9 + ], + "target_map": "/shattered_islands/world_6_45", + "target_tile": [ + 3, + 9 + ], + "arch": "stairs_down", + "line": 232, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_46", + "source_tile": [ + 1, + 1 + ], + "target_map": "/shattered_islands/world_6_46_-1", + "target_tile": [ + 1, + 1 + ], + "arch": "stairs_stone_down", + "line": 240, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_46", + "source_tile": [ + 2, + 11 + ], + "target_map": "/shattered_islands/world_6_46", + "target_tile": [ + 2, + 11 + ], + "arch": "shop_mat", + "line": 542, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_46", + "source_tile": [ + 4, + 11 + ], + "target_map": "/shattered_islands/world_6_46", + "target_tile": [ + 4, + 11 + ], + "arch": "shop_mat", + "line": 941, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_46", + "source_tile": [ + 19, + 1 + ], + "target_map": "/shattered_islands/world_6_46", + "target_tile": [ + 19, + 1 + ], + "arch": "shop_mat", + "line": 3569, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_46", + "source_tile": [ + 19, + 11 + ], + "target_map": "/shattered_islands/world_6_46", + "target_tile": [ + 19, + 11 + ], + "arch": "shop_mat", + "line": 3631, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_46", + "source_tile": [ + 21, + 1 + ], + "target_map": "/shattered_islands/world_6_46", + "target_tile": [ + 21, + 1 + ], + "arch": "shop_mat", + "line": 3921, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_46", + "source_tile": [ + 21, + 11 + ], + "target_map": "/shattered_islands/world_6_46", + "target_tile": [ + 21, + 11 + ], + "arch": "shop_mat", + "line": 4017, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_46_-1", + "source_tile": [ + 1, + 1 + ], + "target_map": "/shattered_islands/world_6_46", + "target_tile": [ + 1, + 1 + ], + "arch": "stairs_stone_up", + "line": 49, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_48", + "source_tile": [ + 3, + 1 + ], + "target_map": "/shattered_islands/world_6_48_-1", + "target_tile": [ + 3, + 1 + ], + "arch": "ladder_down", + "line": 452, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_48", + "source_tile": [ + 4, + 12 + ], + "target_map": "/shattered_islands/world_6_48_1", + "target_tile": [ + 4, + 12 + ], + "arch": "stairs_wood_up2", + "line": 704, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_48", + "source_tile": [ + 5, + 12 + ], + "target_map": "/shattered_islands/world_6_48_1", + "target_tile": [ + 4, + 12 + ], + "arch": "stairs_wood_up", + "line": 861, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_48_-1", + "source_tile": [ + 3, + 1 + ], + "target_map": "/shattered_islands/world_6_48", + "target_tile": [ + 3, + 1 + ], + "arch": "ladder_up", + "line": 370, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_48_1", + "source_tile": [ + 4, + 12 + ], + "target_map": "/shattered_islands/world_6_48", + "target_tile": [ + 5, + 12 + ], + "arch": "stairs_wood_down", + "line": 492, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_48_1", + "source_tile": [ + 5, + 12 + ], + "target_map": "/shattered_islands/world_6_48", + "target_tile": [ + 5, + 12 + ], + "arch": "stairs_down", + "line": 590, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_51", + "source_tile": [ + 14, + 17 + ], + "target_map": "/shattered_islands/world_6_51_-1", + "target_tile": [ + 14, + 17 + ], + "arch": "stairs_down", + "line": 2019, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_51", + "source_tile": [ + 15, + 17 + ], + "target_map": "/shattered_islands/world_6_51_-1", + "target_tile": [ + 14, + 17 + ], + "arch": "stairs_stone_down", + "line": 2385, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_51", + "source_tile": [ + 16, + 6 + ], + "target_map": "/shattered_islands/ships/ship_2", + "target_tile": [ + 6, + 2 + ], + "arch": "stairs_wood_down", + "line": 2481, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_51", + "source_tile": [ + 18, + 18 + ], + "target_map": "/shattered_islands/world_6_51_-2", + "target_tile": [ + 18, + 18 + ], + "arch": "well_stone", + "line": 2948, + "form": "explicit", + "vertical": "up", + "vertical_inferred_from": "reciprocal well_bucket at /shattered_islands/world_6_51_-2 (18,18)" + }, + { + "source_map": "/shattered_islands/world_6_51_-1", + "source_tile": [ + 14, + 17 + ], + "target_map": "/shattered_islands/world_6_51", + "target_tile": [ + 15, + 17 + ], + "arch": "stairs_stone_up", + "line": 332, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_51_-1", + "source_tile": [ + 15, + 17 + ], + "target_map": "/shattered_islands/world_6_51", + "target_tile": [ + 15, + 17 + ], + "arch": "stairs_stone_up2", + "line": 413, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_51_-1", + "source_tile": [ + 18, + 21 + ], + "target_map": "/shattered_islands/world_6_51_-1", + "target_tile": [ + 18, + 21 + ], + "arch": "shop_mat", + "line": 748, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_51_-1", + "source_tile": [ + 20, + 21 + ], + "target_map": "/shattered_islands/world_6_51_-1", + "target_tile": [ + 20, + 21 + ], + "arch": "shop_mat", + "line": 997, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_51_-1", + "source_tile": [ + 22, + 11 + ], + "target_map": "/shattered_islands/world_6_51_-2", + "target_tile": [ + 22, + 11 + ], + "arch": "hole", + "line": 1127, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_51_-2", + "source_tile": [ + 18, + 18 + ], + "target_map": "/shattered_islands/world_6_51", + "target_tile": [ + 18, + 18 + ], + "arch": "well_bucket", + "line": 1450, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_51_-2", + "source_tile": [ + 19, + 16 + ], + "target_map": "/shattered_islands/world_6_51_-2", + "target_tile": [ + 18, + 17 + ], + "arch": "invis_exit", + "line": 1508, + "form": "same-map", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_54_-1", + "source_tile": [ + 5, + 5 + ], + "target_map": "/shattered_islands/world_6_54_-2", + "target_tile": [ + 5, + 5 + ], + "arch": "hole2", + "line": 96, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_54_-2", + "source_tile": [ + 5, + 5 + ], + "target_map": "/shattered_islands/world_6_54_-1", + "target_tile": [ + 5, + 5 + ], + "arch": "rope_up", + "line": 352, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_57", + "source_tile": [ + 9, + 14 + ], + "target_map": "/shattered_islands/world_6_57", + "target_tile": [ + 9, + 14 + ], + "arch": "well_stone", + "line": 1611, + "form": "same-map", + "vertical": "up", + "vertical_inferred_from": "reciprocal well_bucket at /shattered_islands/world_6_57_-1 (9,14)" + }, + { + "source_map": "/shattered_islands/world_6_57_-1", + "source_tile": [ + 9, + 14 + ], + "target_map": "/shattered_islands/world_6_57", + "target_tile": [ + 9, + 14 + ], + "arch": "well_bucket", + "line": 1598, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_66", + "source_tile": [ + 18, + 15 + ], + "arch": "hole", + "line": 1406, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_66_-1", + "source_tile": [ + 18, + 15 + ], + "target_map": "/shattered_islands/world_6_66", + "target_tile": [ + 19, + 15 + ], + "arch": "rope_up", + "line": 1216, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_67", + "source_tile": [ + 4, + 14 + ], + "target_map": "/shattered_islands/world_6_67", + "target_tile": [ + 4, + 14 + ], + "arch": "hole", + "line": 653, + "form": "same-map", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_6_67_-1", + "source_tile": [ + 4, + 14 + ], + "target_map": "/shattered_islands/world_6_67", + "target_tile": [ + 4, + 14 + ], + "arch": "rope_up", + "line": 253, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_6_67_-1", + "source_tile": [ + 8, + 9 + ], + "target_map": "/shattered_islands/strakewood_island/underground_city/underground_city_0_1_-2", + "target_tile": [ + 22, + 22 + ], + "arch": "portal_yellow", + "line": 665, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_72", + "source_tile": [ + 6, + 11 + ], + "target_map": "/shattered_islands/world_8_73", + "target_tile": [ + 11, + 10 + ], + "arch": "portal_green", + "line": 998, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_6_78", + "source_tile": [ + 19, + 21 + ], + "target_map": "/shattered_islands/ships/ship_1", + "target_tile": [ + 6, + 10 + ], + "arch": "stairs_wood_down", + "line": 2073, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_43_1", + "source_tile": [ + 1, + 15 + ], + "target_map": "/shattered_islands/world_7_43_2", + "target_tile": [ + 1, + 15 + ], + "arch": "stairs_spiral_stone_up", + "line": 96, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_43_2", + "source_tile": [ + 1, + 15 + ], + "target_map": "/shattered_islands/world_7_43_1", + "target_tile": [ + 1, + 15 + ], + "arch": "stairs_spiral_stone_down", + "line": 93, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_44", + "source_tile": [ + 13, + 13 + ], + "target_map": "/shattered_islands/world_7_44_1", + "target_tile": [ + 13, + 12 + ], + "arch": "stairs_stone_up", + "line": 2381, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_44_1", + "source_tile": [ + 1, + 18 + ], + "target_map": "/shattered_islands/world_7_44_2", + "target_tile": [ + 1, + 18 + ], + "arch": "stairs_spiral_stone_up", + "line": 227, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_44_1", + "source_tile": [ + 13, + 12 + ], + "target_map": "/shattered_islands/world_7_44", + "target_tile": [ + 13, + 13 + ], + "arch": "stairs_stone_down", + "line": 677, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_44_2", + "source_tile": [ + 1, + 18 + ], + "target_map": "/shattered_islands/world_7_44_1", + "target_tile": [ + 1, + 18 + ], + "arch": "stairs_spiral_stone_down", + "line": 183, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_45", + "source_tile": [ + 4, + 9 + ], + "target_map": "/shattered_islands/world_7_45_1", + "target_tile": [ + 4, + 9 + ], + "arch": "stairs_wood_up2", + "line": 821, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_45", + "source_tile": [ + 5, + 9 + ], + "target_map": "/shattered_islands/world_7_45_1", + "target_tile": [ + 5, + 9 + ], + "arch": "stairs_wood_up", + "line": 1006, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_45_-1", + "source_tile": [ + 20, + 16 + ], + "target_map": "/shattered_islands/world_7_45_-1", + "target_tile": [ + 23, + 13 + ], + "arch": "hole", + "line": 1160, + "form": "same-map", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_45_-1", + "source_tile": [ + 21, + 16 + ], + "target_map": "/shattered_islands/world_7_45_-1", + "target_tile": [ + 21, + 16 + ], + "arch": "shop_mat", + "line": 1253, + "form": "shop-mat", + "vertical": "down", + "vertical_inferred_from": "co-located hole" + }, + { + "source_map": "/shattered_islands/world_7_45_-1", + "source_tile": [ + 23, + 13 + ], + "target_map": "/shattered_islands/world_7_45_-1", + "target_tile": [ + 20, + 16 + ], + "arch": "hole", + "line": 1425, + "form": "same-map", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_45_-1", + "source_tile": [ + 23, + 16 + ], + "target_map": "/shattered_islands/world_7_45_-1", + "target_tile": [ + 23, + 16 + ], + "arch": "shop_mat", + "line": 1457, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_7_45_1", + "source_tile": [ + 4, + 9 + ], + "target_map": "/shattered_islands/world_7_45", + "target_tile": [ + 5, + 9 + ], + "arch": "stairs_wood_down", + "line": 363, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_45_1", + "source_tile": [ + 5, + 9 + ], + "target_map": "/shattered_islands/world_7_45", + "target_tile": [ + 5, + 9 + ], + "arch": "stairs_down", + "line": 433, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_46", + "source_tile": [ + 6, + 13 + ], + "target_map": "/shattered_islands/world_7_46_-1", + "target_tile": [ + 6, + 13 + ], + "arch": "sewer_hole1", + "line": 1407, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_46", + "source_tile": [ + 14, + 5 + ], + "arch": "stairs_wood_up", + "line": 2764, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_7_46_-1", + "source_tile": [ + 6, + 13 + ], + "target_map": "/shattered_islands/world_7_46", + "target_tile": [ + 6, + 13 + ], + "arch": "ladder_up", + "line": 385, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_47", + "source_tile": [ + 1, + 19 + ], + "target_map": "/shattered_islands/world_7_47_-1", + "target_tile": [ + 1, + 19 + ], + "arch": "sewer_hole2", + "line": 262, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_47", + "source_tile": [ + 9, + 9 + ], + "target_map": "/shattered_islands/world_7_47_-1", + "target_tile": [ + 9, + 9 + ], + "arch": "stairs_stone_down", + "line": 1483, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_47_-1", + "source_tile": [ + 1, + 19 + ], + "target_map": "/shattered_islands/world_7_47", + "target_tile": [ + 1, + 19 + ], + "arch": "ladder_up", + "line": 279, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_47_-1", + "source_tile": [ + 9, + 3 + ], + "target_map": "/shattered_islands/world_7_47", + "target_tile": [ + 9, + 3 + ], + "arch": "ladder_up", + "line": 1202, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_47_-1", + "source_tile": [ + 9, + 5 + ], + "target_map": "/shattered_islands/world_7_47", + "target_tile": [ + 9, + 5 + ], + "arch": "ladder_up", + "line": 1240, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_47_-1", + "source_tile": [ + 9, + 9 + ], + "target_map": "/shattered_islands/world_7_47", + "target_tile": [ + 9, + 9 + ], + "arch": "stairs_stone_up", + "line": 1272, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_48", + "source_tile": [ + 3, + 10 + ], + "target_map": "/shattered_islands/world_7_48_-1", + "target_tile": [ + 3, + 10 + ], + "arch": "hole", + "line": 497, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_48_-1", + "source_tile": [ + 3, + 10 + ], + "target_map": "/shattered_islands/world_7_48", + "target_tile": [ + 3, + 10 + ], + "arch": "ladder_up", + "line": 390, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_60", + "source_tile": [ + 17, + 8 + ], + "target_map": "/shattered_islands/world_7_60_-1", + "target_tile": [ + 17, + 8 + ], + "arch": "hole", + "line": 2260, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_64", + "source_tile": [ + 18, + 7 + ], + "target_map": "/shattered_islands/world_7_64_1", + "target_tile": [ + 18, + 7 + ], + "arch": "stairs_wood_up2", + "line": 2767, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_64", + "source_tile": [ + 19, + 7 + ], + "target_map": "/shattered_islands/world_7_64_1", + "target_tile": [ + 18, + 7 + ], + "arch": "stairs_wood_up", + "line": 2948, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_64", + "source_tile": [ + 21, + 13 + ], + "target_map": "/shattered_islands/world_7_64_-1", + "target_tile": [ + 21, + 13 + ], + "arch": "stairs_down", + "line": 3373, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_64", + "source_tile": [ + 21, + 14 + ], + "target_map": "/shattered_islands/world_7_64_-1", + "target_tile": [ + 21, + 13 + ], + "arch": "stairs_stone_down", + "line": 3389, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_64", + "source_tile": [ + 22, + 13 + ], + "target_map": "/shattered_islands/world_7_64_-1", + "target_tile": [ + 22, + 13 + ], + "arch": "stairs_down", + "line": 3564, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_64", + "source_tile": [ + 22, + 14 + ], + "target_map": "/shattered_islands/world_7_64_-1", + "target_tile": [ + 22, + 13 + ], + "arch": "stairs_stone_down", + "line": 3573, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_64_-1", + "source_tile": [ + 20, + 7 + ], + "target_map": "/shattered_islands/world_7_64_-2", + "target_tile": [ + 21, + 7 + ], + "arch": "stairs_stone_down", + "line": 1705, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_64_-1", + "source_tile": [ + 21, + 7 + ], + "target_map": "/shattered_islands/world_7_64_-2", + "target_tile": [ + 21, + 7 + ], + "arch": "stairs_down", + "line": 1790, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_64_-1", + "source_tile": [ + 21, + 13 + ], + "target_map": "/shattered_islands/world_7_64", + "target_tile": [ + 21, + 14 + ], + "arch": "stairs_stone_up", + "line": 1831, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_64_-1", + "source_tile": [ + 21, + 14 + ], + "target_map": "/shattered_islands/world_7_64", + "target_tile": [ + 21, + 14 + ], + "arch": "stairs_stone_up2", + "line": 1840, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_64_-1", + "source_tile": [ + 22, + 13 + ], + "target_map": "/shattered_islands/world_7_64", + "target_tile": [ + 22, + 14 + ], + "arch": "stairs_stone_up", + "line": 1905, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_64_-1", + "source_tile": [ + 22, + 14 + ], + "target_map": "/shattered_islands/world_7_64", + "target_tile": [ + 22, + 14 + ], + "arch": "stairs_stone_up2", + "line": 1914, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_64_-2", + "source_tile": [ + 20, + 7 + ], + "target_map": "/shattered_islands/world_7_64_-1", + "target_tile": [ + 20, + 7 + ], + "arch": "stairs_stone_up2", + "line": 1182, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_64_-2", + "source_tile": [ + 21, + 7 + ], + "target_map": "/shattered_islands/world_7_64_-1", + "target_tile": [ + 20, + 7 + ], + "arch": "stairs_stone_up", + "line": 1258, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_64_1", + "source_tile": [ + 14, + 7 + ], + "target_map": "/shattered_islands/world_7_64_2", + "target_tile": [ + 14, + 7 + ], + "arch": "stairs_wood_up2", + "line": 727, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_64_1", + "source_tile": [ + 15, + 7 + ], + "target_map": "/shattered_islands/world_7_64_2", + "target_tile": [ + 14, + 7 + ], + "arch": "stairs_wood_up", + "line": 804, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_64_1", + "source_tile": [ + 18, + 7 + ], + "target_map": "/shattered_islands/world_7_64", + "target_tile": [ + 19, + 7 + ], + "arch": "stairs_wood_down", + "line": 1037, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_64_1", + "source_tile": [ + 19, + 7 + ], + "target_map": "/shattered_islands/world_7_64", + "target_tile": [ + 19, + 7 + ], + "arch": "stairs_down", + "line": 1133, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_64_2", + "source_tile": [ + 14, + 7 + ], + "target_map": "/shattered_islands/world_7_64_1", + "target_tile": [ + 15, + 7 + ], + "arch": "stairs_wood_down", + "line": 888, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_64_2", + "source_tile": [ + 15, + 7 + ], + "target_map": "/shattered_islands/world_7_64_1", + "target_tile": [ + 15, + 7 + ], + "arch": "stairs_down", + "line": 985, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_65", + "source_tile": [ + 8, + 12 + ], + "target_map": "/shattered_islands/world_7_65_-1", + "target_tile": [ + 8, + 12 + ], + "arch": "invis_exit", + "line": 918, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_7_65", + "source_tile": [ + 9, + 12 + ], + "target_map": "/shattered_islands/world_7_65_-1", + "target_tile": [ + 9, + 12 + ], + "arch": "invis_exit", + "line": 1083, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_7_66_-1", + "source_tile": [ + 13, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_1_-1", + "target_tile": [ + 2, + 17 + ], + "arch": "hole", + "line": 938, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_66_-1", + "source_tile": [ + 16, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_1_0_-2", + "target_tile": [ + 2, + 2 + ], + "arch": "hole", + "line": 1146, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_66_-1", + "source_tile": [ + 19, + 15 + ], + "target_map": "/shattered_islands/strakewood_island/zechna_temple/zechna_temple_0_0_-3", + "target_tile": [ + 4, + 23 + ], + "arch": "hole", + "line": 1345, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_68", + "source_tile": [ + 12, + 20 + ], + "target_map": "/shattered_islands/world_7_68_-1", + "target_tile": [ + 12, + 20 + ], + "arch": "hole2", + "line": 1752, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_7_68_-1", + "source_tile": [ + 12, + 20 + ], + "target_map": "/shattered_islands/world_7_68", + "target_tile": [ + 12, + 20 + ], + "arch": "rope_up", + "line": 127, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_71", + "source_tile": [ + 19, + 22 + ], + "target_map": "/shattered_islands/world_6_72", + "target_tile": [ + 17, + 11 + ], + "arch": "exit_green", + "line": 2430, + "form": "explicit", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_7_77", + "source_tile": [ + 18, + 21 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/treehome_1", + "target_tile": [ + 4, + 4 + ], + "arch": "ladder_up", + "line": 2287, + "form": "explicit", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_7_78", + "source_tile": [ + 9, + 7 + ], + "target_map": "/shattered_islands/world_7_78", + "target_tile": [ + 9, + 7 + ], + "arch": "shop_mat", + "line": 1354, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_7_78", + "source_tile": [ + 11, + 7 + ], + "target_map": "/shattered_islands/world_7_78", + "target_tile": [ + 11, + 7 + ], + "arch": "shop_mat", + "line": 1646, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_8_45_-1", + "source_tile": [ + 3, + 12 + ], + "target_map": "/shattered_islands/world_8_45_-2", + "target_tile": [ + 3, + 12 + ], + "arch": "stairs_down", + "line": 361, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_8_45_-1", + "source_tile": [ + 3, + 13 + ], + "target_map": "/shattered_islands/world_8_45_-2", + "target_tile": [ + 3, + 12 + ], + "arch": "stairs_stone_down", + "line": 370, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_8_45_-1", + "source_tile": [ + 8, + 23 + ], + "target_map": "/shattered_islands/world_8_45", + "target_tile": [ + 8, + 23 + ], + "arch": "ladder_up", + "line": 959, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_45_-1", + "source_tile": [ + 10, + 20 + ], + "target_map": "/shattered_islands/world_8_45", + "target_tile": [ + 10, + 20 + ], + "arch": "ladder_up", + "line": 1158, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_45_-1", + "source_tile": [ + 10, + 23 + ], + "target_map": "/shattered_islands/world_8_45", + "target_tile": [ + 10, + 23 + ], + "arch": "ladder_up", + "line": 1221, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_45_-2", + "source_tile": [ + 3, + 12 + ], + "target_map": "/shattered_islands/world_8_45_-1", + "target_tile": [ + 3, + 13 + ], + "arch": "stairs_stone_up", + "line": 137, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_45_-2", + "source_tile": [ + 3, + 13 + ], + "target_map": "/shattered_islands/world_8_45_-1", + "target_tile": [ + 3, + 13 + ], + "arch": "stairs_stone_up2", + "line": 146, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_46", + "source_tile": [ + 1, + 5 + ], + "arch": "stairs_stone_up", + "line": 212, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_8_46", + "source_tile": [ + 9, + 5 + ], + "arch": "stairs_stone_up", + "line": 1649, + "form": "unresolved", + "vertical": "up", + "explicit_last_heal_0": true + }, + { + "source_map": "/shattered_islands/world_8_46_-1", + "source_tile": [ + 6, + 2 + ], + "target_map": "/shattered_islands/world_8_46", + "target_tile": [ + 6, + 2 + ], + "arch": "ladder_up", + "line": 240, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_46_-1", + "source_tile": [ + 8, + 2 + ], + "target_map": "/shattered_islands/world_8_46", + "target_tile": [ + 8, + 2 + ], + "arch": "ladder_up", + "line": 334, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_46_-1", + "source_tile": [ + 10, + 2 + ], + "target_map": "/shattered_islands/world_8_46", + "target_tile": [ + 10, + 2 + ], + "arch": "ladder_up", + "line": 428, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_58", + "source_tile": [ + 7, + 11 + ], + "target_map": "/shattered_islands/world_8_58_-1", + "target_tile": [ + 7, + 12 + ], + "arch": "stairs_stone_down", + "line": 1400, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_8_58", + "source_tile": [ + 7, + 12 + ], + "target_map": "/shattered_islands/world_8_58_-1", + "target_tile": [ + 7, + 12 + ], + "arch": "stairs_down", + "line": 1409, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_8_58", + "source_tile": [ + 8, + 11 + ], + "target_map": "/shattered_islands/world_8_58_-1", + "target_tile": [ + 8, + 12 + ], + "arch": "stairs_stone_down", + "line": 1602, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_8_58", + "source_tile": [ + 8, + 12 + ], + "target_map": "/shattered_islands/world_8_58_-1", + "target_tile": [ + 8, + 12 + ], + "arch": "stairs_down", + "line": 1611, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_8_58_-1", + "source_tile": [ + 7, + 11 + ], + "target_map": "/shattered_islands/world_8_58", + "target_tile": [ + 7, + 11 + ], + "arch": "stairs_stone_up2", + "line": 294, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_58_-1", + "source_tile": [ + 7, + 12 + ], + "target_map": "/shattered_islands/world_8_58", + "target_tile": [ + 7, + 11 + ], + "arch": "stairs_stone_up", + "line": 307, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_58_-1", + "source_tile": [ + 8, + 11 + ], + "target_map": "/shattered_islands/world_8_58", + "target_tile": [ + 8, + 11 + ], + "arch": "stairs_stone_up2", + "line": 344, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_58_-1", + "source_tile": [ + 8, + 12 + ], + "target_map": "/shattered_islands/world_8_58", + "target_tile": [ + 8, + 11 + ], + "arch": "stairs_stone_up", + "line": 357, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_63", + "source_tile": [ + 21, + 3 + ], + "target_map": "/shattered_islands/world_8_63_-1", + "target_tile": [ + 21, + 3 + ], + "arch": "hole", + "line": 3083, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_8_63_-1", + "source_tile": [ + 2, + 4 + ], + "arch": "hole2", + "line": 68, + "form": "unresolved", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_8_63_-1", + "source_tile": [ + 21, + 3 + ], + "target_map": "/shattered_islands/world_8_63", + "target_tile": [ + 21, + 3 + ], + "arch": "ladder_up", + "line": 1966, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_68", + "source_tile": [ + 17, + 6 + ], + "target_map": "/shattered_islands/world_8_68_-1", + "target_tile": [ + 17, + 6 + ], + "arch": "ladder_down2", + "line": 2344, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_8_68", + "source_tile": [ + 17, + 17 + ], + "target_map": "/shattered_islands/world_8_68_-1", + "target_tile": [ + 17, + 17 + ], + "arch": "ladder_down2", + "line": 2493, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_8_68_-1", + "source_tile": [ + 17, + 6 + ], + "target_map": "/shattered_islands/world_8_68", + "target_tile": [ + 17, + 6 + ], + "arch": "ladder_tall_up", + "line": 1363, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_68_-1", + "source_tile": [ + 17, + 17 + ], + "target_map": "/shattered_islands/world_8_68", + "target_tile": [ + 17, + 17 + ], + "arch": "ladder_tall_up", + "line": 1446, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_8_78", + "source_tile": [ + 6, + 23 + ], + "target_map": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance", + "target_tile": [ + 1, + 1 + ], + "arch": "well_stone", + "line": 989, + "form": "explicit", + "vertical": "down", + "vertical_inferred_from": "reciprocal ladder_up at /shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance (1,1)" + }, + { + "source_map": "/shattered_islands/world_8_79", + "source_tile": [ + 13, + 7 + ], + "target_map": "/shattered_islands/world_8_79", + "target_tile": [ + 13, + 7 + ], + "arch": "shop_mat", + "line": 2081, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_8_79", + "source_tile": [ + 13, + 9 + ], + "target_map": "/shattered_islands/world_8_79", + "target_tile": [ + 13, + 9 + ], + "arch": "shop_mat", + "line": 2097, + "form": "shop-mat", + "vertical": "none" + }, + { + "source_map": "/shattered_islands/world_9_41", + "source_tile": [ + 12, + 3 + ], + "target_map": "/shattered_islands/ships/ship_1", + "target_tile": [ + 12, + 18 + ], + "arch": "stairs_wood_down", + "line": 1499, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_42", + "source_tile": [ + 12, + 2 + ], + "target_map": "/shattered_islands/ships/ship_1", + "target_tile": [ + 12, + 2 + ], + "arch": "stairs_wood_down", + "line": 1680, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_42", + "source_tile": [ + 21, + 19 + ], + "target_map": "/shattered_islands/world_9_42_1", + "target_tile": [ + 21, + 19 + ], + "arch": "stairs_wood_up2", + "line": 3221, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_42", + "source_tile": [ + 21, + 20 + ], + "target_map": "/shattered_islands/world_9_42_1", + "target_tile": [ + 21, + 19 + ], + "arch": "stairs_wood_up", + "line": 3231, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_42_1", + "source_tile": [ + 21, + 19 + ], + "target_map": "/shattered_islands/world_9_42", + "target_tile": [ + 21, + 20 + ], + "arch": "stairs_wood_down", + "line": 112, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_42_1", + "source_tile": [ + 21, + 20 + ], + "target_map": "/shattered_islands/world_9_42", + "target_tile": [ + 21, + 20 + ], + "arch": "stairs_down", + "line": 121, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_42_2", + "source_tile": [ + 23, + 17 + ], + "target_map": "/shattered_islands/world_9_42_3", + "target_tile": [ + 23, + 17 + ], + "arch": "ladder_up", + "line": 234, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_42_3", + "source_tile": [ + 23, + 17 + ], + "target_map": "/shattered_islands/world_9_42_2", + "target_tile": [ + 23, + 17 + ], + "arch": "ladder_down3", + "line": 207, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44", + "source_tile": [ + 9, + 1 + ], + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 9, + 1 + ], + "arch": "stairs_stone_up2", + "line": 1788, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44", + "source_tile": [ + 9, + 2 + ], + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 9, + 2 + ], + "arch": "stairs_stone_up2", + "line": 1797, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44", + "source_tile": [ + 9, + 3 + ], + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 9, + 3 + ], + "arch": "stairs_stone_up2", + "line": 1806, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44", + "source_tile": [ + 10, + 1 + ], + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 9, + 1 + ], + "arch": "stairs_stone_up", + "line": 2007, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44", + "source_tile": [ + 10, + 2 + ], + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 9, + 2 + ], + "arch": "stairs_stone_up", + "line": 2016, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44", + "source_tile": [ + 10, + 3 + ], + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 9, + 3 + ], + "arch": "stairs_stone_up", + "line": 2025, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 9, + 1 + ], + "target_map": "/shattered_islands/world_9_44", + "target_tile": [ + 10, + 1 + ], + "arch": "stairs_stone_down", + "line": 1355, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 9, + 2 + ], + "target_map": "/shattered_islands/world_9_44", + "target_tile": [ + 10, + 2 + ], + "arch": "stairs_stone_down", + "line": 1363, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 9, + 3 + ], + "target_map": "/shattered_islands/world_9_44", + "target_tile": [ + 10, + 3 + ], + "arch": "stairs_stone_down", + "line": 1371, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 10, + 1 + ], + "target_map": "/shattered_islands/world_9_44", + "target_tile": [ + 10, + 1 + ], + "arch": "stairs_down", + "line": 1477, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 10, + 2 + ], + "target_map": "/shattered_islands/world_9_44", + "target_tile": [ + 10, + 2 + ], + "arch": "stairs_down", + "line": 1491, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 10, + 3 + ], + "target_map": "/shattered_islands/world_9_44", + "target_tile": [ + 10, + 3 + ], + "arch": "stairs_down", + "line": 1505, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 11, + 4 + ], + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 11, + 4 + ], + "arch": "stairs_stone_up2", + "line": 1660, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 11, + 5 + ], + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 11, + 4 + ], + "arch": "stairs_stone_up", + "line": 1669, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 12, + 4 + ], + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 12, + 4 + ], + "arch": "stairs_stone_up2", + "line": 1816, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 12, + 5 + ], + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 12, + 4 + ], + "arch": "stairs_stone_up", + "line": 1825, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 13, + 4 + ], + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 13, + 4 + ], + "arch": "stairs_stone_up2", + "line": 1948, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_1", + "source_tile": [ + 13, + 5 + ], + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 13, + 4 + ], + "arch": "stairs_stone_up", + "line": 1957, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 5, + 1 + ], + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 5, + 1 + ], + "arch": "stairs_stone_up2", + "line": 710, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 5, + 2 + ], + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 5, + 2 + ], + "arch": "stairs_stone_up2", + "line": 718, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 5, + 3 + ], + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 5, + 3 + ], + "arch": "stairs_stone_up2", + "line": 726, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 6, + 1 ], - "v2": [ - 105, - 1991, - -5 + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 5, + 1 ], - "ideal_v2": [ - 105, - 1991, - -1 + "arch": "stairs_stone_up", + "line": 838, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 6, + 2 + ], + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 5, + 2 + ], + "arch": "stairs_stone_up", + "line": 846, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 6, + 3 + ], + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 5, + 3 + ], + "arch": "stairs_stone_up", + "line": 854, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 11, + 4 + ], + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 11, + 3 + ], + "arch": "stairs_stone_down", + "line": 1696, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 11, + 5 + ], + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 11, + 5 + ], + "arch": "stairs_down", + "line": 1705, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 12, + 4 + ], + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 12, + 3 + ], + "arch": "stairs_stone_down", + "line": 1869, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 12, + 5 + ], + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 12, + 5 + ], + "arch": "stairs_down", + "line": 1878, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 13, + 4 ], - "blocked_by": [ - "world_3_82_-1 (gap, layer -2)", - "world_3_82_-1 (gap, layer -3)", - "world_3_83_-1 (gap, layer -1)", - "world_4_82_-1 (gap, layer -4)" - ] + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 13, + 3 + ], + "arch": "stairs_stone_down", + "line": 2020, + "form": "tiled", + "vertical": "down" }, { - "group": "/shattered_islands/strakewood_island/rockforge/rockforge_a", - "v1": [ - -13, - 1347, - -4 + "source_map": "/shattered_islands/world_9_44_2", + "source_tile": [ + 13, + 5 ], - "v2": [ - -13, - 1347, - -5 + "target_map": "/shattered_islands/world_9_44_1", + "target_tile": [ + 13, + 5 ], - "ideal_v2": [ - -13, - 1347, - -2 + "arch": "stairs_down", + "line": 2029, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 5, + 1 ], - "blocked_by": [ - "world_-4_56_-2 (gap, layer -2)", - "world_1_51_-3 (gap, layer -4)", - "world_2_52_-3 (gap, layer -3)" - ] + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 6, + 1 + ], + "arch": "stairs_stone_down", + "line": 790, + "form": "tiled", + "vertical": "down" }, { - "group": "/shattered_islands/strakewood_island/rockforge/rockforge_b", - "v1": [ - -13, - 1347, - -5 + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 5, + 2 + ], + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 6, + 2 + ], + "arch": "stairs_stone_down", + "line": 798, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 5, + 3 + ], + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 6, + 3 + ], + "arch": "stairs_stone_down", + "line": 806, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 6, + 1 ], - "v2": [ - -13, - 1347, - -6 + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 6, + 1 ], - "ideal_v2": [ - -13, - 1347, - -6 + "arch": "stairs_down", + "line": 915, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 6, + 2 + ], + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 6, + 2 + ], + "arch": "stairs_down", + "line": 930, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 6, + 3 + ], + "target_map": "/shattered_islands/world_9_44_2", + "target_tile": [ + 6, + 3 + ], + "arch": "stairs_down", + "line": 945, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 11, + 4 + ], + "target_map": "/shattered_islands/world_9_44_4", + "target_tile": [ + 11, + 4 + ], + "arch": "stairs_stone_up2", + "line": 1776, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 11, + 5 + ], + "target_map": "/shattered_islands/world_9_44_4", + "target_tile": [ + 11, + 4 + ], + "arch": "stairs_stone_up", + "line": 1785, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 12, + 4 + ], + "target_map": "/shattered_islands/world_9_44_4", + "target_tile": [ + 12, + 4 + ], + "arch": "stairs_stone_up2", + "line": 1933, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 12, + 5 + ], + "target_map": "/shattered_islands/world_9_44_4", + "target_tile": [ + 12, + 4 + ], + "arch": "stairs_stone_up", + "line": 1942, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 13, + 4 ], - "blocked_by": [] + "target_map": "/shattered_islands/world_9_44_4", + "target_tile": [ + 13, + 4 + ], + "arch": "stairs_stone_up2", + "line": 2073, + "form": "tiled", + "vertical": "up" }, { - "group": "/shattered_islands/strakewood_island/rockforge/rockforge_c", - "v1": [ - -13, - 1347, - -6 + "source_map": "/shattered_islands/world_9_44_3", + "source_tile": [ + 13, + 5 ], - "v2": [ - -13, - 1347, - -7 + "target_map": "/shattered_islands/world_9_44_4", + "target_tile": [ + 13, + 4 ], - "ideal_v2": [ - -13, - 1347, - -7 + "arch": "stairs_stone_up", + "line": 2082, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_44_4", + "source_tile": [ + 11, + 4 + ], + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 11, + 5 + ], + "arch": "stairs_stone_down", + "line": 1768, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_4", + "source_tile": [ + 11, + 5 + ], + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 11, + 5 + ], + "arch": "stairs_down", + "line": 1777, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_4", + "source_tile": [ + 12, + 4 + ], + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 12, + 5 + ], + "arch": "stairs_stone_down", + "line": 1931, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_4", + "source_tile": [ + 12, + 5 + ], + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 12, + 5 + ], + "arch": "stairs_down", + "line": 1940, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_44_4", + "source_tile": [ + 13, + 4 ], - "blocked_by": [] - } - ], - "exits_kept_as_decoration": [ + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 13, + 5 + ], + "arch": "stairs_stone_down", + "line": 2078, + "form": "tiled", + "vertical": "down" + }, { - "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_map": "/shattered_islands/world_9_44_4", "source_tile": [ - 2, - 1 + 13, + 5 ], - "arch": "ladder_up", - "line": 415, - "plane_source": [ - 281, - 1597, - -1 + "target_map": "/shattered_islands/world_9_44_3", + "target_tile": [ + 13, + 5 ], - "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", - "migrate_as": "decoration with the ladder_up look: no exit, no destination", - "would_land_on": { - "map": "/shattered_islands/world_11_66", - "tile": [ - 17, - 13 - ], - "target_from": "the tile above (plus the landing offset [0, 0])", - "plane_landing": [ - 281, - 1597, - 0 - ], - "plane_target": [ - 281, - 1597, - 0 - ], - "source_tile_objects": [ - "floor_marble_b1a", - "bed_1b.111", - "bedpost_b", - "bed_1b.111", - "ladder_up" - ], - "landing_tile_objects": [ - "water_lt2" - ], - "landing_floor_terrain": [ - "water" - ], - "landing_blocked": false, - "exits_on_source_tile": [], - "return_exits_near_landing": [], - "verdict": "doubtful", - "why": "the landing floor is water (terrain_type 2), which a walking player cannot enter (classic blocked(), server/src/server/map.c:697); the landing is 18 tile(s) from this group's nearest entrance on that layer" - } + "arch": "stairs_down", + "line": 2087, + "form": "tiled", + "vertical": "down" }, { - "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_map": "/shattered_islands/world_9_56", "source_tile": [ - 2, - 7 + 17, + 17 + ], + "target_map": "/shattered_islands/ships/ship_1", + "target_tile": [ + 1, + 18 + ], + "arch": "stairs_wood_down", + "line": 2546, + "form": "explicit", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_65", + "source_tile": [ + 6, + 10 + ], + "target_map": "/shattered_islands/world_9_65_-1", + "target_tile": [ + 6, + 10 + ], + "arch": "well_stone", + "line": 925, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_65_-1", + "source_tile": [ + 6, + 10 + ], + "target_map": "/shattered_islands/world_9_65", + "target_tile": [ + 6, + 10 + ], + "arch": "well_bucket", + "line": 860, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_65_-1", + "source_tile": [ + 11, + 18 + ], + "target_map": "/shattered_islands/world_9_65_-1", + "target_tile": [ + 11, + 21 + ], + "arch": "bollard_tied_s_2", + "line": 1500, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_65_-1", + "source_tile": [ + 11, + 19 + ], + "target_map": "/shattered_islands/world_9_65_-1", + "target_tile": [ + 11, + 18 + ], + "arch": "bollard_tied_s_2", + "line": 1517, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_65_-1", + "source_tile": [ + 11, + 20 + ], + "target_map": "/shattered_islands/world_9_65_-1", + "target_tile": [ + 11, + 21 + ], + "arch": "bollard_tied_s_2", + "line": 1534, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_65_-1", + "source_tile": [ + 11, + 21 + ], + "target_map": "/shattered_islands/world_9_65_-1", + "target_tile": [ + 11, + 18 + ], + "arch": "bollard_tied_s_2", + "line": 1556, + "form": "same-map", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_68", + "source_tile": [ + 1, + 2 + ], + "target_map": "/shattered_islands/world_9_68_-1", + "target_tile": [ + 1, + 2 + ], + "arch": "ladder_down2", + "line": 229, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_68", + "source_tile": [ + 1, + 3 + ], + "target_map": "/shattered_islands/world_9_68_1", + "target_tile": [ + 1, + 3 ], "arch": "ladder_up", - "line": 476, - "plane_source": [ - 281, - 1603, - -1 + "line": 238, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_68", + "source_tile": [ + 1, + 20 ], - "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", - "migrate_as": "decoration with the ladder_up look: no exit, no destination", - "would_land_on": { - "map": "/shattered_islands/world_11_66", - "tile": [ - 17, - 19 - ], - "target_from": "the tile above (plus the landing offset [0, 0])", - "plane_landing": [ - 281, - 1603, - 0 - ], - "plane_target": [ - 281, - 1603, - 0 - ], - "source_tile_objects": [ - "floor_marble_b1a", - "bed_1b.111", - "bedpost_b", - "bed_1b.111", - "ladder_up" - ], - "landing_tile_objects": [ - "water_lt" - ], - "landing_floor_terrain": [ - "water" - ], - "landing_blocked": false, - "exits_on_source_tile": [], - "return_exits_near_landing": [], - "verdict": "doubtful", - "why": "the landing floor is water (terrain_type 2), which a walking player cannot enter (classic blocked(), server/src/server/map.c:697); the landing is 12 tile(s) from this group's nearest entrance on that layer" - } + "target_map": "/shattered_islands/world_9_68_1", + "target_tile": [ + 1, + 20 + ], + "arch": "ladder_up", + "line": 425, + "form": "tiled", + "vertical": "up" }, { - "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_map": "/shattered_islands/world_9_68", "source_tile": [ - 2, - 13 + 1, + 21 + ], + "target_map": "/shattered_islands/world_9_68_-1", + "target_tile": [ + 1, + 21 + ], + "arch": "ladder_down2", + "line": 434, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_68_-1", + "source_tile": [ + 1, + 2 + ], + "target_map": "/shattered_islands/world_9_68", + "target_tile": [ + 1, + 2 ], "arch": "ladder_up", - "line": 541, - "plane_source": [ - 281, - 1609, - -1 + "line": 174, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_68_-1", + "source_tile": [ + 1, + 21 ], - "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", - "migrate_as": "decoration with the ladder_up look: no exit, no destination", - "would_land_on": { - "map": "/shattered_islands/world_11_67", - "tile": [ - 17, - 1 - ], - "target_from": "the tile above (plus the landing offset [0, 0])", - "plane_landing": [ - 281, - 1609, - 0 - ], - "plane_target": [ - 281, - 1609, - 0 - ], - "source_tile_objects": [ - "floor_marble_b1a", - "bed_1b.111", - "bedpost_b", - "bed_1b.111", - "ladder_up" - ], - "landing_tile_objects": [ - "floor_flagm3" - ], - "landing_floor_terrain": [ - "land" - ], - "landing_blocked": false, - "exits_on_source_tile": [], - "return_exits_near_landing": [], - "verdict": "plausible", - "why": "walkable land floor on the landing tile and no exit contradicts it; the landing is 6 tile(s) from this group's nearest entrance on that layer" - } + "target_map": "/shattered_islands/world_9_68", + "target_tile": [ + 1, + 21 + ], + "arch": "ladder_up", + "line": 275, + "form": "tiled", + "vertical": "up" }, { - "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_map": "/shattered_islands/world_9_68_1", "source_tile": [ - 13, - 1 + 1, + 3 + ], + "target_map": "/shattered_islands/world_9_68", + "target_tile": [ + 1, + 3 + ], + "arch": "ladder_down2", + "line": 167, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_68_1", + "source_tile": [ + 1, + 7 + ], + "target_map": "/shattered_islands/world_9_68_2", + "target_tile": [ + 1, + 7 ], "arch": "ladder_up", - "line": 2619, - "plane_source": [ - 292, - 1597, - -1 + "line": 198, + "form": "tiled", + "vertical": "up" + }, + { + "source_map": "/shattered_islands/world_9_68_1", + "source_tile": [ + 1, + 16 ], - "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", - "migrate_as": "decoration with the ladder_up look: no exit, no destination", - "would_land_on": { - "map": "/shattered_islands/world_12_66", - "tile": [ - 4, - 13 - ], - "target_from": "the tile above (plus the landing offset [0, 0])", - "plane_landing": [ - 292, - 1597, - 0 - ], - "plane_target": [ - 292, - 1597, - 0 - ], - "source_tile_objects": [ - "floor_marble_b1b", - "bed_1b.111", - "bedpost_b", - "bed_1b.111", - "ladder_up" - ], - "landing_tile_objects": [ - "water_lt" - ], - "landing_floor_terrain": [ - "water" - ], - "landing_blocked": false, - "exits_on_source_tile": [], - "return_exits_near_landing": [], - "verdict": "doubtful", - "why": "the landing floor is water (terrain_type 2), which a walking player cannot enter (classic blocked(), server/src/server/map.c:697); the landing is 18 tile(s) from this group's nearest entrance on that layer" - } + "target_map": "/shattered_islands/world_9_68_2", + "target_tile": [ + 1, + 16 + ], + "arch": "ladder_up", + "line": 251, + "form": "tiled", + "vertical": "up" }, { - "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_map": "/shattered_islands/world_9_68_1", "source_tile": [ - 13, + 1, + 20 + ], + "target_map": "/shattered_islands/world_9_68", + "target_tile": [ + 1, + 20 + ], + "arch": "ladder_down2", + "line": 282, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_68_2", + "source_tile": [ + 1, 7 ], - "arch": "ladder_up", - "line": 2688, - "plane_source": [ - 292, - 1603, - -1 + "target_map": "/shattered_islands/world_9_68_1", + "target_tile": [ + 1, + 7 ], - "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", - "migrate_as": "decoration with the ladder_up look: no exit, no destination", - "would_land_on": { - "map": "/shattered_islands/world_12_66", - "tile": [ - 4, - 19 - ], - "target_from": "the tile above (plus the landing offset [0, 0])", - "plane_landing": [ - 292, - 1603, - 0 - ], - "plane_target": [ - 292, - 1603, - 0 - ], - "source_tile_objects": [ - "floor_marble_b1a", - "bed_1b.111", - "bedpost_b", - "bed_1b.111", - "ladder_up" - ], - "landing_tile_objects": [ - "water_lt3" - ], - "landing_floor_terrain": [ - "water" - ], - "landing_blocked": false, - "exits_on_source_tile": [], - "return_exits_near_landing": [], - "verdict": "doubtful", - "why": "the landing floor is water (terrain_type 2), which a walking player cannot enter (classic blocked(), server/src/server/map.c:697); the landing is 12 tile(s) from this group's nearest entrance on that layer" - } + "arch": "ladder_down2", + "line": 273, + "form": "tiled", + "vertical": "down" }, { - "source_map": "/shattered_islands/strakewood_island/greyton/jail/jail", + "source_map": "/shattered_islands/world_9_68_2", "source_tile": [ - 13, + 1, + 16 + ], + "target_map": "/shattered_islands/world_9_68_1", + "target_tile": [ + 1, + 16 + ], + "arch": "ladder_down2", + "line": 334, + "form": "tiled", + "vertical": "down" + }, + { + "source_map": "/shattered_islands/world_9_69", + "source_tile": [ + 17, 13 ], - "arch": "ladder_up", - "line": 2757, - "plane_source": [ - 292, - 1609, - -1 - ], - "decision": "owner decision 2026-10-18: keep as bunk-bed decoration, an object without an exit", - "migrate_as": "decoration with the ladder_up look: no exit, no destination", - "would_land_on": { - "map": "/shattered_islands/world_12_67", - "tile": [ - 4, - 1 - ], - "target_from": "the tile above (plus the landing offset [0, 0])", - "plane_landing": [ - 292, - 1609, - 0 - ], - "plane_target": [ - 292, - 1609, - 0 - ], - "source_tile_objects": [ - "floor_marble_b1b", - "bed_1b.111", - "bedpost_b", - "bed_1b.111", - "ladder_up" - ], - "landing_tile_objects": [ - "water_lt3" - ], - "landing_floor_terrain": [ - "water" - ], - "landing_blocked": false, - "exits_on_source_tile": [], - "return_exits_near_landing": [], - "verdict": "doubtful", - "why": "the landing floor is water (terrain_type 2), which a walking player cannot enter (classic blocked(), server/src/server/map.c:697); the landing is 12 tile(s) from this group's nearest entrance on that layer" - } + "target_map": "/shattered_islands/world_9_69", + "target_tile": [ + 18, + 12 + ], + "arch": "exit", + "line": 2455, + "form": "same-map", + "vertical": "none", + "event_scripts": [ + "/python/generic/apartment_teleport.py" + ] } ], "groups": [ @@ -14441,11 +33496,6 @@ "vertical": "down", "source_group": "/shattered_islands/world", "target_group": "/shattered_islands/deserted_tutorial_island/ship_lower_deck", - "authored_target": [ - null, - null, - null - ], "wired": { "target_from": "the tile below (plus the landing offset [0, 1])", "plane_landing": [ @@ -15288,11 +34338,6 @@ "vertical": "up", "source_group": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance", "target_group": "/shattered_islands/world", - "authored_target": [ - null, - null, - null - ], "wired": { "target_from": "the tile above (plus the landing offset [0, 0])", "plane_landing": [ @@ -15357,11 +34402,6 @@ "vertical": "up", "source_group": "/shattered_islands/eld_woods_island/clearhaven/mine/mine_entrance", "target_group": "/shattered_islands/world", - "authored_target": [ - null, - null, - null - ], "wired": { "target_from": "the tile above (plus the landing offset [0, 0])", "plane_landing": [ @@ -27142,11 +46182,6 @@ "vertical": "up", "source_group": "/shattered_islands/strakewood_island/rockforge/rockforge", "target_group": "/shattered_islands/world", - "authored_target": [ - null, - null, - null - ], "wired": { "target_from": "the destination of the invis_exit on the same tile, 1 tile(s) from the landing", "plane_landing": [ @@ -27272,11 +46307,6 @@ "vertical": "up", "source_group": "/shattered_islands/strakewood_island/rockforge/rockforge", "target_group": "/shattered_islands/world", - "authored_target": [ - null, - null, - null - ], "wired": { "target_from": "the destination of the invis_exit on the same tile, 1 tile(s) from the landing", "plane_landing": [ @@ -118269,6 +137299,8 @@ "stairwell_exits_required": 89, "stairwell_intermediate_tiles": 76, "exit_endpoints_checked": 1456, + "exit_inventory_objects": 1200, + "exit_record_copies_checked": 1007, "relayering": { "overworld_tiles": 242824, "tile_heights": 82133, diff --git a/migration/placement-plan/scripts/build_plan.py b/migration/placement-plan/scripts/build_plan.py index bcdecd5..50d20fb 100644 --- a/migration/placement-plan/scripts/build_plan.py +++ b/migration/placement-plan/scripts/build_plan.py @@ -9,7 +9,10 @@ staging directory, verifies the staged plan, and only then replaces the artifacts in the output directory. Standard library only. - python3 -I scripts/build_plan.py [--content DIR] [--out DIR] [--allow-dirty] + python3 -I scripts/build_plan.py [--content DIR] [--out DIR] [--allow-dirty] [--check] + +--check regenerates in a temporary directory and fails unless every artifact +in --out is byte-identical; it writes nothing. Everything is deterministic: inputs are iterated in sorted order and every tie is broken lexicographically. @@ -19,6 +22,7 @@ import argparse import collections +import filecmp import functools import hashlib import json @@ -298,6 +302,76 @@ def occupied_rows(rec) -> list: return ["".join("#" if (x, y) in rec.occupied else "." for x in range(rec.width)) for y in range(rec.height)] +def placement_failed(res) -> bool: + return res is None or bool(res.get("failed")) + + +def place_pool(pool, connected, blockers, depth, place): + """Place the groups of `pool` one at a time, in reach order; return (order, failed, unplaced). + + connected(gid): gid has an exit to something already placed. blockers(gid, + remaining): unplaced groups that must be placed before gid. depth(gid): reach + depth, the first sort key. place(gid): the placement record, or None or + {"failed": True} when gid cannot be placed. A failed group stays unplaced: it + is not in `order`, it is never tried again, and nothing is placed through it, + so groups reachable only through it end up in `unplaced`. + """ + remaining = sorted(pool) + order, failed = [], [] + while remaining: + ready = [gid for gid in remaining if connected(gid)] + if not ready: + break + ok = [gid for gid in ready if not blockers(gid, set(remaining))] + pick = min(ok or ready, key=lambda gid: (depth(gid), gid)) + remaining.remove(pick) + (failed if placement_failed(place(pick)) else order).append(pick) + return order, failed, remaining + + +def exit_source_fields(e: dict) -> dict: + """The fields of an exit that come from the content alone, in record order. + + The exit records and the exit inventory both use them, so the verifier can + compare every record with the exit object it was made from.""" + r = {"source_map": e["source_map"], "source_tile": [e["source_x"], e["source_y"]]} + if e.get("resolved_map"): + r["target_map"] = e["resolved_map"] + r["target_tile"] = [e["resolved_x"], e["resolved_y"]] + r.update(arch=e["arch"], line=e["line"], form=e["form"], vertical=e["vdir"]) + if e.get("target_map") is not None and e["target_map"] != e.get("resolved_map"): + r["authored_target"] = [e["target_map"], e.get("target_x"), e.get("target_y")] + if e.get("target_default_entry"): + r["target_is_map_default_entry"] = True + if e["per_player"]: + r["per_player_last_eat_1"] = True + if e["event_scripts"]: + r["event_scripts"] = e["event_scripts"] + if e.get("dir_inferred_from"): + r["vertical_inferred_from"] = e["dir_inferred_from"] + return r + + +def inventory_entry(e: dict) -> dict: + """One exit object as read from the content, before placement or wiring. + + Besides the record fields it carries the reader's facts the data problems + are derived from: the tile-link slot of a tiled exit without a link, the + authored path of a dynamic exit, a target problem with its authored target, + and an explicit last_heal 0.""" + rec = exit_source_fields(e) + if e["form"] == "tiled-unresolved": + rec["tiled_slot"] = e["tiled_slot"] + if e["form"] == "dynamic": + rec["dynamic_target"] = e.get("raw_target") + if e.get("problem"): + rec["problem"] = e["problem"] + rec["problem_target"] = [e.get("target_map"), e.get("target_x"), e.get("target_y")] + if e.get("explicit_last_heal_0"): + rec["explicit_last_heal_0"] = True + return rec + + # --------------------------------------------------------------------- main def build(content: Path, allow_dirty: bool = False): revision = content_revision(content) @@ -1085,20 +1159,13 @@ def above_below_blockers(gid, pool): return blockers def run_pool(pool, tentative): - remaining = sorted(pool) - order = [] - while remaining: - ready = [gid for gid in remaining if connections_to_placed(gid)] - if not ready: - break - ok = [gid for gid in ready if not above_below_blockers(gid, set(remaining))] - pick_from = ok or ready - pick = min(pick_from, key=lambda gid: (G[gid]["reach_depth"] if G[gid]["reach_depth"] is not None else 99, gid)) - res = place_group(pick, tentative=tentative) - S.placements[pick] = res - order.append(pick) - remaining.remove(pick) - return order, remaining + def place(gid): + res = place_group(gid, tentative=tentative) + S.placements[gid] = {"failed": True, "ideal": None} if res is None else res + return res + + return place_pool(pool, lambda gid: bool(connections_to_placed(gid)), above_below_blockers, + lambda gid: G[gid]["reach_depth"] if G[gid]["reach_depth"] is not None else 99, place) ow_relay = relay[ow_id] ow_pieces = ow_relay.pieces # map -> final layer -> plane tiles @@ -1172,17 +1239,23 @@ def run_placement(radius): if ts: S.occ.add(m, L, functools.partial(_piece, m, L), bbox=fp.bbox_of(ts)) build_relayer_stairwells() - o1, l1 = run_pool(to_place, False) - o2, l2 = run_pool(tentative_ids, True) - o3 = [] + o1, f1, l1 = run_pool(to_place, False) + o2, f2, l2 = run_pool(tentative_ids, True) + o3, f3, s3 = [], [], [] for gid, (anchor, dl, why) in sorted(TENTATIVE_ALIGN.items()): - if gid in G and gid not in S.placed and anchor in S.placed: + if gid in G and gid not in S.placements and anchor not in S.placed: + s3.append(gid) # its anchor is not placed: the group stays unplaced + # only for groups no pool tried: a group whose own placement failed stays failed + if gid in G and gid not in S.placements and anchor in S.placed: a = S.placed[anchor] res = place_group(gid, tentative=True, forced={"dx": a["dx"], "dy": a["dy"], "L": a["L"] + dl}) - if res: - res["tentative_reason"] = why - S.placements[gid] = res - o3.append(gid) + if placement_failed(res): + S.placements[gid] = {"failed": True, "ideal": (res or {}).get("ideal")} + f3.append(gid) + continue + res["tentative_reason"] = why + S.placements[gid] = res + o3.append(gid) # view conflicts against the final state (stairwell tiles count as their owner's tiles); # every stairwell is drawn only on the player's layer (owner decision 2026-10-18), so # the conflicts are listed but hidden in the engine. @@ -1209,7 +1282,9 @@ def run_placement(radius): for sw in S.stairwells: n[sw["kind"]] += 1 sw["id"] = "{}{}".format(prefix[sw["kind"]], n[sw["kind"]]) - return o1, l1, o2 + o3, l2 + # A failed group stays unplaced (and fails verification, so the plan is not published). + # an uncertain group no pool reached may still get its tentative spot from an alignment hint + return o1, sorted(l1 + f1), o2 + o3, sorted((set(l2) - set(o3)) | set(f2)), sorted(f1 + f2 + f3), s3 def becomes_counts(): c = collections.Counter() @@ -1274,22 +1349,11 @@ def portal_reason(e, s, t, dist, dl): return out def exit_record(e, with_eval=True): - r = { - "source_map": e["source_map"], "source_tile": [e["source_x"], e["source_y"]], - "target_map": e["resolved_map"], "target_tile": [e["resolved_x"], e["resolved_y"]], - "arch": e["arch"], "line": e["line"], "form": e["form"], "vertical": e["vdir"], - "source_group": e["source_group"], "target_group": e["target_group"], - } - if e["resolved_map"] != e.get("target_map"): - r["authored_target"] = [e.get("target_map"), e.get("target_x"), e.get("target_y")] - if e.get("target_default_entry"): - r["target_is_map_default_entry"] = True - if e["per_player"]: - r["per_player_last_eat_1"] = True - if e["event_scripts"]: - r["event_scripts"] = e["event_scripts"] - if e.get("dir_inferred_from"): - r["vertical_inferred_from"] = e["dir_inferred_from"] + src = exit_source_fields(e) + r = {k: src.pop(k) for k in ("source_map", "source_tile", "target_map", "target_tile", "arch", "line", + "form", "vertical")} + r.update(source_group=e["source_group"], target_group=e["target_group"]) + r.update(src) if e.get("wired"): r["wired"] = e["wired"] if with_eval: @@ -1305,7 +1369,7 @@ def exit_record(e, with_eval=True): return r # Sensitivity: the same placement with the shipped client's view radius. - run_placement(CLIENT_VIEW_RADIUS) + _o, sens_left1, _t, sens_left2, sens_failed, sens_skipped = run_placement(CLIENT_VIEW_RADIUS) sens_placed = {gid: dict(p) for gid, p in S.placed.items()} sensitivity = { "view_radius_tiles": CLIENT_VIEW_RADIUS, @@ -1314,8 +1378,10 @@ def exit_record(e, with_eval=True): "crossing_exit_objects_after_placement": dict(sorted(becomes_counts().items())), "stairwells": len(S.stairwells), "stairwell_sites_blocked": len(S.blocked_sites), + "groups_unplaced": sorted(set(sens_left1) | set(sens_left2) | set(sens_failed) | set(sens_skipped)), } - order1, left1, order_t, left2 = run_placement(VIEW_RADIUS) + # an alignment group skipped for want of its anchor fails verification (its hint is not applied) + order1, left1, order_t, left2, failed_placements, _skipped = run_placement(VIEW_RADIUS) place_index = {gid: i for i, gid in enumerate(S.order)} placements = S.placements sensitivity["groups_placed_differently"] = sorted( @@ -1427,6 +1493,9 @@ def tile_objects(m, x, y): e2 = dict(e, form="wired", resolved_map=tm, resolved_x=tx, resolved_y=ty, target_group=gof[tm]) e2["wired"] = evidence wired.append(e2) + if sg == gof[tm] == ow_id: + ow_internal.append(e2) # wired to another overworld map: an overworld exit, not a crossing + continue cross.append(e2) G[sg]["exits_out"].append(e2) G[gof[tm]]["entrances"].append(e2) @@ -1907,7 +1976,7 @@ def bbox_layers(gid, dx=0, dy=0, L=0): counts["{}:{}".format(g["classification"], g["reason"])] += 1 sw_spans = collections.Counter(sw["top_layer"] - sw["bottom_layer"] for sw in S.stairwells) plan = { - "schema": "atrinik-overworld-placement-plan/3", + "schema": "atrinik-overworld-placement-plan/4", "generated_by": "scripts/build_plan.py", "content_root": str(content), "content_git_head": revision["head"], @@ -1919,6 +1988,8 @@ def bbox_layers(gid, dx=0, dy=0, L=0): "stairwell_max_bend_tiles": STAIRWELL_MAX_BEND, "layer_height_z": LAYER_HEIGHT_Z, "relayer_min_region_tiles": relayer.MIN_REGION_TILES, "walkable_dz": relayer.WALKABLE_DZ, "owner_keep_as_decoration": [[m, list(t), a] for (m, t, a) in sorted(OWNER_KEEP_AS_DECORATION)], + "tentative_alignments": [{"group": gid, "anchor": anchor, "layer_delta": dl, "why": why} + for gid, (anchor, dl, why) in sorted(TENTATIVE_ALIGN.items())], "overworld_map_size": 24, "excluded_trees": list(md.EXCLUDED_TOP)}, "conventions": { "plane": "overworld plane; layer 0 surface, negative below, positive above", @@ -1959,6 +2030,10 @@ def bbox_layers(gid, dx=0, dy=0, L=0): "layer above or below (see its 'wired' evidence)", "exits_kept_as_decoration": "dangling exits the owner keeps as decoration (no exit): not wired, with the " "landing they would have had", + "exit_inventory": "every exit object read from the content (inventory exits excluded), keyed by " + "source_map and line: the content fields of its exit record with its legacy form " + "and resolved target (none for an exit that leads nowhere), plus the facts its data " + "problems come from; the exit records and data problems are checked against it", }, "summary": {"maps_total": len(maps), "groups_total": len(groups), "non_overworld_groups": len(groups) - 1, "by_classification": dict(sorted(counts.items())), @@ -1980,7 +2055,8 @@ def bbox_layers(gid, dx=0, dy=0, L=0): "seams": len(seams_out), "new_stairs": len(new_stairs), "steep_steps": len(steep_out), "new_contacts": len(contacts_out)}, "placement_order": order1, "tentative_placement_order": order_t, - "unplaced_place_groups": left1, "unplaced_uncertain_groups": left2}, + "unplaced_place_groups": left1, "unplaced_uncertain_groups": left2, + "failed_placements": failed_placements}, "start_map": start_map, "view_radius_sensitivity": sensitivity, "stairwells": S.stairwells, @@ -1988,6 +2064,7 @@ def bbox_layers(gid, dx=0, dy=0, L=0): "sloped_stairwell_sites_blocked": S.slope_failed, "placement_changes_from_v1": changes_v1, "exits_kept_as_decoration": kept_decoration, + "exit_inventory": sorted((inventory_entry(e) for e in exits), key=lambda r: (r["source_map"], r["line"])), "groups": groups_out, "relayering": relayering, "data_problems": problems, @@ -2018,15 +2095,44 @@ def publish(stage: Path, out: Path) -> list: return new +MAX_CHECK_ENTRIES = 4096 # the output directory holds a few dozen artifacts and the scripts' folder + + +def compare_artifacts(stage: Path, out: Path): + """Artifacts that differ between a fresh run in `stage` and the published set in `out`, or None if + `out` has more than MAX_CHECK_ENTRIES entries (it is then not a plan's output directory). + + Sizes are compared first and contents in fixed-size chunks (filecmp), so an + oversized file in `out` is a difference, not a whole-file read.""" + names = set() + for d in (stage, out): + with os.scandir(d) as it: + for n_, entry in enumerate(it): + if n_ >= MAX_CHECK_ENTRIES: + return None + if ARTIFACT_RE.match(entry.name): + names.add(entry.name) + return [n for n in sorted(names) if not ((stage / n).is_file() and (out / n).is_file() + and filecmp.cmp(stage / n, out / n, shallow=False))] + + def main(argv=None): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--content", type=Path, default=DEFAULT_CONTENT) ap.add_argument("--out", type=Path, default=DEFAULT_OUT) ap.add_argument("--allow-dirty", action="store_true", help="allow uncommitted changes in the content checkout (experiments only; recorded in the JSON)") + ap.add_argument("--check", action="store_true", + help="regenerate in a temporary directory and fail unless every artifact in --out is " + "byte-identical (binds the published plan, its exit inventory included, to the content; " + "use the --content path the plan records); writes nothing") args = ap.parse_args(argv) out = args.out.resolve() - out.mkdir(parents=True, exist_ok=True) + if not args.check: + out.mkdir(parents=True, exist_ok=True) + elif not out.is_dir(): + print("nothing to check: {} is not a directory".format(out)) + return 1 plan, maps, G, ow_id, ow_relay = build(args.content.resolve(), allow_dirty=args.allow_dirty) print(json.dumps(plan["summary"], indent=1)) if not plan["verification"]["ok"]: @@ -2035,7 +2141,7 @@ def main(argv=None): import render # noqa: E402 import report # noqa: E402 import verify_plan # noqa: E402 - with tempfile.TemporaryDirectory(prefix=".placement-plan-stage-", dir=out) as tmp: + with tempfile.TemporaryDirectory(prefix=".placement-plan-stage-", dir=None if args.check else out) as tmp: stage = Path(tmp) (stage / "placement-plan.json").write_text(json.dumps(plan, indent=1, sort_keys=False) + "\n", encoding="utf-8") @@ -2045,6 +2151,18 @@ def main(argv=None): if not staged["ok"] or staged != plan["verification"]: print("staged plan failed verification; nothing written:", staged["errors"][:10]) return 1 + if args.check: + differ = compare_artifacts(stage, out) + if differ is None: + print("check FAILED: {} has more than {} entries".format(out, MAX_CHECK_ENTRIES)) + return 1 + if differ: + print("check FAILED: {} artifact(s) in {} differ from a fresh run: {}{}".format( + len(differ), out, ", ".join(differ[:10]), ", ..." if len(differ) > 10 else "")) + return 1 + print("check: all {} artifacts in {} are byte-identical to a fresh run".format( + sum(1 for p in out.iterdir() if ARTIFACT_RE.match(p.name)), out)) + return 0 written = publish(stage, out) print("wrote {} artifacts to {}".format(len(written), out)) print("verification: OK") diff --git a/migration/placement-plan/scripts/report.py b/migration/placement-plan/scripts/report.py index 5f5c93f..fe5dc9e 100644 --- a/migration/placement-plan/scripts/report.py +++ b/migration/placement-plan/scripts/report.py @@ -160,10 +160,12 @@ def write_report(plan, out: Path): sum(1 for c in rl["caves"] if c["choice"] == "lower-regions-in-place"), rs["maps_moved"], plain_kept, plain_well, rs["new_stairs"], rs["seams"])) c = s["crossing_exit_objects_after_placement"] + wired_crossing = sum(1 for e in wired_exits(plan) + if e["source_group"] != e["target_group"] and e.get("becomes") == "stairs") w("- Exit objects crossing group boundaries after placement (both directions, every exit object counted): " "**{} become plain stairs** ({} of them wired dangling exits), **{} are replaced by stairwells**, **{} stay " "portals**. Another {} exit objects lead into separate planes and stay portals by definition.".format( - c.get("stairs", 0), s["wired_exits"], c.get("stairwell", 0), c.get("portal", 0), + c.get("stairs", 0), wired_crossing, c.get("stairwell", 0), c.get("portal", 0), s["crossing_exit_objects_into_separate_planes"])) w("- **Stairwells: {}**: {} for placed groups (SW), {} sloped (SL), {} for overworld exits the relayering " "put 2 or more layers apart (SR). Every one is drawn only on the layer the player is on.".format( @@ -197,11 +199,14 @@ def write_report(plan, out: Path): vr = ver.get("relayering", {}) w("- Independent check (`scripts/verify_plan.py`, JSON only): **{}**: {} placed member maps, {} nearby owner " "pairs measured (closest unrelated pair {} tiles apart, gap rule needs more than {}), {} stairwells with " - "{} intermediate tiles, {} exit endpoints re-derived; relayering re-derived from {} overworld tiles and {} " + "{} intermediate tiles, {} exit record copies matched against the inventory of {} exit objects, {} exit " + "endpoints re-derived; relayering re-derived from {} overworld tiles and {} " "tile heights ({} regions, {} units, {} moved tiles, {} cut edges in {} seam pieces).".format( "PASS" if ver.get("ok") else "FAIL", ver.get("member_maps"), ver.get("owner_pairs_checked"), (ver.get("closest_pairs", {}).get("unrelated") or ["-"])[0], R, ver.get("stairwells_checked"), - ver.get("stairwell_intermediate_tiles"), ver.get("exit_endpoints_checked"), num(vr.get("overworld_tiles", 0)), + ver.get("stairwell_intermediate_tiles"), num(ver.get("exit_record_copies_checked", 0)), + num(ver.get("exit_inventory_objects", 0)), ver.get("exit_endpoints_checked"), + num(vr.get("overworld_tiles", 0)), num(vr.get("tile_heights", 0)), vr.get("regions"), vr.get("units"), num(vr.get("moved_tiles", 0)), vr.get("cut_edges"), vr.get("seam_pieces"))) w("") @@ -284,9 +289,12 @@ def write_report(plan, out: Path): "(`client/src/client/commands.c:139-140`). That is **10 tiles** each way.") w("- Replacement stack: neither `{}` nor `{}` defines a view radius yet.".format(CLIENT, SERVER)) w("- **The plan uses {} tiles**, the server's bound. Comparison run with the shipped client's {} tiles " - "(`view_radius_sensitivity` in the JSON): {}; {} stairwells; groups that would sit elsewhere: {}.".format( + "(`view_radius_sensitivity` in the JSON): {}; {} stairwells; groups that would sit elsewhere: {}{}.".format( R, sens["view_radius_tiles"], fmt_counts(sens["crossing_exit_objects_after_placement"]), - sens["stairwells"], ", ".join("`{}`".format(base(x)) for x in sens["groups_placed_differently"]) or "none")) + sens["stairwells"], ", ".join("`{}`".format(base(x)) for x in sens["groups_placed_differently"]) or "none", + "; groups that could not be placed: {}".format(", ".join("`{}`".format(base(x)) for x in + sens["groups_unplaced"])) + if sens.get("groups_unplaced") else "")) w("- Other layers: classic also streams the linked levels from 6 below to 6 above " "(`MAP2_MAX_DEPTH 6`, `libatrinik/socket.h:87-89`), but levels hidden from the camera are pruned before " "they are sent (`server/src/server/los.c:737`); a level below the player is hidden by the floor of the " @@ -486,9 +494,10 @@ def placed_table(rows): def wired_exits(plan): + """Every wired exit: crossing ones and those wired between two overworld maps (internal_exits).""" out, seen = [], set() for g in plan["groups"]: - for _key, e in crossing(g): + for e in [e for _key, e in crossing(g)] + g.get("internal_exits", []): k = (e["source_map"], e["line"]) if e.get("form") == "wired" and k not in seen: seen.add(k) @@ -1089,7 +1098,9 @@ def write_reproducing(plan, w): w("```sh") w("cd migration/placement-plan") w("python3 -I scripts/build_plan.py --content {} --out .".format(plan["content_root"])) - w("python3 -I scripts/verify_plan.py placement-plan.json") + w("python3 -I scripts/verify_plan.py placement-plan.json --content {}".format(plan["content_root"])) + w("python3 -I scripts/test_placement_plan.py") + w("python3 -I scripts/build_plan.py --content {} --out . --check".format(plan["content_root"])) w("```") w("") w("`--content` must be a clean checkout of `atrinik/content` at `{}` (the shared checkout was behind `main`, " @@ -1097,7 +1108,16 @@ def write_reproducing(plan, w): "library (the PNGs are written by `raster.py`). The content checkout must be clean under `arch/` and " "`maps/` (`--allow-dirty` is for experiments and marks the JSON). Artifacts are generated in a staging " "directory, the staged JSON is verified again, and only then are `placement-plan.json`, `REPORT.md` and the " - "PNGs replaced; a failed run leaves the old artifacts in place.".format(plan["content_git_head"][:12])) + "PNGs replaced; a failed run leaves the old artifacts in place. A group that cannot be placed stays unplaced " + "and fails verification, so such a plan is never published. `test_placement_plan.py` tampers with copies of " + "the committed JSON and checks that the verifier rejects each one; it needs no content checkout. " + "`verify_plan.py` makes two checks. Without `--content` it re-derives the placement rules from the JSON " + "alone. With `--content` it also rebuilds the plan from the content with the generator and requires the " + "published JSON to equal the rebuild field for field (the checkout path aside), so an edit that keeps the " + "JSON consistent with itself (an exit dropped from `exit_inventory` together with its count, say) fails " + "there. `build_plan.py --check` regenerates every artifact in a temporary directory and fails unless each " + "is byte-identical (run it with the `--content` path the plan records).".format( + plan["content_git_head"][:12])) w("") w("The run is deterministic (sorted inputs, lexicographic tie-breaks) and read-only against the content " "checkout; rerunning with the same arguments on the same content rewrites `placement-plan.json`, " diff --git a/migration/placement-plan/scripts/test_placement_plan.py b/migration/placement-plan/scripts/test_placement_plan.py new file mode 100644 index 0000000..d09bb86 --- /dev/null +++ b/migration/placement-plan/scripts/test_placement_plan.py @@ -0,0 +1,987 @@ +#!/usr/bin/env python3 +"""Negative tests for the placement-plan verifier and generator (standard library only). + + python3 -I scripts/test_placement_plan.py [--jobs N] + PLACEMENT_PLAN_CONTENT=DIR python3 -I scripts/test_placement_plan.py + +Each verifier test tampers with a copy of the committed placement-plan.json +and checks that verify_plan.verify() rejects it with the expected error. The +generator tests drive build_plan.place_pool() with stand-in callbacks. Neither +needs a content checkout. With PLACEMENT_PLAN_CONTENT naming a checkout at the +plan's content_git_head, ContentFidelity also checks that verify_content() +rejects edits that keep the JSON consistent with itself; without it, those +tests are skipped. Each test runs the full verifier, so the tests run in up +to 8 worker processes (--jobs 1 runs them in this process). +""" + +from __future__ import annotations + +import json +import os +import sys +import unittest +from pathlib import Path + +sys.dont_write_bytecode = True +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import build_plan # noqa: E402 +import verify_plan # noqa: E402 + +PLAN = Path(__file__).resolve().parents[1] / "placement-plan.json" +INDEX = ("entrances_from_overworld", "exits_to_overworld", "entrances_from_other_groups", "exits_to_other_groups", + "internal_exits") +MINE = "/shattered_islands/eld_woods_island/clearhaven/mine/" +CROSSING = (MINE + "mine_entrance_a", 641) # ladder_up to mine_entrance: one copy in each group + + +def group(plan, gid): + return next(g for g in plan["groups"] if g["id"] == gid) + + +def copies(plan, key): + """(group, list name, record) for every copy of exit `key` in the exit index.""" + return [(g, lst, e) for g in plan["groups"] for lst in INDEX for e in g.get(lst, []) + if (e["source_map"], e["line"]) == key] + + +def delete_copies(plan, key, only_group=None): + for g, lst, e in copies(plan, key): + if only_group is None or g["id"] == only_group: + g[lst].remove(e) + + +class VerifierTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.text = PLAN.read_text(encoding="utf-8") + + def plan(self): + return json.loads(self.text) + + def assertCaught(self, plan, *needles): + res = verify_plan.verify(plan) + self.assertFalse(res["ok"], "tampered plan verified") + for needle in needles: + self.assertTrue(any(needle in e for e in res["errors"]), + "no error mentions {!r}; errors: {}".format(needle, res["errors"][:8])) + + +class CommittedPlan(VerifierTest): + def test_committed_plan_verifies(self): + res = verify_plan.verify(self.plan()) + self.assertTrue(res["ok"], res["errors"][:8]) + self.assertEqual(res["exit_inventory_objects"], len(self.plan()["exit_inventory"])) + + def test_a_malformed_plan_fails_without_a_crash(self): + for change in (lambda p: p["exit_inventory"][0].update(source_tile=None), + lambda p: p.pop("stairwells"), + lambda p: p["groups"][0].update(maps=None)): + p = self.plan() + change(p) + res = verify_plan.verify(p) + self.assertFalse(res["ok"]) + self.assertTrue(res["errors"]) + + def test_booleans_and_floats_are_not_numbers(self): + key = ("/shattered_islands/berri_lur/temple_loki/tol_0201", 441) + p = self.plan() + for _g, _lst, e in copies(p, key): + e["layer_delta"] = True + for r in p["relayering"]["exits"]: + if (r["source_map"], r["line"]) == key: + r["layer_delta_after"] = True + self.assertCaught(p, "the plan has booleans or floats where integers belong") + p = self.plan() + p["stairwells"][0]["tiles"][1]["layer"] = float(p["stairwells"][0]["tiles"][1]["layer"]) + self.assertCaught(p, "the plan has booleans or floats where integers belong, e.g. /stairwells[0]/tiles[1]/layer") + + def test_strict_json_comparison(self): + self.assertTrue(verify_plan.same_json({"a": [1, {"b": None}]}, {"a": [1, {"b": None}]})) + for a, b in (([1], [True]), ({"a": 1}, {"a": 1.0}), ({"a": None}, {}), ([1, 2], [2, 1])): + self.assertFalse(verify_plan.same_json(a, b), (a, b)) + + def test_an_oversized_plan_file(self): + import contextlib + import io + + limit, out = verify_plan.MAX_PLAN_BYTES, io.StringIO() + try: + verify_plan.MAX_PLAN_BYTES = 1000 + with contextlib.redirect_stdout(out): + self.assertEqual(verify_plan.main([str(PLAN)]), 1) + finally: + verify_plan.MAX_PLAN_BYTES = limit + self.assertIn("is larger than 1000 bytes", out.getvalue()) + + def test_the_relayering_cache_follows_its_inputs(self): + # a changed relayering input is recomputed, never served from the cache + p = self.plan() + c = p["relayering"]["new_contacts"][0] + verify_plan.verify(self.plan()) + c["walkable"] = not c["walkable"] + self.assertCaught(p, "new contact {} / {} records walkable".format(tuple(c["tiles"][0]), tuple(c["tiles"][1]))) + # every input is in the key: each change below computes the relayering again + calls, rules = [], verify_plan.relayering_rules + ow_rows = next(g for g in self.plan()["groups"] if g["classification"] == "overworld")["occupied_rows_by_layer"] + args = [ow_rows, {}, self.plan()["relayering"], [], {("m", 1)}, [50, 50, 10, 1]] + + def counting(*a): + calls.append(a) + return [], {}, {} + + try: + verify_plan.relayering_rules = counting + verify_plan._RELAYER_CACHE.clear() + verify_plan._relayering(*args) + verify_plan._relayering(*args) + self.assertEqual(len(calls), 1) + for i, changed in ((0, dict(ow_rows, extra={})), (2, dict(args[2], seam_cell=25)), + (3, [{"source_map": "m"}]), (4, {("m", 2)}), (5, [50, 50, 10, 2])): + a = list(args) + a[i] = changed + verify_plan._relayering(*a) + self.assertEqual(len(calls), 2 + [0, 2, 3, 4, 5].index(i), "input {} is not in the key".format(i)) + finally: + verify_plan.relayering_rules = rules + verify_plan._RELAYER_CACHE.clear() + + def test_the_plan_identifies_its_input(self): + for change in (lambda p: p.pop("content_root"), lambda p: p.update(content_root=None), + lambda p: p.update(content_root=""), lambda p: p.update(content_git_head="3ad2015"), + lambda p: p.update(content_worktree_clean="yes"), + lambda p: p["content_inputs_digest"].update(sha256="x")): + p = self.plan() + change(p) + self.assertCaught(p, "the plan does not identify its input") + + def test_another_schema(self): + p = self.plan() + p["schema"] = "atrinik-overworld-placement-plan/3" + self.assertCaught(p, "schema atrinik-overworld-placement-plan/3 is not atrinik-overworld-placement-plan/4") + + +class ExitInventory(VerifierTest): + """Fix 1: the exit records are checked against the complete source exit inventory.""" + + def test_deleting_every_copy_of_a_crossing_exit(self): + p = self.plan() + self.assertEqual(len(copies(p, CROSSING)), 2) + delete_copies(p, CROSSING) + self.assertCaught(p, "exit record(s) missing, e.g. exit {}:{}".format(*CROSSING), + "summary crossing exits", "crossing exits of {}".format(MINE + "mine_entrance_a")) + + def test_deleting_one_copy_of_a_crossing_exit(self): + p = self.plan() + delete_copies(p, CROSSING, only_group=MINE + "mine_entrance") + self.assertCaught(p, "exit record(s) missing, e.g. exit {}:{} from entrances_from_other_groups of {}".format( + CROSSING[0], CROSSING[1], MINE + "mine_entrance")) + + def test_deleting_an_overworld_entrance(self): + p = self.plan() + e = group(p, MINE + "mine_entrance")["entrances_from_overworld"][0] + delete_copies(p, (e["source_map"], e["line"])) + self.assertCaught(p, "exit record(s) missing, e.g. exit {}:{} from entrances_from_overworld".format( + e["source_map"], e["line"])) + + def test_deleting_an_overworld_exit(self): + p = self.plan() + ow = next(g for g in p["groups"] if g["classification"] == "overworld") + e = ow["internal_exits"].pop(0) + self.assertCaught(p, "exit record(s) missing, e.g. exit {}:{} from internal_exits".format( + e["source_map"], e["line"])) + + def test_deleting_an_exit_from_the_inventory_and_the_index(self): + p = self.plan() + p["exit_inventory"] = [x for x in p["exit_inventory"] if (x["source_map"], x["line"]) != CROSSING] + delete_copies(p, CROSSING) + self.assertCaught(p, "the exit inventory lists {} exit objects, the summary {}".format( + len(p["exit_inventory"]), len(p["exit_inventory"]) + 1)) + + def test_deleting_an_exit_from_the_inventory_only(self): + p = self.plan() + p["exit_inventory"] = [x for x in p["exit_inventory"] if (x["source_map"], x["line"]) != CROSSING] + p["summary"]["exit_objects"] -= 1 + self.assertCaught(p, "exit record(s) the exit inventory does not call for, e.g. exit {}:{}".format(*CROSSING)) + + def test_listing_an_exit_twice_in_the_inventory(self): + p = self.plan() + p["exit_inventory"].append(dict(p["exit_inventory"][0])) + p["summary"]["exit_objects"] += 1 + self.assertCaught(p, "is listed twice in the exit inventory") + + def test_a_copy_in_the_wrong_list(self): + p = self.plan() + g = group(p, MINE + "mine_entrance") + e = next(e for e in g["entrances_from_other_groups"] if (e["source_map"], e["line"]) == CROSSING) + g["entrances_from_other_groups"].remove(e) + g["entrances_from_overworld"].append(e) + self.assertCaught(p, "exit record(s) missing", "exit record(s) the exit inventory does not call for") + + def test_an_extra_copy(self): + p = self.plan() + e = copies(p, CROSSING)[0][2] + group(p, MINE + "mine_unique")["entrances_from_other_groups"].append(dict(e)) + self.assertCaught(p, "exit record(s) the exit inventory does not call for, e.g. exit {}:{}".format(*CROSSING)) + + def test_a_record_that_differs_from_the_inventory(self): + for field, value in (("source_tile", [0, 0]), ("target_tile", [0, 0]), ("arch", "hole"), + ("target_map", MINE + "mine_unique"), ("form", "tiled")): + with self.subTest(field=field): + p = self.plan() + for _g, _lst, e in copies(p, CROSSING): + e[field] = value + self.assertCaught(p, "record of exit {}:{} differs from its inventory entry in {}".format( + CROSSING[0], CROSSING[1], field)) + + def test_dropping_the_plane_endpoints_of_a_placed_exit(self): + p = self.plan() + for _g, _lst, e in copies(p, CROSSING): + for k in ("plane_source", "plane_target", "xy_distance", "layer_delta", "becomes"): + e.pop(k) + self.assertCaught(p, "exit {}:{} lacks placement fields ['plane_source', 'plane_target', 'xy_distance', " + "'layer_delta', 'becomes'], although both of its ends are placed".format(*CROSSING)) + + def test_a_wrong_overworld_plane_end(self): + p = self.plan() + e = group(p, MINE + "mine_entrance")["entrances_from_overworld"][0] + e["overworld_plane_source"] = [0, 0, 0] + self.assertCaught(p, "exit {}:{} overworld_plane_source [0, 0, 0]".format(e["source_map"], e["line"])) + + def test_summary_and_group_counts(self): + p = self.plan() + p["summary"]["crossing_exit_objects_after_placement"]["stairs"] += 1 + self.assertCaught(p, "summary crossing exits") + p = self.plan() + p["summary"]["crossing_exit_objects_into_separate_planes"] -= 1 + self.assertCaught(p, "summary crossing exits into separate planes") + p = self.plan() + p["summary"]["portal_reasons"]["slope-blocked"] += 1 + self.assertCaught(p, "summary portal reasons") + p = self.plan() + group(p, MINE + "mine_entrance_a")["placement"]["crossing_exits_after_placement"]["stairs"] -= 1 + self.assertCaught(p, "crossing exits of {}".format(MINE + "mine_entrance_a")) + + def test_deleting_a_wired_exit(self): + p = self.plan() + w = p["data_problems"]["dangling_exits_wired"][0] + delete_copies(p, (w["source_map"], w["line"])) + self.assertCaught(p, "exit record(s) missing, e.g. exit {}:{}".format(w["source_map"], w["line"]), + "wired exit {}:{} has no exit record".format(w["source_map"], w["line"])) + p = self.plan() + w = p["data_problems"]["dangling_exits_wired"].pop(0) + delete_copies(p, (w["source_map"], w["line"])) + self.assertCaught(p, "wired exits listed, the summary says") + + def test_a_wired_copy_that_differs(self): + p = self.plan() + p["data_problems"]["dangling_exits_wired"][0]["target_tile"] = [0, 0] + self.assertCaught(p, "differs from its inventory entry in target_tile") + + def test_a_main_connection_that_is_no_record(self): + p = self.plan() + group(p, MINE + "mine_entrance_a")["placement"]["main_connection"]["source_tile"] = [0, 0] + self.assertCaught(p, "main connection of {} is not one of its exit records".format(MINE + "mine_entrance_a")) + + def test_a_main_connection_that_did_not_fix_the_placement(self): + # the deck's other up-stairs, one tile east, would put the deck one tile further west + deck = "/shattered_islands/deserted_tutorial_island/ship_lower_deck" + p = self.plan() + mc = group(p, deck)["placement"]["main_connection"] + mc.update(source_tile=[2, 6], arch="stairs_wood_up") + self.assertCaught(p, "ideal spot {'dx': -161, 'dy': 1832, 'L': -1} of " + deck + " does not follow from its " + "main connection ({'dx': -162, 'dy': 1832, 'L': -1})") + + def test_offsets_that_are_not_the_ideal_spot(self): + # ellamin sits 4 layers below its ideal spot: flipping its orientation moves the ideal layer the other way + ellamin = "/shattered_islands/incuna/ellamin" + for field, value in (("shift_from_ideal", [1, 0]), ("extra_layers_from_ideal", 1), ("orientation", "above"), + ("orientation", "sideways")): + with self.subTest(field=field, value=value): + p = self.plan() + group(p, ellamin)["placement"][field] = value + self.assertCaught(p, "offsets of {} are not its ideal spot".format(ellamin)) + + def test_main_connections(self): + deck = "/shattered_islands/deserted_tutorial_island/ship_lower_deck" + p = self.plan() + group(p, deck)["placement"]["main_connection"] = None + self.assertCaught(p, "placement of {} has no main connection".format(deck)) + p = self.plan() + group(p, deck)["placement"]["main_connection"]["kind"] = "sideways" + self.assertCaught(p, "main connection of {} has kind sideways".format(deck)) + p = self.plan() + group(p, deck)["placement"]["tentative_reason"] = "hint" + self.assertCaught(p, "proposed placement of {} carries an alignment hint".format(deck)) + p = self.plan() + group(p, deck)["placement"].update(main_connection=None, tentative_reason="hint") + self.assertCaught(p, "proposed placement of {} carries an alignment hint".format(deck), + "placement of {} has no main connection".format(deck)) + p = self.plan() + tent = group(p, p["summary"]["tentative_placement_order"][0])["placement"] + tent["main_connection"] = dict(group(p, deck)["placement"]["main_connection"]) + self.assertCaught(p, "has both a main connection and an alignment hint") + # mine_entrance_a is entered from mine_entrance, placed before it; claim it was placed after + p = self.plan() + a, b = group(p, MINE + "mine_entrance_a")["placement"], group(p, MINE + "mine_entrance")["placement"] + self.assertEqual(a["main_connection"]["source_map"], MINE + "mine_entrance") + a["placement_index"], b["placement_index"] = b["placement_index"], a["placement_index"] + o = p["summary"]["placement_order"] + i, j = o.index(MINE + "mine_entrance_a"), o.index(MINE + "mine_entrance") + o[i], o[j] = o[j], o[i] + res = verify_plan.verify(p) + self.assertIn("main connection of {} leads to {}, which is not placed before it".format( + MINE + "mine_entrance_a", MINE + "mine_entrance"), res["errors"]) + + def test_a_data_problem_that_differs_from_the_inventory(self): + p = self.plan() + p["data_problems"]["exits"][0]["source_tile"] = [99, 99] + self.assertCaught(p, "data problem of exit") + + def test_deleting_a_data_problem(self): + p = self.plan() + lst = p["data_problems"]["exits"] + d = next(d for d in lst if d["kind"] == "exit-without-destination") + lst.remove(d) + self.assertCaught(p, "1 exit data problem(s) differ from the exit inventory, e.g. ", + '"kind": "exit-without-destination", "line": {}'.format(d["line"])) + p = self.plan() + d = p["data_problems"]["exits"][0] + d["severity"] = "info" if d.get("severity") == "problem" else "problem" + self.assertCaught(p, "exit data problem(s) differ from the exit inventory") + + def test_an_exit_on_a_map_that_is_in_no_group(self): + p = self.plan() + key = ("/shattered_islands/deserted_tutorial_island/ship_lower_deck", 64) + for d in p["exit_inventory"] + p["data_problems"]["exits"]: + if (d["source_map"], d["line"]) == key: + d["source_map"] = "/not/a/map" + self.assertCaught(p, "exit /not/a/map:64 is on a map that is in no group") + + def test_a_dynamic_exit_target(self): + key = ("/shattered_islands/world_18_4", 1958) + p = self.plan() + d = next(d for d in p["data_problems"]["exits"] if (d["source_map"], d["line"]) == key) + self.assertEqual(d["kind"], "dynamic-random-map-exit") + d["detail"] = "/somewhere/else" + self.assertCaught(p, "exit data problem(s) differ from the exit inventory") + p = self.plan() + next(x for x in p["exit_inventory"] if (x["source_map"], x["line"]) == key)["dynamic_target"] = "/elsewhere" + self.assertCaught(p, "exit data problem(s) differ from the exit inventory") + + def test_a_kept_decoration_in_the_wrong_place(self): + p = self.plan() + p["exits_kept_as_decoration"][0]["plane_source"] = [0, 0, 0] + self.assertCaught(p, "kept as decoration is at [0, 0, 0]") + + def test_a_reversed_exit_direction(self): + p = self.plan() + key = ("/shattered_islands/berri_lur/temple_loki/tol_0201", 441) + self.assertTrue(copies(p, key)) + for _g, _lst, e in copies(p, key): + e["vertical"] = "down" if e["vertical"] == "up" else "up" + self.assertCaught(p, "record of exit {}:{} differs from its inventory entry in vertical".format(*key)) + + def test_partial_plane_endpoints_on_an_unplaced_exit(self): + p = self.plan() + key = ("/emergency", 17) + for _g, _lst, e in copies(p, key): + self.assertNotIn("plane_source", e) + e["plane_source"] = [0, 0, 0] + self.assertCaught(p, "exit {}:{} has placement fields ['plane_source'], although not both of its ends " + "are placed".format(*key)) + + def test_placement_fields(self): + cases = (("xy_distance", 5, "records 5 tiles"), ("layer_delta", 3, "/ 3 layers"), + ("stairwell", "SW1", "labelled stairs records stairwell SW1"), + ("portal_reason", "overworld", "portal reason overworld"), + ("bogus", 1, "has fields the verifier does not know: bogus")) + for field, value, needle in cases: + with self.subTest(field=field): + p = self.plan() + for _g, _lst, e in copies(p, CROSSING): + e[field] = value + self.assertCaught(p, needle) + + def test_a_wrong_portal_reason_text(self): + p = self.plan() + e = next(e for g in p["groups"] for e in g.get("entrances_from_other_groups", []) if e.get("portal_reason")) + for _g, _lst, c in copies(p, (e["source_map"], e["line"])): + c["portal_reason_text"] = "no reason" + self.assertCaught(p, "exit {}:{} labelled portal".format(e["source_map"], e["line"])) + + def test_classifications(self): + p = self.plan() + group(p, "/emergency")["classification"] = "seperate" + self.assertCaught(p, "group /emergency has classification seperate with reason system") + p = self.plan() + group(p, "/emergency")["reason"] = "place" + self.assertCaught(p, "group /emergency has classification separate with reason place") + p = self.plan() + p["summary"]["by_classification"]["separate:system"] += 1 + self.assertCaught(p, "summary group and map counts differ from the groups") + + def test_duplicate_identities(self): + p = self.plan() + apt = "/shattered_islands/eld_woods_island/apartments/apartment_cheap" + dup = json.loads(json.dumps(group(p, apt))) + p["groups"].append(dup) + s = p["summary"] + s["groups_total"] += 1 + s["non_overworld_groups"] += 1 + s["maps_total"] += dup["map_count"] + s["by_classification"]["{}:{}".format(dup["classification"], dup["reason"])] += 1 + self.assertCaught(p, "group id {} is listed several times".format(apt)) + p = self.plan() + ow = next(g for g in p["groups"] if g["classification"] == "overworld") + ow["cells_by_layer"]["0"].append(list(ow["cells_by_layer"]["0"][0])) + ow["map_count"] += 1 + ow["layers"]["0"] += 1 + p["summary"]["maps_total"] += 1 + self.assertCaught(p, "member map /shattered_islands/world_{}_{} is listed several times".format( + *ow["cells_by_layer"]["0"][0])) + + def test_the_overworld_map_count(self): + p = self.plan() + next(g for g in p["groups"] if g["classification"] == "overworld")["map_count"] += 1 + p["summary"]["maps_total"] += 1 + self.assertCaught(p, "summary group and map counts differ from the groups") + + def test_unknown_source_values(self): + for field, value, needle in (("form", "banana", "has form banana"), + ("vertical", "sideways", "has form explicit, direction sideways")): + with self.subTest(field=field): + p = self.plan() + next(x for x in p["exit_inventory"] if (x["source_map"], x["line"]) == CROSSING)[field] = value + for _g, _lst, e in copies(p, CROSSING): + e[field] = value + self.assertCaught(p, "exit {}:{} {}".format(CROSSING[0], CROSSING[1], needle)) + p = self.plan() + x = next(x for x in p["exit_inventory"] if x["form"] == "unresolved") + x.update(target_map="/emergency", target_tile=[0, 0]) + self.assertCaught(p, "exit {}:{} has form unresolved, direction {} and a target".format( + x["source_map"], x["line"], x["vertical"])) + + def test_value_types(self): + for field, value in (("arch", 7), ("source_tile", [1.5, 2]), ("per_player_last_eat_1", "yes"), + ("event_scripts", "script.py"), ("vertical_inferred_from", 3)): + with self.subTest(field=field): + p = self.plan() + next(x for x in p["exit_inventory"] if (x["source_map"], x["line"]) == CROSSING)[field] = value + for _g, _lst, e in copies(p, CROSSING): + e[field] = value + self.assertCaught(p, "exit {!r}:{!r} has a value of the wrong type".format(*CROSSING)) + + def test_a_same_group_exit_with_bad_values(self): + key = ("/planes/creation/creation_0501", 427) + for change in ({"target_map": None, "target_tile": None}, {"source_tile": [2.5, 1]}): + with self.subTest(change=change): + p = self.plan() + next(x for x in p["exit_inventory"] if (x["source_map"], x["line"]) == key).update(change) + self.assertCaught(p, "exit {!r}:{!r} has a value of the wrong type".format(*key)) + + def test_a_dynamic_exit_without_its_target(self): + key = ("/shattered_islands/world_18_4", 1958) + p = self.plan() + next(x for x in p["exit_inventory"] if (x["source_map"], x["line"]) == key).pop("dynamic_target") + next(d for d in p["data_problems"]["exits"] if (d["source_map"], d["line"]) == key)["detail"] = None + self.assertCaught(p, "exit {!r}:{!r} has a value of the wrong type".format(*key)) + + def test_a_wired_target_outside_its_map(self): + p = self.plan() + key = (MINE + "mine_entrance", 181) + d = next(d for d in p["data_problems"]["dangling_exits_wired"] if (d["source_map"], d["line"]) == key) + self.assertEqual((d["target_map"], d["target_tile"]), ("/shattered_islands/world_8_79", [8, 1])) + d.update(target_map="/shattered_islands/world_7_79", target_tile=[32, 1]) + for _g, _lst, e in copies(p, key): + e.update(target_map="/shattered_islands/world_7_79", target_tile=[32, 1]) + self.assertCaught(p, "exit {}:{} targets tile [32, 1] outside /shattered_islands/world_7_79".format(*key)) + + def test_a_boolean_wired_coordinate(self): + p = self.plan() + key = (MINE + "mine_entrance", 181) + next(d for d in p["data_problems"]["dangling_exits_wired"] if (d["source_map"], d["line"]) == key)[ + "target_tile"] = [8, True] + for _g, _lst, e in copies(p, key): + e["target_tile"] = [8, True] + self.assertCaught(p, "exit {}:{} targets tile [8, True] outside".format(*key)) + + def test_an_invalid_tile_link_slot(self): + p = self.plan() + x = next(x for x in p["exit_inventory"] if x["form"] == "tiled-unresolved") + x["tiled_slot"] = 999 + for d in p["data_problems"]["exits"]: + if (d["source_map"], d["line"]) == (x["source_map"], x["line"]) and d["kind"] == "tiled-exit-without-link": + d["detail"] = "last_heal 999 (None) but the map has no such tile link; the exit leads nowhere" + self.assertCaught(p, "exit {}:{} has form tiled-unresolved and tile-link slot 999".format( + x["source_map"], x["line"])) + p = self.plan() + next(x for x in p["exit_inventory"] if x["form"] == "explicit")["tiled_slot"] = 9 + self.assertCaught(p, "has form explicit and tile-link slot 9") + + def test_a_target_outside_its_map(self): + p = self.plan() + key = ("/shattered_islands/ships/ship_1", 122) + x = next(x for x in p["exit_inventory"] if (x["source_map"], x["line"]) == key) + x.update(target_map="/emergency", target_tile=[999, 999]) + for _g, _lst, e in copies(p, key): + e.update(target_map="/emergency", target_tile=[999, 999]) + self.assertCaught(p, "exit {}:{} targets tile [999, 999] outside /emergency".format(*key)) + + def test_a_wrong_portal_reason(self): + p = self.plan() + e = next(e for g in p["groups"] for e in g.get("entrances_from_other_groups", []) + if e.get("portal_reason") == "other-entrance-aligned") + for _g, _lst, c in copies(p, (e["source_map"], e["line"])): + c["portal_reason"] = "group-shifted" + self.assertCaught(p, "portal reason group-shifted (slope reachable None); expected portal reason " + "other-entrance-aligned") + + +class StairwellReplaces(VerifierTest): + """Fix 2: every stairwell replaces entry equals its indexed exit record.""" + + def test_each_copied_field(self): + for field, value in (("source_tile", [0, 0]), ("target_map", "/shattered_islands/world_0_0"), + ("target_tile", [0, 0]), ("arch", "hole"), ("plane_source", [0, 0, 0]), + ("plane_target", [0, 0, 0])): + with self.subTest(field=field): + p = self.plan() + r = p["stairwells"][0]["replaces"][0] + r[field] = value + self.assertCaught(p, "SW1 replaces entry of exit {}:{} differs from its exit record in {}".format( + r["source_map"], r["line"], field)) + + def test_all_fields_at_once(self): + p = self.plan() + for r in p["stairwells"][0]["replaces"]: + r.update(source_tile=[0, 0], target_map="/shattered_islands/world_0_0", target_tile=[0, 0], + plane_source=[0, 0, 0], plane_target=[0, 0, 0]) + self.assertCaught(p, "SW1 replaces entry") + + def test_shape(self): + p = self.plan() + sr1 = next(sw for sw in p["stairwells"] if sw["id"] == "SR1") + sr1["shape"] = "banana" + self.assertCaught(p, "SR1 shape banana does not fit") + p = self.plan() + sr1 = next(sw for sw in p["stairwells"] if sw["id"] == "SR1") + sr1["shape"] = "sloped" if sr1["shape"] == "column" else "column" + self.assertCaught(p, "SR1 shape {} does not fit".format(sr1["shape"])) + + def test_owners(self): + p = self.plan() + sw = p["stairwells"][0] + sw["other"], sw["connects"] = "/nowhere", sorted([sw["group"], "/nowhere"]) + self.assertCaught(p, "SW1 owners") + p = self.plan() + sr1 = next(sw for sw in p["stairwells"] if sw["id"] == "SR1") + sr1["owner"] = "stairwell@0,0,0" + self.assertCaught(p, "SR1 owners") + + def test_intermediate_appearance(self): + p = self.plan() + sw = p["stairwells"][0] + sw["intermediate_appearance"] = "stairs_red_down" + self.assertCaught(p, "SW1 intermediate tiles do not show") + p = self.plan() + p["stairwells"][0]["tiles"][1]["objects"][0]["walkable"] = True + self.assertCaught(p, "SW1 intermediate tiles do not show") + + def test_the_bend_limit(self): + p = self.plan() + p["settings"]["stairwell_max_bend_tiles"] = 0 + bent = [sw["id"] for sw in p["stairwells"] if sw["shape"] == "column" and sw["max_bend"]] + self.assertTrue(bent) + self.assertCaught(p, *("{} bends 1 tile(s) from its column".format(i) for i in bent)) + + def test_stairwell_mode(self): + p = self.plan() + p["stairwells"][0]["mode"] = "banana" + self.assertCaught(p, "SW1 mode banana disagrees") + + def test_series_bend_and_counts(self): + p = self.plan() + p["stairwells"][0]["id"], p["stairwells"][1]["id"] = p["stairwells"][1]["id"], p["stairwells"][0]["id"] + self.assertCaught(p, "is out of its series") + p = self.plan() + p["stairwells"][0]["max_bend"] += 1 + self.assertCaught(p, "SW1 max_bend") + p = self.plan() + p["summary"]["stairwells_by_kind"]["placement"] += 1 + self.assertCaught(p, "summary stairwell and blocked-site counts differ from the lists") + + def test_an_unreachable_blocked_slope(self): + p = self.plan() + site = p["sloped_stairwell_sites_blocked"][0] + self.assertEqual((site["top"], site["bottom"]), ([198, 1895, 0], [194, 1895, -4])) + site["top"], site["bottom"] = [199, 1895, 0], [193, 1895, -4] + self.assertCaught(p, "blocked sloped stairwell site [199, 1895, 0] -> [193, 1895, -4] is no stairwell site") + + def test_an_empty_blocked_site(self): + for key in ("stairwell_sites_blocked", "sloped_stairwell_sites_blocked"): + with self.subTest(key=key): + p = self.plan() + p[key].append(dict(p[key][0], exits=[])) + self.assertCaught(p, "lists 0 exits") + p = self.plan() + p["stairwell_sites_blocked"][0]["shape"] = "sloped" + self.assertCaught(p, "with shape sloped (column by its ends)") + + def test_direction(self): + p = self.plan() + r = p["stairwells"][0]["replaces"][0] + r["goes"] = "up" if r["goes"] == "down" else "down" + self.assertCaught(p, "SW1 exit {}:{} goes {}, but its record goes".format(r["source_map"], r["line"], r["goes"])) + + def test_other_copies(self): + p = self.plan() + p["stairwell_sites_blocked"][0]["exits"][0]["source_tile"] = [0, 0] + self.assertCaught(p, "blocked column site entry of exit") + p = self.plan() + p["sloped_stairwell_sites_blocked"][0]["exits"][0]["arch"] = "hole" + self.assertCaught(p, "blocked sloped site entry of exit") + p = self.plan() + p["relayering"]["exits"][0]["target_tile"] = [0, 0] + self.assertCaught(p, "relayering exit of exit") + p = self.plan() + p["relayering"]["exits"][0]["crosses_to_group"] = "/emergency" + self.assertCaught(p, "crosses to /emergency") + + def test_a_placement_site_moved_to_the_relayering_sites(self): + p = self.plan() + site = p["stairwell_sites_blocked"].pop(0) + x = site["exits"][0] + r = copies(p, (x["source_map"], x["line"]))[0][2] + up_src = r["plane_source"][2] > r["plane_target"][2] + top, bottom = (r["source_map"], r["target_map"]) if up_src else (r["target_map"], r["source_map"]) + p["relayering"]["stairwells_blocked"].append( + {"top_map": top, "bottom_map": bottom, "top": site["top"], "bottom": site["bottom"], + "shape": site["shape"], "exits": site["exits"]}) + p["summary"]["stairwell_sites_blocked"] -= 1 + p["summary"]["relayering_stairwell_sites_blocked"] += 1 + self.assertCaught(p, "blocked relayering site lists exit {}:{}, which is not an overworld exit".format( + x["source_map"], x["line"])) + + def test_blocked_site_owners(self): + p = self.plan() + p["stairwell_sites_blocked"][0]["other"] = "/shattered_islands/world_0_0" + self.assertCaught(p, "lists exit {}:{} of other owners".format( + p["stairwell_sites_blocked"][0]["exits"][0]["source_map"], p["stairwell_sites_blocked"][0]["exits"][0]["line"])) + + def test_an_unrelated_exit_at_a_blocked_site(self): + p = self.plan() + key = ("/shattered_islands/world_11_61", 668) + e = copies(p, key)[0][2] + self.assertEqual(e["becomes"], "stairs") + p["stairwell_sites_blocked"][0]["exits"].append( + {"source_map": e["source_map"], "source_tile": e["source_tile"], "arch": e["arch"], "line": e["line"]}) + self.assertCaught(p, "blocked column stairwell site lists exit {}:{}, which needs no stairwell".format(*key)) + + +class FailedPlacement(VerifierTest): + """Fix 3: a plan with a failed group does not verify, so it is not published.""" + + GID = "/shattered_islands/eld_woods_island/clearhaven/treehome_4" + + def fail(self, p, gid): + g = group(p, gid) + g["placement"] = {"status": "failed", "ideal": g["placement"]["ideal"]} + return g + + def test_failed_group_left_in_the_placement_order(self): + # what the generator used to publish: the failed group stayed in placement_order + p = self.plan() + self.fail(p, self.GID) + self.assertCaught(p, "placement of {} failed; a plan with a failed group must not be published".format( + self.GID), "place group {} has no proposed placement".format(self.GID)) + + def test_failed_group_kept_unplaced(self): + # what the generator writes now: unplaced and listed as failed; still refused + p = self.plan() + idx = group(p, self.GID)["placement"]["placement_index"] + self.fail(p, self.GID) + s = p["summary"] + s["placement_order"].remove(self.GID) + s["unplaced_place_groups"] = [self.GID] + s["failed_placements"] = [self.GID] + for g in p["groups"]: + pl = g.get("placement") or {} + if pl.get("placement_index") is not None and pl["placement_index"] > idx: + pl["placement_index"] -= 1 + res = verify_plan.verify(p) + self.assertFalse(res["ok"]) + self.assertIn("placement of {} failed; a plan with a failed group must not be published".format(self.GID), + res["errors"]) + self.assertFalse(any("summary unplaced" in e or "failed_placements" in e or "placement order" in e + for e in res["errors"]), res["errors"][:8]) + + def test_failed_tentative_group(self): + p = self.plan() + gid = p["summary"]["tentative_placement_order"][0] + self.fail(p, gid) + self.assertCaught(p, "placement of {} failed".format(gid)) + + def test_tentative_placement_only_by_classification_or_hint(self): + p = self.plan() + gid = p["summary"]["tentative_placement_order"][0] + g = group(p, gid) + self.assertEqual((g["classification"], g["reason"]), ("separate", "unreachable")) + g["placement"].pop("tentative_reason") + self.assertCaught(p, "separate group {} (unreachable) has a tentative placement without an alignment " + "hint".format(gid)) + p = self.plan() + group(p, gid)["reason"] = "per_player" + self.assertCaught(p, "separate group {} (per_player) has a tentative placement".format(gid)) + + def test_a_fabricated_alignment_hint(self): + # the last proposed placement turned into an uncertain group with a made-up hint instead of a connection + p = self.plan() + s = p["summary"] + gid = s["placement_order"].pop() + s["tentative_placement_order"].insert(0, gid) + s["by_classification"]["place:place"] -= 1 + s["by_classification"]["uncertain:uncertain"] = 1 + g = group(p, gid) + g.update(classification="uncertain", reason="uncertain") + g["placement"].update(status="tentative", main_connection=None, tentative_reason="made up") + self.assertCaught(p, "alignment hint of {} is not a configured alignment to a placed anchor".format(gid)) + # a configured hint whose layer distance does not match the placement + p = self.plan() + p["settings"]["tentative_alignments"][0]["layer_delta"] -= 1 + self.assertCaught(p, "alignment hint of {} is not a configured alignment".format( + p["settings"]["tentative_alignments"][0]["group"])) + + def test_a_layer_outside_the_vertical_bounds(self): + p = self.plan() + g = group(p, MINE + "mine_unique_a")["placement"] + self.assertEqual((g["ideal"]["L"], g["layer_offset"]), (-4, -5)) + g["vertical_bounds_from_exit_semantics"] = [-4, -4] + self.assertCaught(p, "layer offset -5 of {} is outside its vertical bounds [-4, -4]".format( + MINE + "mine_unique_a")) + + def test_placement_numbers_are_integers(self): + for field, value in (("extra_layers_from_ideal", True), ("shift_from_ideal", [False, 0]), + ("layer_offset", -5.0)): + with self.subTest(field=field): + p = self.plan() + group(p, MINE + "mine_unique_a")["placement"][field] = value + self.assertCaught(p, "placement of {} has offsets, ideal, shift or extra layers that are not " + "integers".format(MINE + "mine_unique_a")) + + def test_search_limits(self): + p = self.plan() + p["settings"]["max_extra_layers"] = 0 + self.assertCaught(p, "placement of /shattered_islands/incuna/ellamin goes 4 layer(s)") + p = self.plan() + p["settings"]["max_shift_tiles"] = -1 + self.assertCaught(p, "past the search limits") + + def test_a_dangling_alignment(self): + p = self.plan() + p["settings"]["tentative_alignments"].append( + {"group": "/not/a/group", "anchor": "/also/missing", "layer_delta": 0, "why": "fabricated"}) + self.assertCaught(p, "tentative alignment of /not/a/group to /also/missing is not the hint of a tentative " + "placement") + # an entry between two ordinary proposed groups is not applied anywhere + p = self.plan() + a, b = p["summary"]["placement_order"][:2] + p["settings"]["tentative_alignments"].append({"group": b, "anchor": a, "layer_delta": -1, "why": "stale"}) + self.assertCaught(p, "tentative alignment of {} to {} is not the hint of a tentative placement".format(b, a)) + + def test_the_overworld_grid(self): + p = self.plan() + p["settings"]["overworld_map_size"] = 25 + self.assertCaught(p, "settings.overworld_map_size 25 is not the 24-tile grid") + p = self.plan() + ow = next(g for g in p["groups"] if g["classification"] == "overworld") + path = sorted(ow["occupied_rows_by_layer"])[0] + layer = sorted(ow["occupied_rows_by_layer"][path])[0] + ow["occupied_rows_by_layer"][path][layer][0] += "." + self.assertCaught(p, "rows of {} on layer {} are not 24 x 24".format(path, layer)) + + def test_a_placement_without_status(self): + p = self.plan() + group(p, "/emergency")["placement"] = {"status": None, "x_offset": 123} + self.assertCaught(p, "placement of /emergency has no status") + + def test_alignment_reasons_are_text(self): + p = self.plan() + a = p["settings"]["tentative_alignments"][0] + a["why"] = ["x"] + group(p, a["group"])["placement"]["tentative_reason"] = ["x"] + self.assertCaught(p, "tentative_reason of {} is not a text".format(a["group"]), + "tentative alignment of {} to {} is not the hint".format(a["group"], a["anchor"])) + + def test_place_group_without_a_placement(self): + p = self.plan() + group(p, self.GID).pop("placement") + self.assertCaught(p, "place group {} has no proposed placement".format(self.GID)) + + def test_summary_lists(self): + p = self.plan() + p["summary"]["unplaced_place_groups"] = [self.GID] + self.assertCaught(p, "summary unplaced_place_groups") + p = self.plan() + p["summary"]["failed_placements"] = [self.GID] + self.assertCaught(p, "summary failed_placements") + p = self.plan() + p["summary"]["placement_order"].reverse() + self.assertCaught(p, "placement order in the summary differs") + p = self.plan() + p["summary"].pop("failed_placements") + self.assertCaught(p, "summary failed_placements") + + +@unittest.skipUnless(os.environ.get("PLACEMENT_PLAN_CONTENT"), "set PLACEMENT_PLAN_CONTENT to a content checkout") +class ContentFidelity(VerifierTest): + """Edits that keep the JSON consistent with itself pass verify() but not verify_content().""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.content = Path(os.environ["PLACEMENT_PLAN_CONTENT"]).resolve() + cls.fresh = verify_plan.rebuild(cls.content) + + def assertUnfaithful(self, p): + self.assertTrue(verify_plan.verify(p)["ok"], "the JSON-only check should not see this edit") + res = verify_plan.verify_content(p, self.content, self.fresh) + self.assertFalse(res["ok"]) + self.assertIn("the plan differs from a rebuild from the content", res["errors"][0]) + + def test_the_committed_plan_is_the_rebuild(self): + self.assertTrue(verify_plan.verify_content(self.plan(), self.content, self.fresh)["ok"]) + + def test_edits_the_json_only_check_cannot_see(self): + key = ("/planes/creation/creation_0501", 427) + p = self.plan() # an exit dropped from the inventory together with its count + p["exit_inventory"] = [x for x in p["exit_inventory"] if (x["source_map"], x["line"]) != key] + p["summary"]["exit_objects"] -= 1 + self.assertUnfaithful(p) + p = self.plan() # a recordless exit moved to another source line + next(x for x in p["exit_inventory"] if (x["source_map"], x["line"]) == key)["line"] = -1 + self.assertUnfaithful(p) + p = self.plan() # an optional flag present as null in every copy + for _g, _lst, e in copies(p, CROSSING): + e["target_is_map_default_entry"] = None + self.assertUnfaithful(p) + p = self.plan() # explanatory text the JSON-only check does not re-derive + group(p, "/emergency")["evidence"][0] = "made up" + self.assertUnfaithful(p) + + def test_type_strict_comparison(self): + p = self.plan() # caught by both checks, and by the rebuild comparison type for type + p["summary"]["wired_exits"] = 5.0 + self.assertFalse(verify_plan.verify(p)["ok"]) + self.assertFalse(verify_plan.verify_content(p, self.content, self.fresh)["ok"]) + + +class Report(VerifierTest): + """The report counts wired exits from the records.""" + + def test_an_overworld_wired_exit_is_no_crossing_stairs(self): + import tempfile + + import report + + p = self.plan() + ow = next(g for g in p["groups"] if g["classification"] == "overworld") + wired = next(e for g in p["groups"] for key in INDEX for e in g.get(key, []) if e.get("form") == "wired") + ow["internal_exits"].append(dict(ow["internal_exits"][0], form="wired", wired=wired["wired"])) + p["summary"]["wired_exits"] += 1 # as the generator counts it: every wired exit + with tempfile.TemporaryDirectory() as tmp: + report.write_report(p, Path(tmp)) + text = (Path(tmp) / "REPORT.md").read_text(encoding="utf-8") + self.assertIn("**66 become plain stairs** (5 of them wired dangling exits)", text) + self.assertIn("**Wired exits**: 6 legacy exits", text) + + +class PlacePool(unittest.TestCase): + """Fix 3 in the generator: a failed group stays unplaced and nothing is placed through it.""" + + # a -> b -> c, and d reachable from the overworld directly + LINKS = {"a": {"ow"}, "b": {"a"}, "c": {"b"}, "d": {"ow"}} + + def run_pool(self, fails, pool=("a", "b", "c", "d")): + placed, calls = {"ow"}, [] + + def place(gid): + calls.append(gid) + if gid in fails: + return fails[gid] + placed.add(gid) + return {"chosen": gid} + + res = build_plan.place_pool(pool, lambda gid: bool(self.LINKS[gid] & placed), lambda gid, rem: set(), + lambda gid: 0, place) + return res, calls + + def test_all_placed(self): + (order, failed, left), calls = self.run_pool({}) + self.assertEqual((order, failed, left), (["a", "b", "c", "d"], [], [])) + self.assertEqual(calls, order) + + def test_failed_group_stays_unplaced(self): + (order, failed, left), calls = self.run_pool({"b": {"failed": True, "ideal": {}}}) + self.assertEqual(order, ["a", "d"]) + self.assertEqual(failed, ["b"]) + self.assertEqual(left, ["c"]) # reachable only through b + self.assertEqual(calls.count("b"), 1) # never retried + + def test_no_placement_record_is_a_failure(self): + (order, failed, left), _calls = self.run_pool({"a": None}) + self.assertEqual((order, failed, left), (["d"], ["a"], ["b", "c"])) + + +def _run_one(test_id): + """Run one test in a worker process; return (id, outcome, report).""" + import io + + out = io.StringIO() + result = unittest.TextTestRunner(stream=out, verbosity=0).run( + unittest.defaultTestLoader.loadTestsFromName(test_id, sys.modules[__name__])) + outcome = "ok" if result.wasSuccessful() else "fail" + if result.wasSuccessful() and result.skipped: + outcome = "skip" + return test_id, outcome, "" if result.wasSuccessful() else out.getvalue() + + +def main(argv=None): + """Run the suite, by default in parallel worker processes (one test per task; results are the same).""" + import argparse + import multiprocessing + import time + from concurrent.futures import ProcessPoolExecutor + + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--jobs", type=int, default=min(os.cpu_count() or 1, 8), + help="worker processes (1 runs the tests in this process)") + args = ap.parse_args(argv) + if args.jobs <= 1 or "fork" not in multiprocessing.get_all_start_methods(): + return 0 if unittest.main(argv=[sys.argv[0]], exit=False).result.wasSuccessful() else 1 + + def ids(suite): + for t in suite: + if isinstance(t, unittest.TestSuite): + yield from ids(t) + else: + yield t.id().split(".", 1)[1] # Class.method, looked up in this module by the worker + + tests = sorted(ids(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))) + start = time.monotonic() + verify_plan.verify(json.loads(PLAN.read_text(encoding="utf-8"))) # warm the relayering cache the workers fork + with ProcessPoolExecutor(args.jobs, mp_context=multiprocessing.get_context("fork")) as pool: + results = list(pool.map(_run_one, tests)) + failed = [(t, report) for t, outcome, report in results if outcome == "fail"] + for t, report in failed: + sys.stderr.write("{}\nFAIL: {}\n{}\n".format("=" * 70, t, report)) + skipped = sum(1 for _t, outcome, _r in results if outcome == "skip") + sys.stderr.write("{}\nRan {} tests in {:.3f}s ({} workers)\n\n{}\n".format( + "-" * 70, len(results), time.monotonic() - start, args.jobs, + "FAILED (failures={})".format(len(failed)) if failed else + "OK" + (" (skipped={})".format(skipped) if skipped else ""))) + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/migration/placement-plan/scripts/verify_plan.py b/migration/placement-plan/scripts/verify_plan.py index fb1fb8e..acbdddd 100644 --- a/migration/placement-plan/scripts/verify_plan.py +++ b/migration/placement-plan/scripts/verify_plan.py @@ -1,9 +1,53 @@ #!/usr/bin/env python3 -"""Independent check of placement-plan.json (uses only the JSON). +"""Check placement-plan.json: its rules from the JSON alone, and its fidelity to the content. - python3 -I scripts/verify_plan.py [placement-plan.json] + python3 -I scripts/verify_plan.py [placement-plan.json] [--content DIR] +Two checks with separate jobs: + + * verify(plan), always: re-derives the placement rules from the plan's own + data (the list below). It needs only the JSON, so tests and CI run it. + * verify_content(plan, DIR), with --content: rebuilds the plan from the + content checkout at content_git_head with the generator and requires the + published JSON to equal the rebuild field for field (content_root, the + checkout's path, aside). This is the fidelity check: every copied field, + source fact, inventory entry and count is compared with what the content + produces, so an edit that keeps the JSON consistent with itself (an exit + dropped from exit_inventory together with summary.exit_objects, a source + value changed in every copy) fails here, even when the rules still hold. + +The JSON-only cross-checks of copies below are defence in depth, not the +fidelity guarantee; that is --content. + +verify(plan) checks: + +Values: + * the plan identifies its input: a content_root path, a 40-hex + content_git_head, content_worktree_clean, and the inputs digest + * the plan's numbers are integers: no floats, and booleans only under + the flag keys (BOOL_KEYS); the plan file is at most MAX_PLAN_BYTES +Groups: + * overworld maps are 24 x 24 (settings.overworld_map_size, every rows grid) + * one overworld group; group ids and member maps (overworld cells + included) are unique; every group's classification and reason are known + values; the summary's group, map and classification counts are re-derived + (the overworld's map and per-layer counts from its cells) Placement (every placed group, proposed and tentative): + * every place group has a proposed placement and every uncertain group a + tentative one (an unreachable group may have one from an alignment hint); + a failed placement fails the plan; the summary's placement order, unplaced + groups and failed placements are re-derived + * offsets = ideal + shift_from_ideal and extra_layers_from_ideal in the + orientation's direction; the ideal follows from the main connection (its + far end, one layer away in the exit's direction, within the recorded + vertical bounds), which is one of the group's exits to the overworld or + an earlier placed group; only an alignment-hinted tentative placement + has none (a proposed placement carries no hint), and its hint is one of + settings.tentative_alignments: its ideal is the anchor's offsets, the + configured layers away; every configured alignment is applied (its + group's tentative placement carries it) + * no placement goes further from its ideal spot than max_extra_layers and + max_shift_tiles allow, or outside its vertical bounds * member plane origin == local_offset + (x_offset, y_offset, layer_offset); no placed group has a relayered region (its member maps are recorded on one layer each; the generator refuses to publish otherwise) @@ -28,12 +72,43 @@ stairwell carries the engine rule segments_drawn_only_on_player_layer * a stairwell's direction matches the exits it replaces, and each end holds exactly the exit objects (archetype, exit flag) that start there (a - one-way stairwell's far end is a landing) + one-way stairwell's far end is a landing); every replaces entry equals its + exit record (tiles, target, archetype, plane endpoints, direction) + * a column stairwell leaves its column by at most stairwell_max_bend_tiles + (the same limit the blocked-site chain search uses) + * kind, shape (from its ends), id series, max_bend, owners (group, owner, + other, connects: from the exits it replaces) and the intermediate tiles' + decoration (the exits' stair family) are re-derived; so are the summary's + stairwell and blocked-site counts Exits: + * exit_inventory lists every exit object of the content once, with values + of the right types, a known form and direction, a target exactly when its + form resolves one, the data-problem facts its form or problem needs, + each resolved target on a tile of its target map; the exit + records are exactly the copies it calls for (one per group a crossing exit + touches, the overworld's own exits in internal_exits; a wired exit as + data_problems lists it); every content field of a record (tiles, target, + archetype, form, direction, flags) equals its inventory entry, and a record + holds no field the verifier does not know; the exit data problems (every + entry, field by field) and the summary and per-group crossing counts are + re-derived + * every other copy of an exit (blocked sites, relayering exits, wired and + kept dangling exits, data problems, main connections) equals its record + or inventory entry; a blocked site lists at least one exit, only exits + that need a stairwell, between its own two owners (a relayering site: + overworld exits between its top and bottom maps), and has the shape of + its ends, which are 2+ layers and at most one tile per layer apart; a + kept decoration sits at its tile on the plane; + every placement has a main connection to something placed before it, + except one forced by an alignment hint * the records of one exit (one per group it touches) are identical; an exit labelled stairwell is listed in exactly that stairwell's replaces - * every exit record's plane_source / plane_target equals origin + tile on - the tile's final layer, and its "becomes" label matches its geometry; + * an exit record has its placement fields (plane ends, distances, label) + exactly when both ends are placed (or overworld); the plane ends and + overworld_plane_source / _target equal origin + tile on the tile's final + layer, xy_distance and layer_delta follow from them, the "becomes" label + matches the geometry, and a portal's reason, its text and slope_reachable + are re-derived from the blocked sites, the layers and the later placed end; wired exits are plain stairs and their verdict follows from their evidence; exits the owner keeps as decoration have no exit record * every exit 2+ layers and at most one tile per layer apart is a stairwell @@ -50,6 +125,23 @@ moved tile ends on layer 0 * cut walkable edges, seams, seam pieces and their new stairs; steep steps; new contacts; moved maps and the before/after layer deltas of exits +Not re-derived: the generator's explanations and evidence, which the +migration does not act on. That is classification evidence, reach depth and +script references; the placement rationale beyond the consistency above +(orientation and its evidence, the vertical bounds and constraint note, +conflicts_resolved, main_stairwell_blocked, bounding boxes, formulas); the +evidence of blocked sites (column_occupied, layers, reason), of wiring and of +kept decorations (would_land_on) other than the verdict and target; the +relayering's descriptions (why, resolution, judgement calls, cave options, the +maps, boxes and z ranges listed per region and unit) beyond the re-derived +regions, units and final layers; placement_changes_from_v1, +view_radius_sensitivity and conventions; and the tile-link, layout and +connectivity data problems. +This check uses only the JSON, so it cannot see an edit that keeps the JSON +consistent with itself (an exit dropped from exit_inventory together with +summary.exit_objects, say). `build_plan.py --check` covers that: it +regenerates from the content and fails unless every artifact is +byte-identical. Exit status 0 when every check passes. """ @@ -57,13 +149,79 @@ import bisect import collections +import hashlib import json import re import sys from pathlib import Path +sys.dont_write_bytecode = True # --content imports the generator; keep the source tree free of caches + WORLD_RE = re.compile(r"/world_(-?\d+)_(-?\d+)(?:_(-?\d+))?$") +OW_SIZE = 24 # overworld maps are 24 x 24; plane tile = 24 * filename coordinate + tile +SCHEMA = "atrinik-overworld-placement-plan/4" # /4 adds exit_inventory and the failed-placement bookkeeping +MAX_PLAN_BYTES = 64 * 1024 * 1024 # the plan is about 3 MB; a larger file is not a plan +# The plan's numbers are integers; JSON booleans appear only under these flag keys, and no value is a float. +BOOL_KEYS = {"content_worktree_clean", "segments_drawn_only_on_player_layer", "existing", "exit", "walkable", + "landing_blocked", "per_player_last_eat_1", "explicit_last_heal_0", "target_is_map_default_entry", + "unique_getpath", "test_only", "house_tuple_unique", "main_stairwell_blocked", "slope_reachable", + "relayered", "ok"} + + +def _has_bad_number(plan) -> bool: + """Whether the plan holds a float, or a boolean outside the flag keys (fast pass, no paths).""" + stack = [(plan, None)] + while stack: + x, key = stack.pop() + t = type(x) + if t is dict: + stack.extend((v, k) for k, v in x.items() if type(v) is not str) + elif t is list: + stack.extend((v, key) for v in x if type(v) is not str) + elif t is float or t is bool and key not in BOOL_KEYS: + return True + return False + + +def number_type_errors(plan, limit=5) -> list: + """Paths of booleans outside the flag keys and of floats: a boolean is not a count or a coordinate, + however Python compares it.""" + if not _has_bad_number(plan): + return [] + bad, stack = [], [(plan, None, "")] + while stack and len(bad) < limit: + x, key, path = stack.pop() + if isinstance(x, bool): + if key not in BOOL_KEYS: + bad.append(path) + elif isinstance(x, float): + bad.append(path) + elif isinstance(x, dict): + stack.extend((v, k, "{}/{}".format(path, k)) for k, v in x.items()) + elif isinstance(x, list): + stack.extend((v, key, "{}[{}]".format(path, i)) for i, v in enumerate(x)) + return sorted(bad) N8 = [(dx, dy) for dy in (-1, 0, 1) for dx in (-1, 0, 1) if (dx, dy) != (0, 0)] +# Fields of an exit record that come from the content: equal to its inventory entry. +SOURCE_FIELDS = ("source_map", "source_tile", "target_map", "target_tile", "arch", "line", "form", "vertical", + "authored_target", "target_is_map_default_entry", "per_player_last_eat_1", "event_scripts", + "vertical_inferred_from") +# Fields a record has exactly when both of its ends are placed, and those that depend on its label. +PLACED_FIELDS = ("plane_source", "plane_target", "xy_distance", "layer_delta", "becomes") +LABEL_FIELDS = ("stairwell", "portal_reason", "portal_reason_text", "slope_reachable") +RECORD_FIELDS = set(SOURCE_FIELDS) | set(PLACED_FIELDS) | set(LABEL_FIELDS) | { + "source_group", "target_group", "wired", "overworld_plane_source", "overworld_plane_target"} +# Stair families: a stairwell's intermediate tiles show the down archetype of the first family +# (in this order) that one of its exit archetypes belongs to (build_plan.STAIR_FAMILIES). +STAIR_FAMILIES = (("stairs_spiral_stone_", "stairs_spiral_stone_down"), ("stairs_stone_", "stairs_stone_down"), + ("stairs_wood_", "stairs_wood_down"), ("stairs_red_", "stairs_red_down"), + ("stairs_", "stairs_down"), ("ladder_", "ladder_down")) +SW_PREFIX = {"placement": "SW", "sloped": "SL", "relayering": "SR"} +# The reader's exit forms: those that resolve to a target, and those that never do. +TARGET_FORMS = {"explicit", "tiled", "same-map", "automatic-link", "shop-mat"} +NO_TARGET_FORMS = {"tiled-unresolved", "unresolved", "dynamic"} +# Tile-link slot names of the map format (the reader's SLOT_NAMES), used in data-problem details. +SLOT_NAMES = {1: "N", 2: "E", 3: "S", 4: "W", 5: "NE", 6: "SE", 7: "SW", 8: "NW", 9: "up", 10: "down"} def world_cell(path: str): @@ -88,12 +246,14 @@ def __init__(self): def add(self, x, y): self.rows[y].append(x) self.count += 1 - b = self.box - self.box = (x, y, x, y) if b is None else (min(b[0], x), min(b[1], y), max(b[2], x), max(b[3], y)) def done(self): + """Sort each row and compute the bounding box; called once every tile is added.""" for xs in self.rows.values(): xs.sort() + if self.rows: + self.box = (min(xs[0] for xs in self.rows.values()), min(self.rows), + max(xs[-1] for xs in self.rows.values()), max(self.rows)) def any_within(self, x, y, r) -> bool: b = self.box @@ -133,123 +293,545 @@ def flood(keys, same): return out -def verify(plan: dict) -> dict: - st = plan["settings"] - R, M, tol = st["view_radius_tiles"], st["contact_margin_tiles"], st["stair_tolerance_tiles"] - HZ, MINR, WDZ = st["layer_height_z"], st["relayer_min_region_tiles"], st["walkable_dz"] - ow = next(g for g in plan["groups"] if g["classification"] == "overworld") - ow_id = ow["id"] - rl = plan["relayering"] - errors = [] +_RELAYER_CACHE = {} # one entry: the last relayering re-derived in this process, by the hash of its inputs - # ------------------------------------------------------------ occupancy - owners = collections.defaultdict(dict) # layer -> owner -> Rows - claim = collections.defaultdict(dict) # layer -> (x, y) -> (owner, map) - origin = {} - leg = {} # (legacy layer, x, y) -> [map, final layers] - def put(owner, mapname, layer, x0, y0, rows): - rr = owners[layer].setdefault(owner, Rows()) - for y, row in enumerate(rows): - for x, ch in enumerate(row): - if ch == "#": - t = (x0 + x, y0 + y) - prev = claim[layer].get(t) - if prev is not None and prev != (owner, mapname): - errors.append("tile {} layer {} claimed by {} and {}".format(t, layer, prev[1], mapname)) - claim[layer][t] = (owner, mapname) - rr.add(*t) +def _relayering(ow_rows, leg, rl, internal_exits, record_keys, settings): + """relayering_rules() for these inputs, memoized: tests and the generator verify plans whose relayering + inputs are identical, and the result is a pure function of them (the key covers every input).""" + key = hashlib.sha256(json.dumps([ow_rows, rl, internal_exits, sorted(record_keys), settings], sort_keys=True, + separators=(",", ":")).encode()).hexdigest() + if key not in _RELAYER_CACHE: + _RELAYER_CACHE.clear() + _RELAYER_CACHE[key] = relayering_rules(leg, rl, internal_exits, record_keys, *settings) + return _RELAYER_CACHE[key] - ow_rows = ow["occupied_rows_by_layer"] - for path, by_layer in sorted(ow_rows.items()): + +def relayering_rules(leg, rl, internal_exits, record_keys, HZ, MINR, WDZ, tol): + """The relayering, re-derived from the overworld rows (leg) and the tile heights. + + Returns (errors, final layer of every legacy overworld tile, counts). A pure + function of its arguments; record_keys holds the seam pieces' exits that have an exit record.""" + errors = [] + heights, blocked, ground = {}, set(), {} + for path, rows in sorted(rl["tile_heights"].items()): X, Y, Z = world_cell(path) - for Ls, rows in sorted(by_layer.items(), key=lambda kv: int(kv[0])): - put(path, path, int(Ls), 24 * X, 24 * Y, rows) - for y, row in enumerate(rows): - for x, ch in enumerate(row): - if ch == "#": - leg.setdefault((Z, 24 * X + x, 24 * Y + y), [path, []])[1].append(int(Ls)) - placed = [] - for g in plan["groups"]: - p = g.get("placement") - if not p or p.get("status") not in ("proposed", "tentative"): - continue - placed.append(g) - for m in g["maps"]: - lo = m["local_offset"] - exp = [lo[0] + p["x_offset"], lo[1] + p["y_offset"], lo[2] + p["layer_offset"]] - got = p["member_origins"][m["path"]] - if exp != got: - errors.append("origin mismatch {} {} != {}".format(m["path"], got, exp)) - origin[m["path"]] = got - if "occupied_rows" not in m: - errors.append("no occupied rows for placed map {}".format(m["path"])) + for y, row in enumerate(rows): + for x, tok in enumerate(row.split(" ")): + if tok == ".": + continue + k = (Z, 24 * X + x, 24 * Y + y) + if tok.endswith("b"): + blocked.add(k) + tok = tok[:-1] + if "/" in tok: + tok, g = tok.split("/") + ground[k] = int(g) + heights[k] = int(tok) + split = {} + for sp in rl["split_tiles"]: + X, Y, Z = world_cell(sp["map"]) + split[(Z, 24 * X + sp["tile"][0], 24 * Y + sp["tile"][1])] = sp + # a split tile's raised part is its upper layer; which tiles split is derived below + main = {k: max(v[1]) for k, v in leg.items()} + # regions + q = {k: h // HZ for k, h in heights.items() if h >= HZ} + comps = flood(q, lambda a, b: q[a] == q[b]) + comps.sort(key=lambda c: (-len(c), c[0])) + relay_comp = [len(c) >= MINR for c in comps] + comp_of = {k: i for i, c in enumerate(comps) for k in c} + changed = True + while changed: + changed = False + for i, c in enumerate(comps): + if relay_comp[i]: continue - put(g["id"], m["path"], got[2], got[0], got[1], m["occupied_rows"]) - for layer in owners.values(): - for rr in layer.values(): - rr.done() + if any((k[0], k[1] + dx, k[2] + dy) in comp_of and relay_comp[comp_of[(k[0], k[1] + dx, k[2] + dy)]] + for k in c for dx, dy in N8): + relay_comp[i] = True + changed = True + relayered = {k for i, c in enumerate(comps) if relay_comp[i] for k in c} + got_r = sorted((c[0][0], q[c[0]], len(c)) for i, c in enumerate(comps) if relay_comp[i]) + got_k = sorted((c[0][0], q[c[0]], len(c)) for i, c in enumerate(comps) if not relay_comp[i]) + if got_r != sorted((r["layer_before"], r["q"], r["tiles"]) for r in rl["regions"]): + errors.append("relayered regions differ from the tile heights") + if got_k != sorted((r["layer_before"], r["q"], r["tiles"]) for r in rl["kept_regions"]): + errors.append("kept regions differ from the tile heights") + # units and shifts + units = [] + unit_of = {} + bylayer = collections.defaultdict(list) + for k in leg: + if k[0] != 0: + bylayer[k[0]].append(k) + for L in sorted(bylayer): + for c in flood(bylayer[L], lambda a, b: True): + for k in c: + unit_of[k] = len(units) + units.append(c) + du = {} + for i, c in enumerate(units): + offs = {main[k] - k[0] for k in c if k not in relayered} + if len(offs) > 1: + errors.append("unit with {} tiles on layer {} does not move as one: shifts {}".format( + len(c), c[0][0], sorted(offs))) + du[i] = min(offs) if offs else 0 + if du[i] > 0 and c[0][0] < 1 or du[i] < 0 and c[0][0] >= 0: + errors.append("unit on layer {} moves {} (storeys move only up, caves only down)".format(c[0][0], du[i])) + dr = {} + for k in sorted(relayered): + d = main[k] - k[0] - q[k] - (du[unit_of[k]] if k in unit_of else 0) + ci = comp_of[k] + if dr.setdefault(ci, d) != d: + errors.append("region tile {} adjustment {} differs from its region's {}".format(k, d, dr[ci])) + if d > 0: + errors.append("region tile {} is adjusted up by {}".format(k, d)) + for k in leg: + if k in relayered: + continue + if k[0] == 0 and main[k] != 0: + errors.append("surface tile {} of {} moved to layer {} without a region".format(k, leg[k][0], main[k])) + # split tiles: a relayered tile whose sub-layer 0 floor stays below its raised floor keeps + # that ground part on its unit's layer; derived from the tile heights, not the records + derived_split = {} + for k in relayered: + g = ground.get(k) + base_layer = k[0] + (du[unit_of[k]] if k in unit_of else 0) + if g is not None and g // HZ < q[k] and base_layer != main[k]: + derived_split[k] = base_layer + if set(derived_split) != set(split): + errors.append("split tiles differ: {} derived from the tile heights, {} recorded".format( + len(derived_split), len(split))) + for k, v in leg.items(): + want = sorted({derived_split[k], main[k]}) if k in derived_split else [main[k]] + if sorted(v[1]) != want: + errors.append("legacy tile {} of {} appears on layers {}, expected {}".format(k, v[0], sorted(v[1]), want)) + for k, sp in split.items(): + if k not in derived_split or sp["ground_layer"] != derived_split[k] or sp["upper_layer"] != main[k] or \ + sp["floors"].get("0") != ground.get(k): + errors.append("split tile {} is inconsistent".format(k)) + if sorted((r["layer_before"], r["q"], r["tiles"], r["in_place_adjustment"]) for r in rl["regions"]) != \ + sorted((comps[i][0][0], q[comps[i][0]], len(comps[i]), dr.get(i, 0)) for i in range(len(comps)) + if relay_comp[i]): + errors.append("region in-place adjustments differ from the final layers") + got_u = sorted((units[i][0][0], d, len(units[i])) for i, d in du.items() if d) + if got_u != sorted((u["layer_before"], u["shift"], u["tiles"]) for u in rl["units_moved"]): + errors.append("moved units differ from the final layers") + # column order, caves underground, nothing lands on the surface + cols = collections.defaultdict(list) + for k, v in leg.items(): + cols[(k[1], k[2])].append((k[0], min(v[1]), max(v[1]))) + for (x, y), lst in cols.items(): + lst.sort() + for (la, _mna, mxa), (lb, mnb, _mxb) in zip(lst, lst[1:]): + if not mxa < mnb: + errors.append("column ({}, {}): layer {} ends at or above layer {}".format(x, y, la, lb)) + for k, v in leg.items(): + if k[0] < 0 and max(v[1]) >= 0: + errors.append("cave tile {} of {} reaches layer {}".format(k, v[0], max(v[1]))) + if main[k] != k[0] and main[k] == 0: + errors.append("moved tile {} of {} ends on the surface layer".format(k, v[0])) + # maps moved + mm = collections.defaultdict(collections.Counter) + moved_maps = set() + for k, v in leg.items(): + mm[v[0]][main[k]] += 1 + if main[k] != k[0]: + moved_maps.add(v[0]) + exp_maps = sorted(moved_maps) + if exp_maps != sorted(m["path"] for m in rl["maps_moved"]): + errors.append("moved maps differ from the final layers") + for m in rl["maps_moved"]: + if {str(L): n for L, n in sorted(mm[m["path"]].items())} != m["final_layers"]: + errors.append("final layers of {} differ".format(m["path"])) - # rows coverage near every placed or stairwell tile - used_cells = collections.defaultdict(set) - for g in placed: - for z, by in owners.items(): - rr = by.get(g["id"]) - if rr is not None: - used_cells[z].update((x // 24, y // 24) for x, y in rr.tiles()) - for sw in plan["stairwells"]: - for t in sw["tiles"][1:-1]: - used_cells[t["layer"]].add((t["x"] // 24, t["y"] // 24)) - prefix = ow_id + "_" - for z, cells in sorted(used_cells.items()): - have = {tuple(c) for c in ow["cells_by_layer"].get(str(z), [])} - for cx, cy in sorted(cells): - for dx in (-1, 0, 1): - for dy in (-1, 0, 1): - if (cx + dx, cy + dy) in have: - path = "{}{}_{}{}".format(prefix, cx + dx, cy + dy, "" if z == 0 else "_{}".format(z)) - if path not in ow_rows: # an included map carries every one of its final layers - errors.append("rows missing for {} near used layer {}".format(path, z)) - for mm in rl["maps_moved"]: - if mm["path"] not in ow_rows: - errors.append("rows missing for relayered map {}".format(mm["path"])) - stacked = set() - for z, cells in ow["cells_by_layer"].items(): - if int(z) != 0: - stacked.update(tuple(c) for c in cells) - for z, cells in ow["cells_by_layer"].items(): - for X, Y in cells: - path = "{}{}_{}{}".format(prefix, X, Y, "" if int(z) == 0 else "_{}".format(z)) - if (int(z) != 0 or (X, Y) in stacked) and path not in ow_rows: - errors.append("rows missing for {} (off the surface or stacked)".format(path)) + def walkable(a, b): + return a in heights and b in heights and a not in blocked and b not in blocked and \ + abs(heights[a] - heights[b]) < WDZ - # ---------------------------------------------------- final overworld layer - void_ends = {(v["map"], tuple(v["tile"])): v["layer"] for v in rl["exit_ends_on_void_tiles"]} + def rest(k): + return heights[k] - HZ * q[k] if k in relayered else heights[k] - def ow_layer(path, tx, ty): - X, Y, Z = world_cell(path) - k = (Z, 24 * X + tx, 24 * Y + ty) - if k in leg: - return max(leg[k][1]) - return void_ends.get((path, (tx, ty)), Z) + moved = sorted(k for k in leg if main[k] != k[0]) + moved_set = set(moved) + cut = set() + for k in moved: + for dx, dy in N8: + n = (k[0], k[1] + dx, k[2] + dy) + if n in leg and walkable(k, n) and main[k] != main[n]: + a, b = min(k, n), max(k, n) + cut.add((a, b) if main[a] < main[b] else (b, a)) + edges = sorted(cut) + parent = list(range(len(edges))) - # ------------------------------------------------------ related owners - def end_of(e, side): - g = e["source_group"] if side == "s" else e["target_group"] - m = e["source_map"] if side == "s" else e["target_map"] - t = e["source_tile"] if side == "s" else e["target_tile"] - if g == ow_id: - return m, ow_layer(m, *t) - return g, None + def find(i): + while parent[i] != i: + parent[i] = parent[parent[i]] + i = parent[i] + return i - rel = collections.defaultdict(set) # owner -> {(owner, layer or None)} - for g in placed: - for key in ("entrances_from_overworld", "exits_to_overworld", "entrances_from_other_groups", - "exits_to_other_groups"): - for e in g.get(key, []): - a, b = end_of(e, "s"), end_of(e, "t") - rel[a[0]].add(b) - rel[b[0]].add(a) + lay = [(main[lo], main[hi]) for lo, hi in edges] + for side in (0, 1): + at = collections.defaultdict(list) + for i, e in enumerate(edges): + at[(lay[i], e[side][1], e[side][2])].append(i) + for i, e in enumerate(edges): + for dx in (-1, 0, 1): + for dy in (-1, 0, 1): + for j in at.get((lay[i], e[side][1] + dx, e[side][2] + dy), ()): + a, b = find(i), find(j) + if a != b: + parent[max(a, b)] = min(a, b) + seams = collections.defaultdict(list) + for i in range(len(edges)): + seams[find(i)].append(i) + cell = rl.get("seam_cell", 24) + pieces = {} + for idx in seams.values(): + sid = min(edges[i] for i in idx) + for i in idx: + lo = edges[i][0] + pieces.setdefault((sid, lo[1] // cell, lo[2] // cell), set()).add(edges[i]) + if len(seams) != len(rl["seams"]) or sum(s["cut_edges"] for s in rl["seams"]) != len(edges): + errors.append("seams differ: {} seams with {} cut edges recomputed, {} with {} recorded".format( + len(seams), len(edges), len(rl["seams"]), sum(s["cut_edges"] for s in rl["seams"]))) + piece_by_edge = {} + for key, es in pieces.items(): + for e in es: + piece_by_edge[e] = key + seen_pieces = set() + ns_by_id = {n["id"]: n for n in rl["new_stairs"]} + for s_ in rl["seams"]: + for pc in s_["pieces"]: + e = (tuple(pc["low_tile"]), tuple(pc["high_tile"])) + key = piece_by_edge.get(e) + if key is None: + errors.append("seam piece edge {} of {} is not a cut walkable edge".format(e, s_["id"])) + continue + if key in seen_pieces or len(pieces[key]) != pc["edges"] or [key[1], key[2]] != pc["cell"]: + errors.append("seam piece {} of {} does not match the recomputed piece".format(pc["cell"], s_["id"])) + seen_pieces.add(key) + if pc["continuity"] == "new stairs" and pc["stairs"] and pc["stairs"][0] in ns_by_id: + first = ns_by_id[pc["stairs"][0]] + if "exit" not in pc and (first["low_tile_legacy"], first["high_tile_legacy"]) != \ + (pc["low_tile"], pc["high_tile"]): + errors.append("seam piece {} of {} does not put its first stairs on its representative edge".format( + pc["cell"], s_["id"])) + elif pc["continuity"] != "existing exit" or tuple(pc.get("exit", ())) not in record_keys: + errors.append("seam piece {} of {} has no continuity".format(pc["cell"], s_["id"])) + for sid in pc["stairs"]: + n = ns_by_id.get(sid) + objs = {o.get("tile"): o for o in (n or {}).get("objects", [])} + lo_o, hi_o = objs.get("low", {}), objs.get("high", {}) + if n is None or len(n["objects"]) != 2 or not ( + lo_o.get("exit") is True and lo_o.get("exit_to") == "high" + and re.match(r"^(stairs|ladder)_.*up\d*$", lo_o.get("archetype", "")) + and hi_o.get("exit") is True and hi_o.get("exit_to") == "low" + and re.match(r"^(stairs|ladder)_.*down\d*$", hi_o.get("archetype", ""))): + errors.append("new stairs {} are not an up stair on the low tile and a down stair on the high " + "tile targeting each other".format(sid)) + continue + lo, hi = (tuple(n["low_tile_legacy"]), tuple(n["high_tile_legacy"])) if n else (None, None) + if n is None or piece_by_edge.get((lo, hi)) != key or n["low"] != [lo[1], lo[2], main[lo]] \ + or n["high"] != [hi[1], hi[2], main[hi]] or n["high"][2] - n["low"][2] != 1 \ + or cheb(n["low"], n["high"]) > tol: + errors.append("new stairs {} of seam piece {} of {} are not plain stairs across one of its " + "cut edges".format(sid, pc["cell"], s_["id"])) + if len(seen_pieces) != len(pieces): + errors.append("{} seam pieces recomputed, {} recorded".format(len(pieces), len(seen_pieces))) + if sorted(n["id"] for n in rl["new_stairs"]) != sorted(i for s_ in rl["seams"] for pc in s_["pieces"] + for i in pc["stairs"]): + errors.append("new stairs and seam pieces do not list the same stairs") + # continuity: the two tiles of every cut edge stay connected on the final layers + # (walking, legacy overworld exits, new stairs) within the maps with tile heights + hmaps = set(rl["tile_heights"]) + parent = {} + + def uf_find(a): + parent.setdefault(a, a) + while parent[a] != a: + parent[a] = parent[parent[a]] + a = parent[a] + return a + + def uf_union(a, b): + ra, rb = uf_find(a), uf_find(b) + if ra != rb: + parent[rb] = ra + + gpos = collections.defaultdict(list) + for k, v in leg.items(): + if v[0] in hmaps and k in heights and k not in blocked: + gpos[(main[k], k[1], k[2])].append((k, rest(k))) + if k in derived_split: + gpos[(derived_split[k], k[1], k[2])].append((("ground",) + k, ground[k])) + for (L, x, y), here in gpos.items(): + for dx, dy in ((1, 0), (-1, 1), (0, 1), (1, 1)): + for a, ra in here: + for b, rb in gpos.get((L, x + dx, y + dy), ()): + if abs(ra - rb) < WDZ: + uf_union(a, b) + for r in internal_exits: + X, Y, Z = world_cell(r["source_map"]) + X2, Y2, Z2 = world_cell(r["target_map"]) + uf_union((Z, 24 * X + r["source_tile"][0], 24 * Y + r["source_tile"][1]), + (Z2, 24 * X2 + r["target_tile"][0], 24 * Y2 + r["target_tile"][1])) + for n in rl["new_stairs"]: + uf_union(tuple(n["low_tile_legacy"]), tuple(n["high_tile_legacy"])) + apart = [e for e in edges if uf_find(e[0]) != uf_find(e[1])] + if apart: + errors.append("{} cut walkable edges are left without continuity, e.g. {}".format(len(apart), apart[0])) + steep = set() + for k in sorted(set(moved) | relayered): + for dx, dy in N8: + n = (k[0], k[1] + dx, k[2] + dy) + if n in leg and walkable(k, n) and main[k] == main[n] and abs(rest(k) - rest(n)) >= WDZ: + steep.add((min(k, n), max(k, n))) + rec_steep = {tuple(sorted(((s_["layer"], *s_["tiles"][0]), (s_["layer"], *s_["tiles"][1])))) for s_ in rl["steep_steps"]} + if steep != rec_steep: + errors.append("steep steps differ: {} recomputed, {} recorded".format(len(steep), len(rec_steep))) + finals = collections.defaultdict(list) + for k, v in leg.items(): + for L in v[1]: + finals[(L, k[1], k[2])].append(k) + contacts = set() + for k in moved: + for dx, dy in N8: + for t in finals.get((main[k], k[1] + dx, k[2] + dy), ()): + if t[0] != k[0]: + contacts.add((min(k, t), max(k, t))) + if contacts != {(tuple(c["tiles"][0]), tuple(c["tiles"][1])) for c in rl["new_contacts"]}: + errors.append("new contacts differ: {} recomputed, {} recorded".format(len(contacts), len(rl["new_contacts"]))) + for c in rl["new_contacts"]: + a, b = tuple(c["tiles"][0]), tuple(c["tiles"][1]) + walk = a in heights and b in heights and a not in blocked and b not in blocked and \ + abs(rest(a) - rest(b)) < WDZ + if c["walkable"] != walk or c["final_layer"] != main.get(a if a in moved_set else b): + errors.append("new contact {} / {} records walkable {} on layer {}; recomputed {}".format( + a, b, c["walkable"], c["final_layer"], walk)) + return errors, main, {"tile_heights": len(heights), "regions": len(got_r), "kept_regions": len(got_k), + "units": len(units), "moved_units": len(got_u), "moved_tiles": len(moved), + "cut_edges": len(edges), "seam_pieces": len(pieces), "steep_steps": len(steep), + "new_contacts": len(contacts)} + + +def verify(plan: dict) -> dict: + """The JSON-only check. A malformed plan (a missing key, a value of the wrong shape) fails it with an + error rather than an exception.""" + try: + return _verify(plan) + except Exception as exc: # noqa: BLE001 - any malformation is a failed check, not a crash + return {"ok": False, "errors": ["the plan is malformed: {}: {}".format(type(exc).__name__, exc)]} + + +def _verify(plan: dict) -> dict: + if plan.get("schema") != SCHEMA: + return {"ok": False, "errors": ["schema {} is not {}, the shape this verifier checks".format( + plan.get("schema"), SCHEMA)]} + errors = [] + # what the plan was built from: the checkout path (shown in the report; --content ignores its value), the + # commit, whether the inputs were clean, and the digest of the input trees + dig = plan.get("content_inputs_digest") + if not (isinstance(plan.get("content_root"), str) and plan["content_root"] + and isinstance(plan.get("content_git_head"), str) and re.fullmatch(r"[0-9a-f]{40}", plan["content_git_head"]) + and type(plan.get("content_worktree_clean")) is bool + and isinstance(dig, dict) and isinstance(dig.get("trees"), list) and dig["trees"] + and all(isinstance(t, str) and t for t in dig["trees"]) and type(dig.get("files")) is int + and isinstance(dig.get("sha256"), str) and re.fullmatch(r"[0-9a-f]{64}", dig["sha256"])): + errors.append("the plan does not identify its input (content_root, content_git_head, content_worktree_clean, " + "content_inputs_digest)") + bad_numbers = number_type_errors(plan) + if bad_numbers: + errors.append("the plan has booleans or floats where integers belong, e.g. {}".format(", ".join(bad_numbers))) + st = plan["settings"] + R, M, tol = st["view_radius_tiles"], st["contact_margin_tiles"], st["stair_tolerance_tiles"] + bend = st["stairwell_max_bend_tiles"] + HZ, MINR, WDZ = st["layer_height_z"], st["relayer_min_region_tiles"], st["walkable_dz"] + ow = next(g for g in plan["groups"] if g["classification"] == "overworld") + ow_id = ow["id"] + rl = plan["relayering"] + + # ------------------------------------------------------------ occupancy + owners = collections.defaultdict(dict) # layer -> owner -> Rows + claim = collections.defaultdict(dict) # layer -> (x, y) -> (owner, map) + origin = {} + leg = {} # (legacy layer, x, y) -> [map, final layers] + + def put(owner, mapname, layer, x0, y0, rows): + """Claim the '#' tiles of `rows` (at x0, y0) on `layer` for owner; return them in plane coordinates.""" + rr, here, me, out = owners[layer].setdefault(owner, Rows()), claim[layer], (owner, mapname), [] + for y, row in enumerate(rows): + x = row.find("#") + while x >= 0: + t = (x0 + x, y0 + y) + prev = here.get(t) + if prev is not None and prev != me: + errors.append("tile {} layer {} claimed by {} and {}".format(t, layer, prev[1], mapname)) + here[t] = me + rr.add(t[0], t[1]) + out.append(t) + x = row.find("#", x + 1) + return out + + ow_rows = ow["occupied_rows_by_layer"] + if st.get("overworld_map_size") != OW_SIZE: + errors.append("settings.overworld_map_size {} is not the {}-tile grid of the overworld coordinates".format( + st.get("overworld_map_size"), OW_SIZE)) + for path, by_layer in sorted(ow_rows.items()): + X, Y, Z = world_cell(path) + for Ls, rows in sorted(by_layer.items(), key=lambda kv: int(kv[0])): + if len(rows) != OW_SIZE or any(len(r) != OW_SIZE for r in rows): + errors.append("rows of {} on layer {} are not {} x {}".format(path, Ls, OW_SIZE, OW_SIZE)) + L = int(Ls) + for t in put(path, path, L, 24 * X, 24 * Y, rows): + leg.setdefault((Z,) + t, [path, []])[1].append(L) + placed = [] + for g in plan["groups"]: + p = g.get("placement") + if not p or p.get("status") not in ("proposed", "tentative"): + continue + placed.append(g) + for m in g["maps"]: + lo = m["local_offset"] + exp = [lo[0] + p["x_offset"], lo[1] + p["y_offset"], lo[2] + p["layer_offset"]] + got = p["member_origins"][m["path"]] + if exp != got: + errors.append("origin mismatch {} {} != {}".format(m["path"], got, exp)) + origin[m["path"]] = got + if "occupied_rows" not in m: + errors.append("no occupied rows for placed map {}".format(m["path"])) + continue + put(g["id"], m["path"], got[2], got[0], got[1], m["occupied_rows"]) + for layer in owners.values(): + for rr in layer.values(): + rr.done() + placed_ids = {g["id"] for g in placed} + summ = plan["summary"] + + # ----------------------------------------------------- placement status + # A failed placement must not be published; every place group is placed + # (proposed) and every uncertain group tentatively. The summary's order and + # unplaced lists are re-derived from the placement records. + others = [g for g in plan["groups"] if g["classification"] != "overworld"] + reasons = {"overworld": {"overworld"}, "place": {"place"}, "uncertain": {"uncertain"}, + "separate": {"guild", "other_world", "per_player", "ship", "system", "unreachable"}} + for g in plan["groups"]: + if g.get("reason") not in reasons.get(g["classification"], ()): + errors.append("group {} has classification {} with reason {}".format( + g["id"], g["classification"], g.get("reason"))) + if sum(1 for g in plan["groups"] if g["classification"] == "overworld") != 1: + errors.append("the plan has no single overworld group") + # identities are unique before anything is counted + for what, items in ( + ("group id", [g["id"] for g in plan["groups"]]), + ("member map", [m["path"] for g in others for m in g["maps"]] + [ + "{}_{}_{}{}".format(ow_id, X, Y, "" if int(z) == 0 else "_{}".format(z)) + for z, cells in ow["cells_by_layer"].items() for X, Y in cells])): + dup = sorted(k for k, n in collections.Counter(items).items() if n > 1) + if dup: + errors.append("{} {} {} listed {} times".format(what, dup[0], "and {} more are".format(len(dup) - 1) + if len(dup) > 1 else "is", "several")) + if summ.get("groups_total") != len(plan["groups"]) or summ.get("non_overworld_groups") != len(others) or \ + summ.get("maps_total") != sum(g["map_count"] for g in plan["groups"]) or \ + any(g["map_count"] != len(g["maps"]) for g in others) or \ + ow["map_count"] != sum(len(c) for c in ow["cells_by_layer"].values()) or \ + ow.get("layers") != {z: len(c) for z, c in sorted(ow["cells_by_layer"].items(), key=lambda kv: int(kv[0]))} or \ + summ.get("by_classification") != dict(sorted(collections.Counter( + "{}:{}".format(g["classification"], g["reason"]) for g in others).items())): + errors.append("summary group and map counts differ from the groups") + status = {g["id"]: (g.get("placement") or {}).get("status") for g in others} + for g in others: + gid, s = g["id"], status[g["id"]] + if g.get("placement") is not None and s is None: + errors.append("placement of {} has no status".format(gid)) + if s == "failed": + errors.append("placement of {} failed; a plan with a failed group must not be published".format(gid)) + elif s not in (None, "proposed", "tentative"): + errors.append("placement of {} has an unknown status {}".format(gid, s)) + want = {"place": "proposed", "uncertain": "tentative"}.get(g["classification"]) + if want is not None and s != want: + errors.append("{} group {} has no {} placement".format(g["classification"], gid, want)) + if s == "proposed" and g["classification"] != "place": + errors.append("{} group {} has a proposed placement".format(g["classification"], gid)) + # tentative: an uncertain group, or an unreachable group placed only by an alignment hint + if s == "tentative" and g["classification"] != "uncertain" and not ( + g["classification"] == "separate" and g["reason"] == "unreachable" + and g["placement"].get("tentative_reason")): + errors.append("{} group {} ({}) has a tentative placement without an alignment hint".format( + g["classification"], gid, g["reason"])) + index = {g["id"]: g["placement"].get("placement_index") for g in placed} + order = summ["placement_order"] + summ["tentative_placement_order"] + if sorted(order) != sorted(index) or [index[gid] for gid in order] != list(range(len(order))) or \ + sorted(summ["placement_order"]) != sorted(gid for gid, s in status.items() if s == "proposed"): + errors.append("placement order in the summary differs from the placed groups") + for key, cls, s_ok in (("unplaced_place_groups", "place", "proposed"), + ("unplaced_uncertain_groups", "uncertain", "tentative")): + want = sorted(g["id"] for g in others if g["classification"] == cls and status[g["id"]] != s_ok) + if summ.get(key) != want: + errors.append("summary {} {} != {}".format(key, summ.get(key), want)) + if summ.get("failed_placements") != sorted(gid for gid, s in status.items() if s == "failed"): + errors.append("summary failed_placements differs from the failed placement records") + + # rows coverage near every placed or stairwell tile + used_cells = collections.defaultdict(set) + for g in placed: + for z, by in owners.items(): + rr = by.get(g["id"]) + if rr is not None: + used_cells[z].update((x // 24, y // 24) for x, y in rr.tiles()) + for sw in plan["stairwells"]: + for t in sw["tiles"][1:-1]: + used_cells[t["layer"]].add((t["x"] // 24, t["y"] // 24)) + prefix = ow_id + "_" + for z, cells in sorted(used_cells.items()): + have = {tuple(c) for c in ow["cells_by_layer"].get(str(z), [])} + for cx, cy in sorted(cells): + for dx in (-1, 0, 1): + for dy in (-1, 0, 1): + if (cx + dx, cy + dy) in have: + path = "{}{}_{}{}".format(prefix, cx + dx, cy + dy, "" if z == 0 else "_{}".format(z)) + if path not in ow_rows: # an included map carries every one of its final layers + errors.append("rows missing for {} near used layer {}".format(path, z)) + for mm in rl["maps_moved"]: + if mm["path"] not in ow_rows: + errors.append("rows missing for relayered map {}".format(mm["path"])) + stacked = set() + for z, cells in ow["cells_by_layer"].items(): + if int(z) != 0: + stacked.update(tuple(c) for c in cells) + for z, cells in ow["cells_by_layer"].items(): + for X, Y in cells: + path = "{}{}_{}{}".format(prefix, X, Y, "" if int(z) == 0 else "_{}".format(z)) + if (int(z) != 0 or (X, Y) in stacked) and path not in ow_rows: + errors.append("rows missing for {} (off the surface or stacked)".format(path)) + + # ---------------------------------------------------- final overworld layer + void_ends = {(v["map"], tuple(v["tile"])): v["layer"] for v in rl["exit_ends_on_void_tiles"]} + + def ow_layer(path, tx, ty): + X, Y, Z = world_cell(path) + k = (Z, 24 * X + tx, 24 * Y + ty) + if k in leg: + return max(leg[k][1]) + return void_ends.get((path, (tx, ty)), Z) + + # ------------------------------------------------------ related owners + def end_of(e, side): + g = e["source_group"] if side == "s" else e["target_group"] + m = e["source_map"] if side == "s" else e["target_map"] + t = e["source_tile"] if side == "s" else e["target_tile"] + if g == ow_id: + return m, ow_layer(m, *t) + return g, None + + rel = collections.defaultdict(set) # owner -> {(owner, layer or None)} + for g in placed: + for key in ("entrances_from_overworld", "exits_to_overworld", "entrances_from_other_groups", + "exits_to_other_groups"): + for e in g.get(key, []): + a, b = end_of(e, "s"), end_of(e, "t") + rel[a[0]].add(b) + rel[b[0]].add(a) for g in placed: got = rel[g["id"]] if sorted({o for o, _L in got}) != g.get("related_owners"): @@ -314,27 +896,225 @@ def hit(r): # ----------------------------------------------------------- exit index records = {} + copies = collections.Counter() # (group, list, exit key) -> copies for g in plan["groups"]: for key in ("entrances_from_overworld", "exits_to_overworld", "entrances_from_other_groups", "exits_to_other_groups", "internal_exits"): for e in g.get(key, []): k = (e["source_map"], e["line"]) + copies[(g["id"], key, k)] += 1 if k in records and records[k] != e: errors.append("exit {}:{} has two different records".format(*k)) records[k] = e - # ----------------------------------------------------------- stairwells - chain = collections.defaultdict(dict) # layer -> owner -> Rows - sw_by_id = {} - for sw in plan["stairwells"]: - if sw["id"] in sw_by_id: - errors.append("stairwell id {} is used twice".format(sw["id"])) - sw_by_id[sw["id"]] = sw - for x in sw["tiles"][1:-1]: - chain[x["layer"]].setdefault(sw["owner"], Rows()).add(x["x"], x["y"]) - for layer in chain.values(): + def same_as(what, copy, ref, fields, ref_name="exit record"): + """Every listed field of a copy of an exit equals the field of its reference.""" + bad = [f for f in fields if copy.get(f) != ref.get(f)] + if bad: + errors.append("{} of exit {}:{} differs from its {} in {}".format( + what, ref["source_map"], ref["line"], ref_name, ", ".join(bad))) + + def owner_of_end(e, side): + """The owner of one end of an exit: the overworld map itself, else the group.""" + return e[side + "_map"] if e[side + "_group"] == ow_id else e[side + "_group"] + + # The exit inventory is the expected set: every exit object read from the + # content. Each one with a destination has a record in every group it touches + # (the overworld's own exits in its internal_exits, none for an exit inside + # one other group), and nothing else has a record. + group_of = {} + for g in others: + for m in g["maps"]: + if group_of.setdefault(m["path"], g["id"]) != g["id"]: + errors.append("map {} is a member of two groups".format(m["path"])) + for z, cells in ow["cells_by_layer"].items(): + for X, Y in cells: + group_of.setdefault("{}{}_{}{}".format(prefix, X, Y, "" if int(z) == 0 else "_{}".format(z)), ow_id) + inv = {} + for x in plan["exit_inventory"]: + k = (x["source_map"], x["line"]) + if k in inv: + errors.append("exit {}:{} is listed twice in the exit inventory".format(*k)) + inv[k] = x + # value types: map paths and archetypes are text, lines and tiles integers, flags true when present + def tile_ok(t_): + return isinstance(t_, list) and len(t_) == 2 and all(type(c) is int for c in t_) + + if not (isinstance(x["source_map"], str) and type(x["line"]) is int and tile_ok(x.get("source_tile")) + and isinstance(x.get("arch"), str) and x["arch"] + and ("target_map" not in x or isinstance(x["target_map"], str) and tile_ok(x.get("target_tile"))) + and all(x.get(f, True) is True for f in ("target_is_map_default_entry", "per_player_last_eat_1", + "explicit_last_heal_0")) + and ("event_scripts" not in x or isinstance(x["event_scripts"], list) and x["event_scripts"] + and all(isinstance(v, str) for v in x["event_scripts"])) + and all(f not in x or isinstance(x[f], str) and x[f] for f in ("vertical_inferred_from", "problem", + "dynamic_target")) + # the facts behind data problems are there exactly for their form or problem + and ("dynamic_target" in x) == (x.get("form") == "dynamic") + and ("problem_target" in x) == ("problem" in x) + and ("problem_target" not in x or isinstance(x["problem_target"], list) + and len(x["problem_target"]) == 3)): + errors.append("exit {!r}:{!r} has a value of the wrong type".format(*k)) + # the tile-link slot of a tiled exit without a link: 1..10, and only on that form; wired ones go up or down + if ("tiled_slot" in x) != (x.get("form") == "tiled-unresolved") or "tiled_slot" in x and \ + x["tiled_slot"] not in SLOT_NAMES: + errors.append("exit {}:{} has form {} and tile-link slot {}".format(k[0], k[1], x.get("form"), + x.get("tiled_slot"))) + # a known form and direction; a target exactly when the form resolves one and the map exists + if x.get("form") not in TARGET_FORMS | NO_TARGET_FORMS or x.get("vertical") not in ("up", "down", "none") or \ + ("target_map" in x) != ("target_tile" in x) or \ + "target_map" in x and x["form"] not in TARGET_FORMS or \ + "target_map" not in x and x["form"] in TARGET_FORMS and not str(x.get("problem", "")).startswith( + "missing-target-map"): + errors.append("exit {}:{} has form {}, direction {} and {} target".format( + k[0], k[1], x.get("form"), x.get("vertical"), "a" if "target_map" in x else "no")) + if len(plan["exit_inventory"]) != summ["exit_objects"]: + errors.append("the exit inventory lists {} exit objects, the summary {}".format( + len(plan["exit_inventory"]), summ["exit_objects"])) + wired_dp = {} + for d in plan["data_problems"]["dangling_exits_wired"]: + k = (d["source_map"], d["line"]) + if k in wired_dp: + errors.append("wired exit {}:{} is listed twice".format(*k)) + wired_dp[k] = d + if len(plan["data_problems"]["dangling_exits_wired"]) != summ["wired_exits"]: + errors.append("{} wired exits listed, the summary says {}".format( + len(plan["data_problems"]["dangling_exits_wired"]), summ["wired_exits"])) + size = {m["path"]: (m["width"], m["height"]) for g in others for m in g["maps"]} + expected_copies = set() + want_rec = {} # exit key -> the record fields the inventory (and wiring) fix + for k, x in sorted(inv.items()): + want = {f: x.get(f) for f in SOURCE_FIELDS} + if k in wired_dp: + if x["form"] != "tiled-unresolved" or "target_map" in x or x.get("tiled_slot") not in (9, 10): + errors.append("wired exit {}:{} is not a legacy exit that leads nowhere".format(*k)) + want.update(form="wired", target_map=wired_dp[k].get("target_map"), + target_tile=wired_dp[k].get("target_tile")) + sg = group_of.get(x["source_map"]) + if sg is None: + errors.append("exit {}:{} is on a map that is in no group".format(*k)) + continue + if want["target_map"] is None: + continue + tg = group_of.get(want["target_map"]) + if tg is None: + errors.append("exit {}:{} leads to {}, which is in no group".format(k[0], k[1], want["target_map"])) + continue + # a target (resolved by the reader, or wired) is a tile of its map + tw, th = size.get(want["target_map"], (OW_SIZE,) * 2) + tt = want.get("target_tile") + if not (isinstance(tt, list) and len(tt) == 2 and all(type(c) is int for c in tt) + and 0 <= tt[0] < tw and 0 <= tt[1] < th): + errors.append("exit {}:{} targets tile {} outside {} ({} x {})".format( + k[0], k[1], tt, want["target_map"], tw, th)) + want.update(source_group=sg, target_group=tg) + want_rec[k] = want + if sg == tg: + if sg == ow_id: + expected_copies.add((ow_id, "internal_exits", k)) + continue + if sg != ow_id: + expected_copies.add((sg, "exits_to_overworld" if tg == ow_id else "exits_to_other_groups", k)) + if tg != ow_id: + expected_copies.add((tg, "entrances_from_overworld" if sg == ow_id else "entrances_from_other_groups", k)) + missing = sorted(expected_copies - set(copies)) + extra = sorted(c for c, n in copies.items() if c not in expected_copies or n > 1) + if missing: + errors.append("{} exit record(s) missing, e.g. exit {}:{} from {} of {}".format( + len(missing), missing[0][2][0], missing[0][2][1], missing[0][1], missing[0][0])) + if extra: + errors.append("{} exit record(s) the exit inventory does not call for, e.g. exit {}:{} in {} of {}".format( + len(extra), extra[0][2][0], extra[0][2][1], extra[0][1], extra[0][0])) + for k, e in sorted(records.items()): + unknown = sorted(set(e) - RECORD_FIELDS) + if unknown: + errors.append("exit {}:{} has fields the verifier does not know: {}".format(k[0], k[1], ", ".join(unknown))) + if k in want_rec: + same_as("record", e, want_rec[k], SOURCE_FIELDS + ("source_group", "target_group"), "inventory entry") + if (e.get("form") == "wired") != ("wired" in e): + errors.append("exit {}:{} has form {} but {} wiring evidence".format( + k[0], k[1], e.get("form"), "carries" if "wired" in e else "no")) + for k, d in sorted(wired_dp.items()): + e = records.get(k) + if e is None: + errors.append("wired exit {}:{} has no exit record".format(*k)) + continue + same_as("wired exit", d, e, ("source_tile", "arch", "target_map", "target_tile")) + if d.get("verdict") != (e.get("wired") or {}).get("verdict"): + errors.append("wired exit {}:{} verdict differs from its record".format(*k)) + # data problems and kept decoration describe exits without a record: compare with the inventory + for what, lst in (("data problem", plan["data_problems"]["exits"]), + ("kept decoration", plan.get("exits_kept_as_decoration", [])), + ("kept decoration problem", plan["data_problems"].get("dangling_exits_kept_as_decoration", []))): + for d in lst: + x = inv.get((d["source_map"], d["line"])) + if x is None: + errors.append("{} names exit {}:{}, which is not in the exit inventory".format( + what, d["source_map"], d["line"])) + continue + same_as(what, d, x, ("source_tile", "arch"), "inventory entry") + if what != "data problem" and x["form"] != "tiled-unresolved": + errors.append("{} exit {}:{} is not a legacy exit that leads nowhere".format( + what, d["source_map"], d["line"])) + # the exit data problems, re-derived entry by entry from the inventory + working = {(x["source_map"], tuple(x["source_tile"])) for x in inv.values() if "target_map" in x} + want_dp = [] + for k, x in sorted(inv.items()): + form, tile = x["form"], tuple(x["source_tile"]) + base = {f: x[f] for f in ("source_map", "source_tile", "arch", "line")} + if form in ("tiled-unresolved", "unresolved") and not x.get("event_scripts"): + if (x["source_map"], tile) in working: + base.update(severity="info", note="another exit object on the same tile has a destination " + "(decorative duplicate)") + elif x.get("explicit_last_heal_0"): + base.update(severity="info", note="explicitly disabled with last_heal 0 (decorative)") + else: + base["severity"] = "problem" + w, h = size.get(x["source_map"], (OW_SIZE,) * 2) + if not (0 <= tile[0] < w and 0 <= tile[1] < h): + want_dp.append(dict(base, kind="exit-outside-map-bounds")) + if form == "tiled-unresolved": + want_dp.append(dict(base, kind="tiled-exit-without-link", detail="last_heal {} ({}) but the map has no " + "such tile link; the exit leads nowhere".format(x.get("tiled_slot"), + SLOT_NAMES.get(x.get("tiled_slot"))))) + elif form == "unresolved": + if not x.get("event_scripts"): + want_dp.append(dict(base, kind="exit-without-destination", + detail="no slaying, no tiled slot, hp/sp -1 and no event script")) + elif x.get("problem"): + want_dp.append(dict(base, kind=x["problem"], target=x.get("problem_target"))) + elif form == "dynamic": + want_dp.append(dict(base, kind="dynamic-random-map-exit", detail=x.get("dynamic_target"), severity="info")) + want_dp = collections.Counter(json.dumps(d, sort_keys=True) for d in want_dp) + got_dp = collections.Counter(json.dumps(d, sort_keys=True) for d in plan["data_problems"]["exits"]) + if got_dp != want_dp: + diff = sorted((want_dp - got_dp) + (got_dp - want_dp)) + errors.append("{} exit data problem(s) differ from the exit inventory, e.g. {}".format(len(diff), diff[0])) + + # ----------------------------------------------------------- stairwells + chain = collections.defaultdict(dict) # layer -> owner -> Rows + sw_by_id = {} + for sw in plan["stairwells"]: + if sw["id"] in sw_by_id: + errors.append("stairwell id {} is used twice".format(sw["id"])) + sw_by_id[sw["id"]] = sw + for x in sw["tiles"][1:-1]: + chain[x["layer"]].setdefault(sw["owner"], Rows()).add(x["x"], x["y"]) + for layer in chain.values(): for rr in layer.values(): rr.done() + n_kind = collections.Counter() + for sw in plan["stairwells"]: + n_kind[sw["kind"]] += 1 + if sw["id"] != "{}{}".format(SW_PREFIX.get(sw["kind"], "?"), n_kind[sw["kind"]]): + errors.append("stairwell {} of kind {} is out of its series".format(sw["id"], sw["kind"])) + if summ.get("stairwells") != len(plan["stairwells"]) or summ.get("stairwells_by_kind") != dict(sorted( + n_kind.items())) or summ.get("stairwell_spans_layers") != {str(k): v for k, v in sorted(collections.Counter( + sw["top_layer"] - sw["bottom_layer"] for sw in plan["stairwells"]).items())} or \ + summ.get("stairwell_sites_blocked") != len(plan["stairwell_sites_blocked"]) or \ + summ.get("sloped_stairwell_sites_blocked") != len(plan["sloped_stairwell_sites_blocked"]) or \ + summ.get("relayering_stairwell_sites_blocked") != len(rl["stairwells_blocked"]): + errors.append("summary stairwell and blocked-site counts differ from the lists") sw_tiles = {} intermediate = 0 kinds = collections.Counter() @@ -358,13 +1138,57 @@ def hit(r): bot = (t[-1]["x"], t[-1]["y"], t[-1]["layer"]) span = top[2] - bot[2] off = cheb(top, bot) - if sw["shape"] == "column" and off > tol or sw["shape"] == "sloped" and not tol < off <= span * tol: + if sw["shape"] != ("column" if off <= tol else "sloped") or off > span * tol: errors.append("{} shape {} does not fit ends {} tiles apart over {} layers".format( sw["id"], sw["shape"], off, span)) if sw["horizontal_offset"] != off: errors.append("{} horizontal_offset {} != {}".format(sw["id"], sw["horizontal_offset"], off)) - if sw["kind"] == "placement" and sw["shape"] != "column" or sw["kind"] == "sloped" and sw["shape"] != "sloped": + if sw["kind"] not in SW_PREFIX or sw["kind"] == "placement" and sw["shape"] != "column" or \ + sw["kind"] == "sloped" and sw["shape"] != "sloped": errors.append("{} kind {} does not match shape {}".format(sw["id"], sw["kind"], sw["shape"])) + if sw.get("max_bend") != max([cheb((x_["x"], x_["y"]), top) for x_ in t[1:-1]] or [0]): + errors.append("{} max_bend {} does not match its tiles".format(sw["id"], sw.get("max_bend"))) + if sw["shape"] == "column" and sw.get("max_bend", 0) > bend: + errors.append("{} bends {} tile(s) from its column, more than stairwell_max_bend_tiles {}".format( + sw["id"], sw.get("max_bend"), bend)) + if not sw["replaces"]: + errors.append("{} replaces no exit".format(sw["id"])) + # owners, re-derived from the exits it replaces: a placed group's stairwell joins that group + # (placed after the other end) and the other owner; a relayering one two overworld maps + pairs_ = set() + for r in sw["replaces"]: + e = records.get((r["source_map"], r["line"])) + if e is not None and "plane_source" in e and "plane_target" in e: + if sw["kind"] == "relayering": + up_src = e["plane_source"][2] > e["plane_target"][2] + pairs_.add((e["source_map"], e["target_map"]) if up_src else (e["target_map"], e["source_map"])) + else: + pairs_.add(tuple(sorted([owner_of_end(e, "source"), owner_of_end(e, "target")]))) + if sw["kind"] == "relayering": + want_o = None if len(pairs_) != 1 else { + "group": None, "owner": "stairwell@{},{},{}".format(*top), "other": next(iter(pairs_))[1], + "connects": sorted(set(next(iter(pairs_))))} + else: + g_, o_ = sw.get("group"), sw.get("other") + want_o = None if len(pairs_) != 1 or tuple(sorted([g_, o_])) not in pairs_ or g_ not in placed_ids or \ + o_ in placed_ids and not (index.get(o_, len(index)) < index.get(g_, -1)) else { + "group": g_, "owner": g_, "other": o_, "connects": sorted({g_, o_})} + if want_o is None or any(sw.get(f) != v for f, v in want_o.items()): + errors.append("{} owners (group {}, owner {}, other {}, connects {}) do not follow from the exits it " + "replaces".format(sw["id"], sw.get("group"), sw.get("owner"), sw.get("other"), + sw.get("connects"))) + # intermediate tiles show the down archetype of the exits' stair family; they are not exits + ends_arch = sorted({r["arch"] for r in sw["replaces"] if r["goes"] == "down"}) + \ + sorted({r["arch"] for r in sw["replaces"] if r["goes"] == "up"}) + fam = next((down for a in ends_arch for prefix, down in STAIR_FAMILIES if a.casefold().startswith(prefix)), + None) + app_src = ("appearance of the down archetype of the {} family used by this stairwell's exits; the " + "intermediate objects are decorations, not exits".format(fam.rsplit("_", 1)[0]) if fam else + "no stair archetype among {}; owner to choose".format(ends_arch)) + want_objs_mid = [{"archetype": fam, "exit": False, "walkable": False, "use": "appearance only"}] if fam else [] + if sw.get("intermediate_appearance") != fam or sw.get("intermediate_appearance_source") != app_src or \ + any(x_.get("objects") != want_objs_mid or x_.get("existing") is not False for x_ in t[1:-1]): + errors.append("{} intermediate tiles do not show {} as a decoration".format(sw["id"], fam)) for r in sw["replaces"]: e = records.get((r["source_map"], r["line"])) if e is None: @@ -372,7 +1196,15 @@ def hit(r): continue if e.get("becomes") != "stairwell" or e.get("stairwell") != sw["id"]: errors.append("{} exit {}:{} is not marked as this stairwell".format(sw["id"], r["source_map"], r["line"])) - s, d = e["plane_source"], e["plane_target"] + # the copy in replaces is what the migration reads: it must be the record + same_as("{} replaces entry".format(sw["id"]), r, e, + ("source_tile", "target_map", "target_tile", "arch", "plane_source", "plane_target")) + s, d = e.get("plane_source"), e.get("plane_target") + if s is None or d is None: + continue # reported with the exit records + if r.get("goes") != ("down" if s[2] > d[2] else "up"): + errors.append("{} exit {}:{} goes {}, but its record goes {}".format( + sw["id"], r["source_map"], r["line"], r.get("goes"), "down" if s[2] > d[2] else "up")) up, low = (s, d) if s[2] > d[2] else (d, s) if up[2] != top[2] or low[2] != bot[2] or cheb(up, top) > tol or cheb(low, bot) > tol: errors.append("{} ends do not match exit {}:{}".format(sw["id"], r["source_map"], r["line"])) @@ -418,7 +1250,7 @@ def hit(r): and rr.any_within(x["x"], x["y"], M)}) if touch != touched.get(key, []): errors.append("{} tile {} contacts {} != recorded {}".format(sw["id"], key, touch, touched.get(key, []))) - if (sw["mode"] == "clear") != (not sw["view_conflicts"] and not sw["contacts"]): + if sw.get("mode") != ("clear" if not sw["view_conflicts"] and not sw["contacts"] else "void-only"): errors.append("{} mode {} disagrees with its view conflicts and contacts".format(sw["id"], sw["mode"])) # ---------------------------------------------------------------- exits @@ -426,20 +1258,129 @@ def hit(r): for sw in plan["stairwells"]: for r in sw["replaces"]: replaced_by[(r["source_map"], r["line"])].append(sw["id"]) + def needs_stairwell(e): + """Ends 2+ layers and at most one tile per layer apart, unless a legacy overworld shaft kept as it was.""" + s_, d_ = e["plane_source"], e["plane_target"] + dl, dist = abs(d_[2] - s_[2]), cheb(s_, d_) + if dl < 2 or dist > dl * tol: + return False + if e["source_group"] == ow_id and e["target_group"] == ow_id: + return abs(world_cell(e["target_map"])[2] - world_cell(e["source_map"])[2]) != dl + return True + + blocked_keys = {} + for key_, sites in (("column", plan["stairwell_sites_blocked"]), ("sloped", plan["sloped_stairwell_sites_blocked"]), + ("relayering", rl["stairwells_blocked"])): + for b in sites: + shape_ = "sloped" if cheb(b["top"], b["bottom"]) > tol else "column" + span_ = b["top"][2] - b["bottom"][2] + if span_ < 2 or cheb(b["top"], b["bottom"]) > span_ * tol: + errors.append("blocked {} stairwell site {} -> {} is no stairwell site: its ends are {} tiles apart " + "over {} layers".format(key_, b["top"], b["bottom"], cheb(b["top"], b["bottom"]), span_)) + if not b["exits"] or b.get("shape") != shape_ or key_ != "relayering" and shape_ != key_: + errors.append("blocked {} stairwell site at {} lists {} exits with shape {} ({} by its ends)".format( + key_, b["top"], len(b["exits"]), b.get("shape"), shape_)) + if key_ != "relayering" and (b.get("group") not in placed_ids or b.get("other") in placed_ids and not ( + index.get(b["other"], len(index)) < index.get(b["group"], -1))): + errors.append("blocked {} stairwell site at {} belongs to {}, which is not the later placed " + "end".format(key_, b["top"], b.get("group"))) + for x in b["exits"]: + k = (x["source_map"], x["line"]) + if k in blocked_keys: + errors.append("exit {}:{} is listed at two blocked stairwell sites".format(*k)) + r = records.get(k) + if r is None: + errors.append("blocked {} stairwell site lists exit {}:{}, which has no exit record".format(key_, *k)) + else: + same_as("blocked {} site entry".format(key_), x, r, ("source_tile", "arch")) + if "plane_source" not in r or "plane_target" not in r or not needs_stairwell(r): + errors.append("blocked {} stairwell site lists exit {}:{}, which needs no stairwell".format( + key_, *k)) + elif key_ == "relayering" and not r["source_group"] == r["target_group"] == ow_id: + errors.append("blocked relayering site lists exit {}:{}, which is not an overworld " + "exit".format(*k)) + elif key_ == "relayering": + up_src = r["plane_source"][2] > r["plane_target"][2] + ends_ = [r["source_map"], r["target_map"]] if up_src else [r["target_map"], r["source_map"]] + if ends_ != [b.get("top_map"), b.get("bottom_map")]: + errors.append("blocked relayering site {} -> {} lists exit {}:{} between {}".format( + b.get("top_map"), b.get("bottom_map"), k[0], k[1], ends_)) + elif sorted([owner_of_end(r, "source"), owner_of_end(r, "target")]) != \ + sorted([b.get("group"), b.get("other")]): + errors.append("blocked {} site of {} / {} lists exit {}:{} of other owners".format( + key_, b.get("group"), b.get("other"), *k)) + blocked_keys[k] = (key_, b) + + def portal_reason(e, k, s, dist, dl): + """The generator's reason an exit stays a portal, its text, and whether a sloped stairwell would reach.""" + if k in blocked_keys and blocked_keys[k][0] == "sloped": + return "slope-blocked", "{} tiles and {} layers apart, but no void sloped chain exists".format( + dist, dl), True + if k in blocked_keys: + return "stairwell-blocked", "aligned over {} layers, but no void stair chain exists".format(dl), None + if dl == 0: + return "same-layer", "both ends on layer {}, {} tiles apart".format(s[2], dist), None + ends_ = [x for x in (e["source_group"], e["target_group"]) if x != ow_id] + if not ends_: + return "overworld", "overworld exit whose ends are {} tiles and {} layer(s) apart".format(dist, dl), None + later = max(ends_, key=lambda x: index.get(x, -1)) + pl = placed_by_id[later]["placement"] + mc = pl.get("main_connection") or {} + slope = True if 2 <= dist <= dl * tol else None + if pl.get("shift_from_ideal") != [0, 0]: + return "group-shifted", "{} was shifted {} from its ideal spot".format(later, pl.get("shift_from_ideal")), \ + slope + if mc.get("source_map") == e["source_map"] and mc.get("source_tile") == e["source_tile"]: + return "main-entrance-offset", "the main connection itself lands {} tiles away".format(dist), slope + return "other-entrance-aligned", "{} is aligned to its main connection; this exit's ends are {} tiles and {} " \ + "layer(s) apart".format(later, dist, dl), slope + + placed_by_id = {g["id"]: g for g in placed} + aligns = {} + for a in st.get("tentative_alignments", []): + if a.get("group") in aligns: + errors.append("tentative alignment of {} is configured twice".format(a.get("group"))) + aligns[a.get("group")] = a + # a configured alignment names real groups and was applied: its group's tentative placement carries + # it (checked against the anchor below), so a stale or made-up entry fails + g_ = placed_by_id.get(a.get("group")) + if g_ is None or a.get("anchor") not in placed_by_id or not isinstance(a.get("layer_delta"), int) or \ + not isinstance(a.get("why"), str) or not a["why"] or g_["placement"].get("status") != "tentative" or \ + g_["placement"].get("tentative_reason") != a["why"]: + errors.append("tentative alignment of {} to {} is not the hint of a tentative placement".format( + a.get("group"), a.get("anchor"))) exits_checked = 0 for (src_map, line), e in sorted(records.items()): + live = all(x == ow_id or x in placed_ids for x in (e["source_group"], e["target_group"])) + have = [f for f in PLACED_FIELDS + LABEL_FIELDS if f in e] + lacks = [f for f in PLACED_FIELDS if f not in e] + if live and lacks or not live and have: + errors.append("exit {}:{} {} placement fields {}, although {} of its ends are placed".format( + src_map, line, "lacks" if live else "has", lacks if live else have, "both" if live else "not both")) + continue ends = {} - for end, mkey, tkey, gkey in (("plane_source", "source_map", "source_tile", "source_group"), - ("plane_target", "target_map", "target_tile", "target_group")): - if end not in e: - continue + for end, ow_end, mkey, tkey, gkey in ( + ("plane_source", "overworld_plane_source", "source_map", "source_tile", "source_group"), + ("plane_target", "overworld_plane_target", "target_map", "target_tile", "target_group")): mp, (tx, ty) = e[mkey], e[tkey] + if (e[gkey] == ow_id) != (ow_end in e): + errors.append("exit {}:{} {} {}".format(src_map, line, "lacks" if ow_end not in e else "has", ow_end)) if e[gkey] == ow_id: + if world_cell(mp) is None: + errors.append("exit {}:{} has an overworld end on {}, which is not an overworld map".format( + src_map, line, mp)) + continue X, Y, _Z = world_cell(mp) exp = [24 * X + tx, 24 * Y + ty, ow_layer(mp, tx, ty)] - else: + if ow_end in e and e[ow_end] != exp: + errors.append("exit {}:{} {} {} != {}".format(src_map, line, ow_end, e[ow_end], exp)) + elif mp in origin and e[gkey] in placed_ids: o = origin[mp] exp = [o[0] + tx, o[1] + ty, o[2]] + else: + continue + if not live: + continue exits_checked += 1 ends[end] = exp if exp != e[end]: @@ -447,7 +1388,18 @@ def hit(r): if len(ends) == 2: s, d = ends["plane_source"], ends["plane_target"] dist, dl = cheb(s, d), abs(d[2] - s[2]) + if e["xy_distance"] != dist or e["layer_delta"] != d[2] - s[2]: + errors.append("exit {}:{} records {} tiles / {} layers, its ends are {} / {} apart".format( + src_map, line, e["xy_distance"], e["layer_delta"], dist, d[2] - s[2])) b = e.get("becomes") + want_reason, want_text, want_slope = portal_reason(e, (src_map, line), s, dist, dl) if b == "portal" \ + else (None, None, None) + if ("stairwell" in e) != (b == "stairwell") or e.get("portal_reason") != want_reason or \ + e.get("portal_reason_text") != want_text or e.get("slope_reachable") != want_slope: + errors.append("exit {}:{} labelled {} records stairwell {}, portal reason {} (slope reachable {}); " + "expected portal reason {} (slope reachable {})".format( + src_map, line, b, e.get("stairwell"), e.get("portal_reason"), + e.get("slope_reachable"), want_reason, want_slope)) sw = sw_by_id.get(e.get("stairwell")) ok = {"stairs": dl == 1 and dist <= tol, "stairwell": sw is not None and dl >= 2 and dist <= (tol if sw["shape"] == "column" else dl * tol), @@ -494,30 +1446,139 @@ def hit(r): errors.append("exit {}:{} is kept as decoration but has an exit record".format(d["source_map"], d["line"])) if not d.get("decision") or "no exit" not in d.get("migrate_as", ""): errors.append("exit {}:{} is kept as decoration without its decision".format(d["source_map"], d["line"])) + # where the kept object ends up: its tile on the plane, like an exit's source end + mp, (tx, ty) = d["source_map"], d["source_tile"] + if group_of.get(mp) == ow_id: + X, Y, _Z = world_cell(mp) + exp = [24 * X + tx, 24 * Y + ty, ow_layer(mp, tx, ty)] + else: + o = origin.get(mp) + exp = None if o is None else [o[0] + tx, o[1] + ty, o[2]] + if d.get("plane_source") != exp: + errors.append("exit {}:{} kept as decoration is at {}, its tile is at {}".format( + d["source_map"], d["line"], d.get("plane_source"), exp)) + + # crossing counts, re-derived from the records (one per exit object) + def counted(es): + return dict(sorted(collections.Counter(str(e.get("becomes")) for e in es).items())) + + crossing = [e for e in records.values() if e["source_group"] != e["target_group"]] + live_crossing = [e for e in crossing if "plane_source" in e and "plane_target" in e] + if counted(live_crossing) != summ["crossing_exit_objects_after_placement"]: + errors.append("summary crossing exits {} differ from the exit records {}".format( + summ["crossing_exit_objects_after_placement"], counted(live_crossing))) + if len(crossing) - len(live_crossing) != summ["crossing_exit_objects_into_separate_planes"]: + errors.append("summary crossing exits into separate planes {} differ from the exit records {}".format( + summ["crossing_exit_objects_into_separate_planes"], len(crossing) - len(live_crossing))) + reasons = collections.Counter(str(e.get("portal_reason")) for e in live_crossing if e.get("becomes") == "portal") + if dict(sorted(reasons.items())) != summ["portal_reasons"]: + errors.append("summary portal reasons differ from the exit records") + for g in placed: + mine = counted(e for e in live_crossing if g["id"] in (e["source_group"], e["target_group"])) + if mine != g["placement"].get("crossing_exits_after_placement"): + errors.append("crossing exits of {} {} differ from its exit records {}".format( + g["id"], g["placement"].get("crossing_exits_after_placement"), mine)) + # the offsets are the ideal spot plus the recorded shift and extra layers (deeper when the group + # is oriented below, higher when above) + pl = g["placement"] + ideal, orient = pl.get("ideal") or {}, pl.get("orientation") + step = {"below": -1, "above": 1}.get(orient) + sh, extra = pl.get("shift_from_ideal") or [None, None], pl.get("extra_layers_from_ideal") + if not (type(extra) is int and isinstance(sh, list) and len(sh) == 2 and all(type(c) is int for c in sh) + and all(type(pl.get(f)) is int for f in ("x_offset", "y_offset", "layer_offset")) + and all(type(ideal.get(f)) is int for f in ("dx", "dy", "L"))): + errors.append("placement of {} has offsets, ideal, shift or extra layers that are not integers".format( + g["id"])) + continue + if extra > st["max_extra_layers"] or max(abs(sh[0]), abs(sh[1])) > st["max_shift_tiles"]: + errors.append("placement of {} goes {} layer(s) and {} beyond its ideal spot, past the search limits " + "(max_extra_layers {}, max_shift_tiles {})".format(g["id"], extra, sh, + st["max_extra_layers"], + st["max_shift_tiles"])) + vb = pl.get("vertical_bounds_from_exit_semantics") or [None, None] + if vb[0] is not None and pl["layer_offset"] < vb[0] or vb[1] is not None and pl["layer_offset"] > vb[1]: + errors.append("layer offset {} of {} is outside its vertical bounds {}".format( + pl["layer_offset"], g["id"], vb)) + if step is None or not isinstance(extra, int) or extra < 0 or None in (ideal.get("dx"), ideal.get("dy"), + ideal.get("L"), *sh) or \ + [pl["x_offset"], pl["y_offset"], pl["layer_offset"]] != [ + ideal["dx"] + sh[0], ideal["dy"] + sh[1], ideal["L"] + step * extra]: + errors.append("offsets of {} are not its ideal spot {} shifted by {} and {} layer(s) {}".format( + g["id"], ideal, sh, extra, orient)) + continue + # the main connection is one of the group's exit records, to something placed before it; + # only a placement forced by an alignment hint has none + mc = pl.get("main_connection") + hinted = bool(pl.get("tentative_reason")) + if "tentative_reason" in pl and not (isinstance(pl["tentative_reason"], str) and pl["tentative_reason"]): + errors.append("tentative_reason of {} is not a text".format(g["id"])) + if hinted and pl.get("status") != "tentative": + errors.append("{} placement of {} carries an alignment hint".format(pl.get("status"), g["id"])) + hinted = False + if hinted: + # a configured alignment hint: the group's ideal spot is its anchor's, the given layers away + a = aligns.get(g["id"]) + anchor = placed_by_id.get(a["anchor"]) if a else None + if anchor is None or pl["tentative_reason"] != a["why"] or ideal != { + "dx": anchor["placement"]["x_offset"], "dy": anchor["placement"]["y_offset"], + "L": anchor["placement"]["layer_offset"] + a["layer_delta"]} or \ + not index.get(a["anchor"], len(index)) < index.get(g["id"], -1): + errors.append("alignment hint of {} is not a configured alignment to a placed anchor".format(g["id"])) + if mc is None or hinted: + if (mc is None) != hinted: + errors.append("placement of {} has {}".format( + g["id"], "no main connection" if mc is None else "both a main connection and an alignment hint")) + continue + if mc.get("kind") not in ("in", "out"): + errors.append("main connection of {} has kind {}".format(g["id"], mc.get("kind"))) + continue + keys = ("entrances_from_overworld", "entrances_from_other_groups") if mc["kind"] == "in" else \ + ("exits_to_overworld", "exits_to_other_groups") + match = [e for key in keys for e in g.get(key, []) + if [e["source_map"], e["source_tile"], e["target_map"], e["target_tile"], e["arch"], e["vertical"]] + == [mc.get("source_map"), mc.get("source_tile"), mc.get("target_map"), mc.get("target_tile"), + mc.get("arch"), mc.get("vdir")]] + if not match: + errors.append("main connection of {} is not one of its exit records".format(g["id"])) + continue + other = match[0]["source_group"] if mc["kind"] == "in" else match[0]["target_group"] + oi, gi = index.get(other), index.get(g["id"]) + if other != ow_id and not (isinstance(oi, int) and isinstance(gi, int) and oi < gi): + errors.append("main connection of {} leads to {}, which is not placed before it".format(g["id"], other)) + continue + # the ideal spot puts the connection's end in this group under (or over) its other end: one + # layer away in the exit's direction (or the orientation's), within the vertical bounds + e = match[0] + if "plane_source" not in e or "plane_target" not in e: + continue # reported with the exit records + far, (near_map, near_tile) = (e["plane_source"], (e["target_map"], e["target_tile"])) if mc["kind"] == "in" \ + else (e["plane_target"], (e["source_map"], e["source_tile"])) + lo_ = next((m["local_offset"] for m in g["maps"] if m["path"] == near_map), None) + if lo_ is None: + continue # reported with the exit records + lx, ly, lz = lo_[0] + near_tile[0], lo_[1] + near_tile[1], lo_[2] + d_ = mc["vdir"] if mc["vdir"] != "none" else ( + ("down" if orient == "below" else "up") if mc["kind"] == "in" else ("up" if orient == "below" else "down")) + below = d_ == ("down" if mc["kind"] == "in" else "up") + L_ = far[2] - 1 - lz if below else far[2] + 1 - lz + lo_b, hi_b = (pl.get("vertical_bounds_from_exit_semantics") or [None, None]) + L_ = max(L_, lo_b) if lo_b is not None else L_ + L_ = min(L_, hi_b) if hi_b is not None else L_ + if ideal != {"dx": far[0] - lx, "dy": far[1] - ly, "L": L_}: + errors.append("ideal spot {} of {} does not follow from its main connection ({})".format( + ideal, g["id"], {"dx": far[0] - lx, "dy": far[1] - ly, "L": L_})) # ------------------------------------------------- required stairwells # Every exit whose ends are 2+ layers apart and at most one tile apart per # layer must be a stairwell, unless a blocked site lists it: crossing exits # (placed groups) always, overworld exits only when the relayering changed # their layer distance. Blocked sites must really have no void chain. - blocked_keys = {} - for key_, sites in (("column", plan["stairwell_sites_blocked"]), ("sloped", plan["sloped_stairwell_sites_blocked"]), - ("relayering", rl["stairwells_blocked"])): - for b in sites: - for x in b["exits"]: - blocked_keys[(x["source_map"], x["line"])] = (key_, b) required = 0 for (src_map, line), e in sorted(records.items()): - if "plane_source" not in e or "plane_target" not in e: + if "plane_source" not in e or "plane_target" not in e or not needs_stairwell(e): continue s_, d_ = e["plane_source"], e["plane_target"] dl, dist = abs(d_[2] - s_[2]), cheb(s_, d_) - if dl < 2 or dist > dl * tol: - continue - if e["source_group"] == ow_id and e["target_group"] == ow_id: - legacy = abs(world_cell(e["target_map"])[2] - world_cell(e["source_map"])[2]) - if legacy == dl: - continue # a legacy overworld shaft the relayering leaves alone required += 1 if (src_map, line) in blocked_keys: if e.get("becomes") != "portal": @@ -546,7 +1607,7 @@ def chain_exists(top, bottom, shape): for dy in (-1, 0, 1): p_ = (x + dx, y + dy) if shape == "column": - ok = cheb(p_, top) <= 2 # STAIRWELL_MAX_BEND + ok = cheb(p_, top) <= bend else: ok = cheb(p_, top) <= i and cheb(p_, bottom) <= n - i if ok and void_at(z, *p_): @@ -560,302 +1621,11 @@ def chain_exists(top, bottom, shape): errors.append("blocked {} stairwell site at {} has a void chain after all".format(shape, b["top"])) # ----------------------------------------------------------- relayering - heights, blocked, ground = {}, set(), {} - for path, rows in sorted(rl["tile_heights"].items()): - X, Y, Z = world_cell(path) - for y, row in enumerate(rows): - for x, tok in enumerate(row.split(" ")): - if tok == ".": - continue - k = (Z, 24 * X + x, 24 * Y + y) - if tok.endswith("b"): - blocked.add(k) - tok = tok[:-1] - if "/" in tok: - tok, g = tok.split("/") - ground[k] = int(g) - heights[k] = int(tok) - split = {} - for sp in rl["split_tiles"]: - X, Y, Z = world_cell(sp["map"]) - split[(Z, 24 * X + sp["tile"][0], 24 * Y + sp["tile"][1])] = sp - # a split tile's raised part is its upper layer; which tiles split is derived below - main = {k: max(v[1]) for k, v in leg.items()} - # regions - q = {k: h // HZ for k, h in heights.items() if h >= HZ} - comps = flood(q, lambda a, b: q[a] == q[b]) - comps.sort(key=lambda c: (-len(c), c[0])) - relay_comp = [len(c) >= MINR for c in comps] - comp_of = {k: i for i, c in enumerate(comps) for k in c} - changed = True - while changed: - changed = False - for i, c in enumerate(comps): - if relay_comp[i]: - continue - if any((k[0], k[1] + dx, k[2] + dy) in comp_of and relay_comp[comp_of[(k[0], k[1] + dx, k[2] + dy)]] - for k in c for dx, dy in N8): - relay_comp[i] = True - changed = True - relayered = {k for i, c in enumerate(comps) if relay_comp[i] for k in c} - got_r = sorted((c[0][0], q[c[0]], len(c)) for i, c in enumerate(comps) if relay_comp[i]) - got_k = sorted((c[0][0], q[c[0]], len(c)) for i, c in enumerate(comps) if not relay_comp[i]) - if got_r != sorted((r["layer_before"], r["q"], r["tiles"]) for r in rl["regions"]): - errors.append("relayered regions differ from the tile heights") - if got_k != sorted((r["layer_before"], r["q"], r["tiles"]) for r in rl["kept_regions"]): - errors.append("kept regions differ from the tile heights") - # units and shifts - units = [] - unit_of = {} - bylayer = collections.defaultdict(list) - for k in leg: - if k[0] != 0: - bylayer[k[0]].append(k) - for L in sorted(bylayer): - for c in flood(bylayer[L], lambda a, b: True): - for k in c: - unit_of[k] = len(units) - units.append(c) - du = {} - for i, c in enumerate(units): - offs = {main[k] - k[0] for k in c if k not in relayered} - if len(offs) > 1: - errors.append("unit with {} tiles on layer {} does not move as one: shifts {}".format( - len(c), c[0][0], sorted(offs))) - du[i] = min(offs) if offs else 0 - if du[i] > 0 and c[0][0] < 1 or du[i] < 0 and c[0][0] >= 0: - errors.append("unit on layer {} moves {} (storeys move only up, caves only down)".format(c[0][0], du[i])) - dr = {} - for k in sorted(relayered): - d = main[k] - k[0] - q[k] - (du[unit_of[k]] if k in unit_of else 0) - ci = comp_of[k] - if dr.setdefault(ci, d) != d: - errors.append("region tile {} adjustment {} differs from its region's {}".format(k, d, dr[ci])) - if d > 0: - errors.append("region tile {} is adjusted up by {}".format(k, d)) - for k in leg: - if k in relayered: - continue - if k[0] == 0 and main[k] != 0: - errors.append("surface tile {} of {} moved to layer {} without a region".format(k, leg[k][0], main[k])) - # split tiles: a relayered tile whose sub-layer 0 floor stays below its raised floor keeps - # that ground part on its unit's layer; derived from the tile heights, not the records - derived_split = {} - for k in relayered: - g = ground.get(k) - base_layer = k[0] + (du[unit_of[k]] if k in unit_of else 0) - if g is not None and g // HZ < q[k] and base_layer != main[k]: - derived_split[k] = base_layer - if set(derived_split) != set(split): - errors.append("split tiles differ: {} derived from the tile heights, {} recorded".format( - len(derived_split), len(split))) - for k, v in leg.items(): - want = sorted({derived_split[k], main[k]}) if k in derived_split else [main[k]] - if sorted(v[1]) != want: - errors.append("legacy tile {} of {} appears on layers {}, expected {}".format(k, v[0], sorted(v[1]), want)) - for k, sp in split.items(): - if k not in derived_split or sp["ground_layer"] != derived_split[k] or sp["upper_layer"] != main[k] or \ - sp["floors"].get("0") != ground.get(k): - errors.append("split tile {} is inconsistent".format(k)) - if sorted((r["layer_before"], r["q"], r["tiles"], r["in_place_adjustment"]) for r in rl["regions"]) != \ - sorted((comps[i][0][0], q[comps[i][0]], len(comps[i]), dr.get(i, 0)) for i in range(len(comps)) - if relay_comp[i]): - errors.append("region in-place adjustments differ from the final layers") - got_u = sorted((units[i][0][0], d, len(units[i])) for i, d in du.items() if d) - if got_u != sorted((u["layer_before"], u["shift"], u["tiles"]) for u in rl["units_moved"]): - errors.append("moved units differ from the final layers") - # column order, caves underground, nothing lands on the surface - cols = collections.defaultdict(list) - for k, v in leg.items(): - cols[(k[1], k[2])].append((k[0], min(v[1]), max(v[1]))) - for (x, y), lst in cols.items(): - lst.sort() - for (la, _mna, mxa), (lb, mnb, _mxb) in zip(lst, lst[1:]): - if not mxa < mnb: - errors.append("column ({}, {}): layer {} ends at or above layer {}".format(x, y, la, lb)) - for k, v in leg.items(): - if k[0] < 0 and max(v[1]) >= 0: - errors.append("cave tile {} of {} reaches layer {}".format(k, v[0], max(v[1]))) - if main[k] != k[0] and main[k] == 0: - errors.append("moved tile {} of {} ends on the surface layer".format(k, v[0])) - # maps moved - mm = collections.defaultdict(collections.Counter) - moved_maps = set() - for k, v in leg.items(): - mm[v[0]][main[k]] += 1 - if main[k] != k[0]: - moved_maps.add(v[0]) - exp_maps = sorted(moved_maps) - if exp_maps != sorted(m["path"] for m in rl["maps_moved"]): - errors.append("moved maps differ from the final layers") - for m in rl["maps_moved"]: - if {str(L): n for L, n in sorted(mm[m["path"]].items())} != m["final_layers"]: - errors.append("final layers of {} differ".format(m["path"])) - - def walkable(a, b): - return a in heights and b in heights and a not in blocked and b not in blocked and \ - abs(heights[a] - heights[b]) < WDZ - - def rest(k): - return heights[k] - HZ * q[k] if k in relayered else heights[k] - - moved = sorted(k for k in leg if main[k] != k[0]) - moved_set = set(moved) - cut = set() - for k in moved: - for dx, dy in N8: - n = (k[0], k[1] + dx, k[2] + dy) - if n in leg and walkable(k, n) and main[k] != main[n]: - a, b = min(k, n), max(k, n) - cut.add((a, b) if main[a] < main[b] else (b, a)) - edges = sorted(cut) - parent = list(range(len(edges))) - - def find(i): - while parent[i] != i: - parent[i] = parent[parent[i]] - i = parent[i] - return i - - lay = [(main[lo], main[hi]) for lo, hi in edges] - for side in (0, 1): - at = collections.defaultdict(list) - for i, e in enumerate(edges): - at[(lay[i], e[side][1], e[side][2])].append(i) - for i, e in enumerate(edges): - for dx in (-1, 0, 1): - for dy in (-1, 0, 1): - for j in at.get((lay[i], e[side][1] + dx, e[side][2] + dy), ()): - a, b = find(i), find(j) - if a != b: - parent[max(a, b)] = min(a, b) - seams = collections.defaultdict(list) - for i in range(len(edges)): - seams[find(i)].append(i) - cell = rl.get("seam_cell", 24) - pieces = {} - for idx in seams.values(): - sid = min(edges[i] for i in idx) - for i in idx: - lo = edges[i][0] - pieces.setdefault((sid, lo[1] // cell, lo[2] // cell), set()).add(edges[i]) - if len(seams) != len(rl["seams"]) or sum(s["cut_edges"] for s in rl["seams"]) != len(edges): - errors.append("seams differ: {} seams with {} cut edges recomputed, {} with {} recorded".format( - len(seams), len(edges), len(rl["seams"]), sum(s["cut_edges"] for s in rl["seams"]))) - piece_by_edge = {} - for key, es in pieces.items(): - for e in es: - piece_by_edge[e] = key - seen_pieces = set() - ns_by_id = {n["id"]: n for n in rl["new_stairs"]} - for s_ in rl["seams"]: - for pc in s_["pieces"]: - e = (tuple(pc["low_tile"]), tuple(pc["high_tile"])) - key = piece_by_edge.get(e) - if key is None: - errors.append("seam piece edge {} of {} is not a cut walkable edge".format(e, s_["id"])) - continue - if key in seen_pieces or len(pieces[key]) != pc["edges"] or [key[1], key[2]] != pc["cell"]: - errors.append("seam piece {} of {} does not match the recomputed piece".format(pc["cell"], s_["id"])) - seen_pieces.add(key) - if pc["continuity"] == "new stairs" and pc["stairs"] and pc["stairs"][0] in ns_by_id: - first = ns_by_id[pc["stairs"][0]] - if "exit" not in pc and (first["low_tile_legacy"], first["high_tile_legacy"]) != \ - (pc["low_tile"], pc["high_tile"]): - errors.append("seam piece {} of {} does not put its first stairs on its representative edge".format( - pc["cell"], s_["id"])) - elif pc["continuity"] != "existing exit" or tuple(pc.get("exit", ())) not in records: - errors.append("seam piece {} of {} has no continuity".format(pc["cell"], s_["id"])) - for sid in pc["stairs"]: - n = ns_by_id.get(sid) - objs = {o.get("tile"): o for o in (n or {}).get("objects", [])} - lo_o, hi_o = objs.get("low", {}), objs.get("high", {}) - if n is None or len(n["objects"]) != 2 or not ( - lo_o.get("exit") is True and lo_o.get("exit_to") == "high" - and re.match(r"^(stairs|ladder)_.*up\d*$", lo_o.get("archetype", "")) - and hi_o.get("exit") is True and hi_o.get("exit_to") == "low" - and re.match(r"^(stairs|ladder)_.*down\d*$", hi_o.get("archetype", ""))): - errors.append("new stairs {} are not an up stair on the low tile and a down stair on the high " - "tile targeting each other".format(sid)) - continue - lo, hi = (tuple(n["low_tile_legacy"]), tuple(n["high_tile_legacy"])) if n else (None, None) - if n is None or piece_by_edge.get((lo, hi)) != key or n["low"] != [lo[1], lo[2], main[lo]] \ - or n["high"] != [hi[1], hi[2], main[hi]] or n["high"][2] - n["low"][2] != 1 \ - or cheb(n["low"], n["high"]) > tol: - errors.append("new stairs {} of seam piece {} of {} are not plain stairs across one of its " - "cut edges".format(sid, pc["cell"], s_["id"])) - if len(seen_pieces) != len(pieces): - errors.append("{} seam pieces recomputed, {} recorded".format(len(pieces), len(seen_pieces))) - if sorted(n["id"] for n in rl["new_stairs"]) != sorted(i for s_ in rl["seams"] for pc in s_["pieces"] - for i in pc["stairs"]): - errors.append("new stairs and seam pieces do not list the same stairs") - # continuity: the two tiles of every cut edge stay connected on the final layers - # (walking, legacy overworld exits, new stairs) within the maps with tile heights - hmaps = set(rl["tile_heights"]) - parent = {} - - def uf_find(a): - parent.setdefault(a, a) - while parent[a] != a: - parent[a] = parent[parent[a]] - a = parent[a] - return a - - def uf_union(a, b): - ra, rb = uf_find(a), uf_find(b) - if ra != rb: - parent[rb] = ra - - gpos = collections.defaultdict(list) - for k, v in leg.items(): - if v[0] in hmaps and k in heights and k not in blocked: - gpos[(main[k], k[1], k[2])].append((k, rest(k))) - if k in derived_split: - gpos[(derived_split[k], k[1], k[2])].append((("ground",) + k, ground[k])) - for (L, x, y), here in gpos.items(): - for dx, dy in ((1, 0), (-1, 1), (0, 1), (1, 1)): - for a, ra in here: - for b, rb in gpos.get((L, x + dx, y + dy), ()): - if abs(ra - rb) < WDZ: - uf_union(a, b) - for r in ow.get("internal_exits", []): - X, Y, Z = world_cell(r["source_map"]) - X2, Y2, Z2 = world_cell(r["target_map"]) - uf_union((Z, 24 * X + r["source_tile"][0], 24 * Y + r["source_tile"][1]), - (Z2, 24 * X2 + r["target_tile"][0], 24 * Y2 + r["target_tile"][1])) - for n in rl["new_stairs"]: - uf_union(tuple(n["low_tile_legacy"]), tuple(n["high_tile_legacy"])) - apart = [e for e in edges if uf_find(e[0]) != uf_find(e[1])] - if apart: - errors.append("{} cut walkable edges are left without continuity, e.g. {}".format(len(apart), apart[0])) - steep = set() - for k in sorted(set(moved) | relayered): - for dx, dy in N8: - n = (k[0], k[1] + dx, k[2] + dy) - if n in leg and walkable(k, n) and main[k] == main[n] and abs(rest(k) - rest(n)) >= WDZ: - steep.add((min(k, n), max(k, n))) - rec_steep = {tuple(sorted(((s_["layer"], *s_["tiles"][0]), (s_["layer"], *s_["tiles"][1])))) for s_ in rl["steep_steps"]} - if steep != rec_steep: - errors.append("steep steps differ: {} recomputed, {} recorded".format(len(steep), len(rec_steep))) - finals = collections.defaultdict(list) - for k, v in leg.items(): - for L in v[1]: - finals[(L, k[1], k[2])].append(k) - contacts = set() - for k in moved: - for dx, dy in N8: - for t in finals.get((main[k], k[1] + dx, k[2] + dy), ()): - if t[0] != k[0]: - contacts.add((min(k, t), max(k, t))) - if contacts != {(tuple(c["tiles"][0]), tuple(c["tiles"][1])) for c in rl["new_contacts"]}: - errors.append("new contacts differ: {} recomputed, {} recorded".format(len(contacts), len(rl["new_contacts"]))) - for c in rl["new_contacts"]: - a, b = tuple(c["tiles"][0]), tuple(c["tiles"][1]) - walk = a in heights and b in heights and a not in blocked and b not in blocked and \ - abs(rest(a) - rest(b)) < WDZ - if c["walkable"] != walk or c["final_layer"] != main.get(a if a in moved_set else b): - errors.append("new contact {} / {} records walkable {} on layer {}; recomputed {}".format( - a, b, c["walkable"], c["final_layer"], walk)) + # the relayering looks up only the seam pieces' exits among the records: pass just their membership + seam_exits = {tuple(pc.get("exit", ())) for s_ in rl["seams"] for pc in s_["pieces"]} + rl_errors, main, rl_counts = _relayering(ow_rows, leg, rl, ow.get("internal_exits", []), + {k for k in seam_exits if k in records}, [HZ, MINR, WDZ, tol]) + errors.extend(rl_errors) # every exit with an overworld end on a moved tile must be listed, and nothing else def moved_end(g, mp, t): if g != ow_id: @@ -868,6 +1638,8 @@ def moved_end(g, mp, t): moved_end(r["source_group"], r["source_map"], r["source_tile"]) or moved_end(r["target_group"], r["target_map"], r["target_tile"]))} listed = {(e["source_map"], e["line"]) for e in rl["exits"]} + if len(listed) != len(rl["exits"]): + errors.append("an exit is listed twice in the relayering exits") if expected != listed: errors.append("relayering exits differ: {} expected but not listed, {} listed but not expected".format( len(expected - listed), len(listed - expected))) @@ -881,6 +1653,13 @@ def moved_end(g, mp, t): if e["layer_delta_before"] != ls[1] - ls[0] or e["layer_delta_after"] != r["layer_delta"] or \ e["becomes"] != r["becomes"]: errors.append("relayering exit {}:{} before/after does not match".format(e["source_map"], e["line"])) + same_as("relayering exit", e, r, ("source_tile", "target_map", "target_tile", "arch", "xy_distance", + "stairwell", "portal_reason")) + crosses = None if r["source_group"] == r["target_group"] else ( + r["target_group"] if r["source_group"] == ow_id else r["source_group"]) + if e.get("crosses_to_group") != crosses: + errors.append("relayering exit {}:{} crosses to {}, its record to {}".format( + e["source_map"], e["line"], e.get("crosses_to_group"), crosses)) for g in placed: if any(r["relayered"] for r in rl["group_regions"].get(g["id"], [])): errors.append("placed group {} has a relayered region, but its maps are recorded on one layer " @@ -892,16 +1671,116 @@ def moved_end(g, mp, t): "stairwells_checked": len(plan["stairwells"]), "stairwells_by_kind": dict(sorted(kinds.items())), "stairwell_exits_required": required, "stairwell_intermediate_tiles": intermediate, "exit_endpoints_checked": exits_checked, - "relayering": {"overworld_tiles": len(leg), "tile_heights": len(heights), "regions": len(got_r), - "kept_regions": len(got_k), "units": len(units), "moved_units": len(got_u), - "moved_tiles": len(moved), "cut_edges": len(edges), "seam_pieces": len(pieces), - "steep_steps": len(steep), "new_contacts": len(contacts)}} + "exit_inventory_objects": len(inv), "exit_record_copies_checked": sum(copies.values()), + "relayering": dict({"overworld_tiles": len(leg)}, **rl_counts)} + + +def same_json(a, b) -> bool: + """JSON equality without Python's coercions (true is not 1, 1.0 is not 1), compared in place.""" + stack = [(a, b)] + while stack: + x, y = stack.pop() + if type(x) is not type(y): + return False + if isinstance(x, dict): + if x.keys() != y.keys(): + return False + stack.extend((x[k], y[k]) for k in x) + elif isinstance(x, list): + if len(x) != len(y): + return False + stack.extend(zip(x, y)) + elif x != y: + return False + return True + + +def first_difference(a, b, path="", limit=12): + """Paths where two JSON values differ, at most `limit` of them (deepest common path).""" + if type(a) is not type(b) or not isinstance(a, (dict, list)): + return [path or "/"] if not same_json(a, b) else [] + if isinstance(a, dict): + out = [] + for k in sorted(set(a) | set(b)): + if k not in a or k not in b: + out.append("{}/{}".format(path, k)) + elif not same_json(a[k], b[k]): + out += first_difference(a[k], b[k], "{}/{}".format(path, k), limit - len(out)) + if len(out) >= limit: + break + return out[:limit] + if isinstance(a, list): + if len(a) != len(b): + return ["{} (length {} != {})".format(path or "/", len(a), len(b))] + out = [] + for i, (x, y) in enumerate(zip(a, b)): + if not same_json(x, y): + out += first_difference(x, y, "{}[{}]".format(path, i), limit - len(out)) + if len(out) >= limit: + break + return out[:limit] + return [path or "/"] if not same_json(a, b) else [] + + +def rebuild(content: Path) -> dict: + """The plan the generator builds from `content` (read-only), as JSON data.""" + sys.path.insert(0, str(Path(__file__).resolve().parent)) + import build_plan # the generator + + return json.loads(json.dumps(build_plan.build(content)[0])) + + +def compare_with_rebuild(plan: dict, fresh: dict) -> list: + """Errors unless `plan` equals the rebuild as JSON, content_root (the checkout's path) aside.""" + published = {k: v for k, v in plan.items() if k != "content_root"} + rebuilt = {k: v for k, v in fresh.items() if k != "content_root"} + if same_json(published, rebuilt): + return [] + diff = first_difference(published, rebuilt) + return ["the plan differs from a rebuild from the content at {}: {}".format( + str(fresh.get("content_git_head"))[:12], ", ".join(diff) or "(value types)")] + + +def verify_content(plan: dict, content: Path, fresh: dict = None) -> dict: + """Fidelity: rebuild the plan from the content with the generator and compare it with `plan`. + + The checkout must be at the plan's content_git_head and clean under the + input trees (the generator also re-checks the inputs digest). Every field + must equal the rebuild as JSON, content_root (the checkout's own path) + aside. `fresh` is a rebuild already made from the same checkout.""" + sys.path.insert(0, str(Path(__file__).resolve().parent)) + import build_plan # the generator (reads the content read-only) + + rev = build_plan.content_revision(content) + if rev["head"] != plan.get("content_git_head") or not rev["clean"]: + return {"ok": False, "errors": ["content checkout {} is at {}{}, the plan was built at {}".format( + content, rev["head"][:12], "" if rev["clean"] else " with uncommitted changes", + str(plan.get("content_git_head"))[:12])]} + errors = compare_with_rebuild(plan, rebuild(content) if fresh is None else fresh) + return {"ok": not errors, "errors": errors} def main(argv=None): - argv = argv if argv is not None else sys.argv[1:] - path = Path(argv[0]) if argv else Path(__file__).resolve().parents[1] / "placement-plan.json" - res = verify(json.loads(path.read_text(encoding="utf-8"))) + import argparse + + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("plan", nargs="?", type=Path, + default=Path(__file__).resolve().parents[1] / "placement-plan.json") + ap.add_argument("--content", type=Path, + help="content checkout at the plan's content_git_head: also check the plan against a rebuild") + args = ap.parse_args(argv if argv is not None else sys.argv[1:]) + if args.plan.stat().st_size > MAX_PLAN_BYTES: + print(json.dumps({"ok": False, "errors": ["{} is larger than {} bytes".format(args.plan, MAX_PLAN_BYTES)]})) + return 1 + plan = json.loads(args.plan.read_text(encoding="utf-8")) + res = verify(plan) + if args.content is not None: + try: + res["content"] = verify_content(plan, args.content.resolve()) + except Exception as exc: # noqa: BLE001 - a checkout that cannot be rebuilt fails the check + res["content"] = {"ok": False, "errors": ["cannot rebuild from {}: {}: {}".format( + args.content, type(exc).__name__, exc)]} + res["ok"] = res["ok"] and res["content"]["ok"] print(json.dumps(res, indent=1)) return 0 if res["ok"] else 1 diff --git a/tools/validate.sh b/tools/validate.sh index 973f9da..5a6811d 100755 --- a/tools/validate.sh +++ b/tools/validate.sh @@ -16,6 +16,7 @@ cargo clippy --locked --workspace --all-targets -- -D warnings cargo test --locked --workspace --all-targets cargo test --locked --workspace --doc python3 tools/test_writable_source.py +python3 -I migration/placement-plan/scripts/test_placement_plan.py cargo build --locked --release --package atrinik-content cargo run --locked --quiet --package atrinik-content -- --version cargo run --locked --quiet --package atrinik-content -- \