Progression Rules - joric/ootss GitHub Wiki

Order of the Sinking Star: progression and completion rules

From sinking_star.exe md5 dfcc6b777e79700fdcb4bfaa7e4fe673, IDA instance abc664c37365. Function names are the ones set in the IDB. "Confirmed" means the decompiled or disassembled body was read. Offsets are in-memory struct offsets (Type_Info). campaign_status (global 0x140B667A8) is Status.sokoban_status in memory. level_states[name] is the per-level Level_State_Flags table, keyed by level_name.

1. Level_Entry unlocking and state

Rule Function jai Confirmed
"Entered/visible": enabled_t_target=1 if level_states has any entry for level_name (any flags, even 0) apply_level_state_to_level_entry 0x1400D90D0, update_unlocks 0x1402DBC80 yes
An entry is added (flags 0) whenever that level is loaded get_or_add_level_state_flags 0x1400F0E50, called by load_level 0x14032D110 campaign.jai:1289 yes
"Solved" = level_states[level_name] & SOLVED(1). Sets solved_t_target=1. Never read from anything else 0x1400D90D0 yes
SOLVED is set on victory for the current level's name handle_level_won 0x1402D8930 campaign.jai:1335 yes
PARTIALLY_SOLVED(2) is set when the hero activates an Overworld_Unlock in that level (sets activated, bark overworld_unlocked) process_hero_contacts 0x1403A44E0 markers.jai yes
ENTERED(4): none of the four callers of get_or_add_level_state_flags sets it (they set 1, 2 and 8). It is not set in the sample save (flag values seen: 0×5, 1×133, 3×3, 9×8). Treat it as legacy callers 0x14025D540, 0x1402D8930, 0x14032D110, 0x1403A44E0 yes (callers)
Two enable checks differ: apply_level_state_to_level_entry needs the entry to be present (flags 0 counts); update_unlocks needs flags != 0. Since load always runs the first one, presence is enough 0x1400D90D0, 0x1402DBC80 L159 yes
TOWER_UNLOCK_SIGNALLED(8): set once a solved tower level's "tower is about to unlock" cue has played 0x14025D540 (inline), refresh_overworld_progress 0x1402D8200 yes
Unlock evaluation runs only on Level_Entries with enabled_t_target==0. The call is `if (id1 id2) gate(id1,id2,id3) else if (id3) gate(0,0,id3)`: with id1 and id2 both 0 only id3 is used; with all three 0 the entry is skipped (a qword test covers id1 and id2 together) 0x1402DBC80
Gate: unlock_is_or_gate true = any nonzero id satisfied; false = all nonzero ids satisfied evaluate_unlock_gate 0x140328210 yes
Unlock is not saved. It is recomputed from the other state at every overworld load and after each change (just a visual enabled_t_target=1, bark level_unlocked) 0x14025D540 @0x1402641CD yes
What an unlocker id may be (it resolves in the same level file): Level_Entry satisfied if its solved_t_target!=0 (SOLVED). Sign satisfied if rank_satisfied. Teleporter if discovered. Marker named intro_end1_freezer_thaw (+0x34C float >0) or with script end3_exploit_level_freezer_thaw (boolean_argument). Judge only for Freezers. Any other type is never satisfied is_unlocker_satisfied 0x14035A140 freezers.jai:585 yes
For Level_Entry gates the mode is -1, so a PARTIALLY_SOLVED unlocker does not count 0x1402DBC80 → 0x140328210(a9=-1) yes
tower_flags: does not gate the entry. It assigns the level to the Tower whose tower_flags is exactly equal count_tower_levels 0x14035A5E0 towers.jai:5-98 yes
dropdown: tower level that is optional for the tower. It is counted in total/solved_dropdown_levels, not in levels_required. All dropdowns solved = the tower's "gold" status 0x14035A5E0, draw_tower_status_sign 0x1402779E0 yes
achievement: solving that entry grants the achievement (bit 0 also set; also advances rank, see 3) 0x1400D90D0, load_level 0x14032D110 @0x14032ED1B yes
currently_broken or currently_disfavored: the entry is destroyed at every level load, so it never exists in play. Drop it from all counts load_level 0x14032D110 @0x14032D795, destroy_entity 0x14030AA70 yes
demo_phase (Level_Entry 0x3C0): no direct [reg+0x3C0] byte read in .text (a read through a rebased pointer would be missed). Only Sign.demo_phase is read, by a level-export path in save.jai (0x140438750, with demo_bounds markers). *choice_demoN in level sets is ignored byte-displacement scan, parse_level_set 0x140188700 yes (scan)
context_type (OPTIONAL 1, DIFFICULT 2, ORDERING 4, EASY 8, OTHER): designer metadata. No gameplay read seen partial

Freezer (main area gate; the overworld has 757 of them)

Rule Function Confirmed
Evaluated while unfrozen==0. It uses unlocked_by_id..id4 and unlock_is_or_gate with the same gate. Satisfied → open update_unlocks 0x1402DBC80 (first loop) yes
Mode = partial_solve_count. If it is not -1, a Level_Entry unlocker also counts when its level is PARTIALLY_SOLVED 0x1402DBC80 → 0x14035A140 yes
Judge unlocker: Judge.state_int0 >= Freezer.judge_unlock_level. On load the first Judge is compared with every freezer that has judge_unlock_level!=0 0x14035A140, reopen_saved_freezers 0x1402C5E00 yes
On opening (RemoveFreezer/freezerOpened), and only if judge_unlock_level==0, the freezer Pid goes into opened list A (overworld) or B (any other level). On load these freezers are reopened from the lists 0x14025D540 @0x140265342, 0x1402C5E00 yes

Sign rank_satisfied (simulate pass 0x140330EC0 @0x1403392AC..0x140339380, simulate.jai; it then reruns update_unlocks)

Sign setup Satisfied when Confirmed
unlock_freezers_with_rank = r (Character_Rank) r == 0xFF or r <= campaign_status.rank (+208) yes
tower_id != 0 that Tower has activated yes
gold_room_connection_unlocker (Signs without tower_id; with one, the tower's activation decides) at least 4 Teleporters on the overworld have gold_room_endpoint && discovered (the count is kept in world+0x63CC, recomputed on overworld load in 0x14025D540 @0x14026C2E2) yes

Tower

Rule Function Confirmed
solved_normal_levels counts Level_Entries with tower_flags == Tower.tower_flags and dropdown==false whose Status flags have SOLVED. Dropdowns are counted separately count_tower_levels 0x14035A5E0 yes
The tower activates when not activated and solved_normal_levels >= levels_required. It enables its teleporter_id Teleporter and grants an achievement from tower_flags: ground pair → TOWER_xx, UPSTAIRS(16) + single letter → TOWER_x_SKY, DOWNSTAIRS(32) + pair → TOWER_xx_UNDERWORLD activate_tower 0x1402487A0 yes
Tower id order: H=1 M=2 S=4 P=8. The pairs are HM=3, HS=5, MS=6, HP=9, MP=10, SP=12, mapped to TOWER_HM, HS, MS, HP, MP, SP tables 0x14076879E / 0x140768794 yes

2. Status.sokoban_status field names (fold into FORMAT.md)

File position Mem off Name Evidence
s64×2 +192 last_saved (Apollo) save_campaign_status 0x1401FDA90
u8 +208 rank (Character_Rank): 0 Visitor, 1 Egressant (ENDGAME1), 2 Pusher (ASCEND), 3 Arranger (ENDGAME2), 4 Intrepid (DESCEND), 5 Sokoban (ENDGAME3); monotonic. Range 0..5, not 0..4 advance_rank_for_achievement 0x140098C40
s64×2 +216 play_time menu 0x140251DC0
s64 +232 achievements, bit n = Achievement n; bit 0 set whenever any is granted
u32 +240 treasure_flags: Skipping Stones treasures. When the skipping_stones level is finished, Level_State.skipping_stones_treasure_flags is ORed in. Overworld Treasure_Display i shows acquired iff popcount > treasure_index 0x14034AB70 @L2495, 0x1402D8200
u32 +244 stat: gems picked up from the floor (Gem with inventory_index<0 in the hero's cell; "GemDustoff") 0x14023F060
u32 +248 stat: gems placed into a Gem_Slot (hero's latest inventory gem) 0x14023F060
u32 +252 stat: Artifacts picked up (every kind, wands included) 0x140311B20
u32 +256 never incremented in this build scan
u32 +260 stat: Friendly_Dragon follow/unfollow toggles (TOGGLE_FRIENDLY_DRAGON action, dragon within 1.25 of the hero) 0x14023F060
u32 +264 stat: successful P1 wand uses ("p1wand_success") 0x140288FB0
u32 +268 stat: beams turned by a torc (cw/ccw/180) 0x140288FB0
u32 +272 stat: prism uses succeeded ("PrismSuccess") 0x140288FB0
u32 +276 stat: entering an enabled Level_Entry 0x14023F060, 0x140311B20
u32 +280 stat: ACTIVATE actions handled (sub_14034AB70, before mirror/teleport handling; not mirror-specific) 0x14034AB70
s64 +288 artifacts: bitmask 1<<Artifact.kind, set only for kinds 3-6 (SPEED_CONTROL, UNDO_SIDE_PANEL, _EXTENDED, _VERY_EXTENDED) on pickup; kinds 0-2 (PROMESST_1/2/3_WAND) set Guy.inventory_flags instead (WAND 0x400, TORC 0x800, PRISM 0x1000); 7 SPAWNER_MORPH only counts in +252. No level contains kind ≥3, and nothing reads the bits (time control is gated by levels_solved>0 or level time_tutorial), so it is always 0 in this build. Loading the overworld clears the bit of each Artifact still present there 0x140311B20, 0x1403BDE70
u32 +296 current_area_pin: recomputed every frame (lowest float_parameter local_area Marker holding the hero whose Area_Pin is activated, else 0); 0 in every non-overworld level, so it is non-zero in a file only if saved on the overworld inside an activated area update_hero_markers 0x140322D50
s32 +300 levels_solved: overworld Level_Entries with solved_t_target!=0. Shown as "% solved" in the campaign list. >0 also enables time control refresh_overworld_progress 0x1402D8200, 0x1400A8830
s32 +304 tower_levels_solved: the same, restricted to tower_flags!=0 0x1402D8200
s32 +308 towers_activated 0x1402D8200
u8 +312 show-area-names toggle for the overhead map (Activate while the TAB map is open; needs ≥2 activated named Area_Pins, else forced 0); UI-only action_activate 0x14024C3C0, 0x1403D2A50
u8 +313 overworld_reached (lobby vs overworld; after a win: back to the overworld instead of advancing in the level set) 0x1402D8930
str16 current level = current_level_set.level_names[current_level_index] 0x1401FDA90
str16 +320 menu-background level 0x140251DC0
table +0 level_states (Jai Hash_Table, entry 32 B: hash, string, u8 flags)
table +56 string table {str key, s64, str}: only load and save touch it; no gameplay use (legacy) callers of 0x1402003B0
list +112 pid list A = opened Freezers on the overworld 0x14025D540 @0x140265304
list +152 pid list B = opened Freezers in non-overworld levels same
runtime +336/+344/+360 not saved (last shown area pin, play-time accounting)

3. Achievements

Achievement Condition Where Confirmed
TOWER_* (16) that tower activates (solved_normal_levels >= levels_required) activate_tower 0x1402487A0 yes
ASCEND / DESCEND hero touches a grant_rank Marker with float_parameter 2 / 4; also sets rank 2 / 4 process_hero_contacts 0x1403A44E0 yes
COMPLETIONIST on the overworld (Settings.special_level==OVERWORLD), every Level_Entry present (broken and disfavored ones are already destroyed) has solved_t_target!=0, i.e. its level is SOLVED. Dropdowns, tower levels and sky/underworld entries are all included. Entries in other levels do not count update_unlocks 0x1402DBC80 end, reopen_saved_freezers 0x1402C5E00 end yes
All others (GOLD_*, ENDGAME1-3, HEROES, MIRRORS, SKIPPING, PROMISE) no code grants them by id. They come from Level_Entry.achievement on the entry whose level is solved (ENDGAME1/2/3 also set rank 1/3/5). Which entry carries which comes from the level data 0x1400D90D0, 0x14032D110 yes (code); data mapping not checked
Sync each overworld load re-sends every set bit 1..32 to Steam. GetAchievement results are discarded 0x14032D110 @L1340, 0x14025D540 @0x140261809 yes

100% (as the game defines it) = COMPLETIONIST = SOLVED for every overworld Level_Entry that is not currently_broken or currently_disfavored. "Required" for tower progress = non-dropdown entries with the tower's tower_flags, up to levels_required. Optional for towers = dropdowns (the gold status). Everything is required for COMPLETIONIST.

4. Campaigns and level sets

Rule Function jai Confirmed
Campaign = save subfolder holding Status.sokoban_status. All campaigns play the same game; there is no alternate mode or NG+ list_campaigns 0x14016FFA0 campaign.jai:324-362 yes
First campaign folder = localized campaign_name_default (en default, de Standard, fr Par défaut, ru по умолчанию...). If the in-memory name is empty when saving, the save code uses the literal default startup 0x14008A550 L1255-1275, 0x1401FDA90 campaign.jai:375,387,827 yes
At startup the most recent campaign is loaded. "Start a New Campaign" names the folder from random localized words campaign_name_h_*(9) + m_*(6) + s_*(7) + p_*(14), concatenated, retried on collision. There is a campaign count limit (menu_campaign_limit_reached, 0x1401C8710) make_new_campaign 0x14011C070 campaign.jai:1739,1745 yes (limit value not read)
Level set (.level_set, called "campaign" in code strings) = ordered level list. *choice after level i = after a win at i return to the overworld (OVERWORLD_DUE_TO_CHOICE). *worlds_collide = mode 2. Otherwise the game advances to the next level of the set parse_level_set 0x140188700, handle_level_won 0x1402D8930 level_sets.jai:89-174 yes
The active set is chosen by level name: find_level_set_for_level 0x14027B520 picks the set containing the level. It prefers sets that are not drafts*/tests*/render_tests*/gyms*/unplaced_levels, and skips design_illustrations once the overworld is reached. Door switch_to_level_set switches by set name. Level sets do not split completion switch_to_level_set 0x1402BB3A0 (global current_level_set) sokoban.jai:7876-7920 yes
On campaign load the game goes to the overworld set (if allowed), else to the saved current level load_campaign 0x1400D0870 L1215 yes
Status current level keeps naming a level after the player leaves it, until Status is saved again. File times don't tell leaving apart from playing: on a live save, leaving a level wrote that level's history and Status, then overworld.history two seconds later, and another time, while still inside the level, overworld.history was written 55 s after Status observed

5. In-game progress displays

Display Computation Function Confirmed
Campaign list "% solved" + play time + date levels_solved (+300) refresh_menu_campaign_list 0x140251DC0 yes
Tower status sign (Sign.display_hook TOWER_STATUS) star: solved_normal_levels >= levels_required; gold: solved_dropdown_levels == total_dropdown_levels, with no minimum and independent of activation (a tower without dropdowns is gold at once). The gold-room variant: discovered gold-room teleporters ≥ 4 draw_tower_status_sign 0x1402779E0 yes
END1/END2/END3_STATUS sign hooks exist in the enum; not traced 0x1403D2A50 no
Area_Pin activated is set on contact (and the_crossroads). It is entity state (overworld .history), not in Status. No N-of-M counts. HUD title: lowest float_parameter local_area Marker containing the hero whose target pin is activated (update_hero_markers); map label at map_label_marker_id for every activated named pin, shown once ≥2 are activated (sub_1403D2A50); no fog check. Activation: touching the pin (activate_area_pin from process_hero_contacts) activate_area_pin 0x1402C5580 yes
View_Giver (map scroll) taking it adds its map_overview_id / _2 Camera_Controls to enabled_alternate_views; the TAB overhead map of a ground layer needs that layer's overhead camera in the list (overworld: 3 scrolls, onground/sky/underworld). Saved as View_Giver.activation_order≠0 + entity_flags 0x200000 in .history. No fog effect take_view_giver 0x1400B6630, 0x1404360C0 yes
Collectibles Voice_Log (71 on the overworld): heard = .history CHANGE of played=1 (set on Activate). Switch.pressed and Gem_Slot.flags are puzzle state in .history, not collectibles. Door (3) are story transitions yes

6. Treasures

Rule Function Confirmed
Treasures are picked up inside the skipping_stones level (story_tag): Level_State.skipping_stones_treasure_flags (u8). "Got treasure bit % twice" shows that bit = Treasure.treasure_index 0x1403A44E0 L1117-1191 yes
On finishing that level (Impeller return), `campaign_status.treasure_flags = skipping_stones_treasure_flags`. The barks distinguish 0, 1, 2-4, 5 and all (>5), so there are 6 treasures (matches 6 Treasure_Display) 0x14034AB70 L2460-2495
Overworld display: Treasure_Display i is acquired iff popcount(treasure_flags) > i (by count, not by bit) refresh_overworld_progress 0x1402D8200 yes
Treasures drive nothing but the Treasure_Display meshes: no achievement, rank, % solved, tower or gate reads treasure_flags yes
Gold rooms: a gold room is visited when its gold_room_endpoint Teleporter is discovered (reached through the activated tower's teleporter pad). A Sign with tower_id is decided by that tower's activation, even when it also has gold_room_connection_unlocker; only a Sign without tower_id uses the count of discovered endpoints (≥ 4). Nothing else about gold rooms is in Status see §1 yes

7. Fog of war

Data: scripts/build_fog.py → data/fog.json. The type table spells Fog_Of_War plainly (no !). Fog_Volume (324 on the overworld) has no region state and takes no part in it.

Rule Function jai Confirmed
Ground layers: 0 ONGROUND [underground_barrier, skyground_barrier), 1 UNDERGROUND (< underground_barrier), 2 SKYGROUND (≥ skyground_barrier, up to the lowest FAST_TRAVEL Camera_Marker box), 3-5 the three fast-travel map rooms. Overworld: -4 / 10 / 75.16. An entity in a Group with bind_to_layer takes that layer instead of its z get_ground_layer 0x14024A600, layer table in 0x140438750 ground_layers.jai:92,158 yes
A primary Fog_Of_War (overworld: ground_primary_fog #56-39, underworld_primary_fog #56-6554, sky_primary_fog #56-6556) owns one region per screen of its layer: region_states has ceil(squares_x/squares_per_screen_x) × ceil(squares_y/squares_per_screen_y) = 10 × 12 = 120 entries (needs per_screen_camera) init_fog_region_states 0x14016D880, get_screen_grid_dims 0x14025CDF0 fog_of_war.jai:155 yes
Region index = sx + 10*sy, sx = int((x - per_screen_camera_corner.x) / 19), sy = int((y - corner.y) / 13); corner (-24, -2) fog_region_index_at 0x1402906B0 volumetric_fog.jai:406 yes
States: 0 FILLED, 1 PARTIAL (fading out), 2 CLEARED. PARTIAL becomes CLEARED when the fade ends and at every level load, so 1 and 2 both mean revealed 0x140407860 @0x140419C50, load_level 0x14032D110, 0x14016D880 yes
A region clears when a hero on the fog's layer stands in it with z <= top; top = the Fog_Of_War_Z in that region (z + ascent) or the fog's z + ascent (ascent < 0 → 6.5). Runs every frame for the current layer, and at load for every hero clear_fog_region_at 0x1402873D0, 0x140407860 @0x140419B90, load_level fog_of_war.jai yes
Other clear sources: Markers named hack_intro_fog_clear; discovered Teleporters (apply_overworld_discovered_teleporters 0x1402E7400); at load, Fog_Of_War_Clear with radius 0 and no direction (none on the overworld) update_hero_markers 0x140322D50, 0x1402E7400, load_level yes
Fog_Of_War_Clear with clear_neighboring_screen (89 on the overworld): when its screen clears, the neighbour EAST (sx+1), NORTH (sy+1), WEST (sx-1) or SOUTH (sy-1) is set CLEARED at once (wrapping) and the rule chains. Radius markers only start manual_clear_time clear_fog_neighbors_from_clear_markers 0x1402A9230 yes
Non-primary Fog_Of_War (25 boxes, all sky layer): one region_states entry (none with clear_as_part_of_room), set PARTIAL when any hero is inside position ± props.box_extents (half-open). create_overhang boxes also build overhang geometry; only_overhang_no_fog ones draw no fog 0x140407860 @0x140419D2D, 0x1402C6520 yes
Fog_Of_War_Z sets the vertical extent of its region on its layer: [z - descent, z + ascent] (defaults 4.5 / 6.5 when negative); first one per region wins 0x1402873D0, renderer 0x14042D230 yes
Saved state: region_states is entity state (NoEditorSave, empty in level data). It is in overworld.history as CHANGE actions on the fog Pids, each carrying the whole array. Current state = new of the action in the record with the highest first-move index (any mip level or the current record). No action = all FILLED. Nothing in Status parse_history yes (sample)
Sample save: ground fog 42/120 revealed (5, 15, 25, 26, 34-36, 40, 44-50, 52-58, 65-67, 75-77, 85-88, 95-97, 105-107, 115-118); sky and underworld never touched (hero z only 0 and 89). All 147 levels with Status state lie in revealed regions; 198 of 920 overworld Level_Entries do yes

8. Overworld state from the save

Latest value rule (all items below): the current value of an entity field is the new value of the CHANGE action in the undo record with the largest key (record is the current one, first move + span, stack index; on equal keys the first such action), as build_fog.save_states does; no action = level-data value. Undo records are replayed and reverted by apply_undo_record_actions 0x14033ABC0 (old or new side per direction; CREATION 2, OCEAN_CHANGE 3). Hero position = the s32×3 of the record with the largest key that has a non-zero span and position (sample: stack 0, move 14320, (141, 113, 0), matching the Guy's latest position CHANGE; the current record is empty, (0, 0, 0)).

Water

Rule Function Confirmed
The only writer of OCEAN_CHANGE is a Flow_Updater (river): ocean_values[idx] = 1 | (river_polarity==1)<<1 | direction<<2, logged as {pid, idx, old, new}. Bit 0 is always set, so OCEAN_CHANGE never drains water; it can only add water (old 0) or change the flow set_ocean_square_state 0x1402CD1A0 (ocean.jai:77), callers 0x14027FE50, 0x14030D6E0, 0x140327C80, 0x14035DAC0 yes (regex for a type-3 record over the 9 decompiled callers of the undo push 0x1401CCC80; the other hit is the replayer 0x14033ABC0)
idx = (x - corner.x) + (y - corner.y) × squares_x in the map's local square (position + corner), x/y rounded with +0.5 floor, grid wrapping applied ocean_square_at 0x14009D0D0, grid_cell_of_position 0x1400AA3A0 yes
Water query: first Ocean_Map whose z is in the ground-layer range of the position's z and z <= pos.z + 2.1 with bit 0 at the square; PLAYER_BLOCKING_ONLY maps count only when asked for find_ocean_square_at 0x14023EDE0 yes
detect_holes: every square gets bit 0 at init, then each voxel of an OCEAN_HOLE Wall_Set with ocean_hole_id == map pid clears it init_ocean_map_values_and_mesh 0x1400B26A0 (ocean.jai:343) yes
Existence: change_existence_group_status sets CURRENTLY_EXISTS and clears INVISIBLE_IN_A_TOGGLABLE_WAY on entering existence (and the reverse); Ocean_Map members are rebuilt through 0x1400B26A0 change_existence_group_status 0x140354DD0 yes (flag writes); what each consumer skips for a non-existing entity is not traced
Sample save: no OCEAN_CHANGE at all. The 36 Ocean_Map CHANGEs are entity_flags (enter existence) of maps at z 89 in fast-travel map room 4; every existence-group Ocean_Map is in layers 3-5. Nothing on the playable layers drains yes (data)

Viewer recipe: for each OCEAN_CHANGE (latest per (pid, index)), water = new & 1; with map.oceans[pid], square = (x0 + idx % sx, y0 + ⌊idx / sx⌋); paint water_rgb where that bit becomes 1 and the stored cell height is below zw.

Ice gates (Freezer)

Rule Function Confirmed
All 757 overworld freezers are one square (meshes GAME_OVER_Freezer_A/B, scale 1; 3 with z scale 0.6). No judge_unlock_level, no Judge entity on the overworld data yes
other unlockers on the overworld are all Signs (23 ids, 17 signs). Sign satisfied: unlock_freezers_with_rank r != 0 → r == 0xFF or r <= rank; else tower_id → that Tower activated; else gold_room_connection_unlocker → world+0x63CC >= 4. rank_satisfied (+914) latches simulate pass 0x140330EC0 @L4823 yes
Opened = pid in Status list A, or the gate is satisfied now (update_unlocks runs at every overworld load). Sample: all 95 in A have a satisfied gate; 1 satisfied gate is not in A yet (#60-5999 at (143, 113), unlocked by fire_and_flip, solved right before the save, next to the hero). No Freezer CHANGE in the history update_unlocks 0x1402DBC80, reopen_saved_freezers 0x1402C5E00 yes

Teleporters

Rule Function Confirmed
teleporter_type MINOR 0 / MAJOR 1. 322 MAJOR (161 world pads, 161 twins) and 102 MINOR: each world pad (layers 0-2) is paired by target_id with a twin in a fast-travel map room (layers 3-5, mostly existence-group members, enabled_t_target 0). Stepping on a pad discovers it and its twin: the twin's CHANGE sets entity_flags (existence), enabled_t_target 1 and discovered data + history yes (sample: 47 discovered, all MAJOR)
discovered = latest CHANGE, default = data (only #57-5182, the crossroads twin, is discovered in data). enabled = latest enabled_t_target CHANGE > 0, else data yes (sample)
apply_overworld_discovered_teleporters runs only when load_level_history_state resets the overworld world state (level_data_version newer than the save's): it reloads the old state and keeps MAJOR pads that were enabled and discovered (also opening a Freezer in the same cell and clearing their fog region). For the viewer the history is the only source 0x1402E7400, load_level_history_state 0x140284AB0 yes
MINOR #91-69793 moves along y (421 position CHANGEs): a moving pad, not state yes (data)
Gold rooms: the 6 ground Towers' teleporter_id are MINOR pads (disabled) next to each tower; their targets are the 6 gold_room_endpoint pads in the gold hub around (-15, 147) (wrapping to y -2). Tower activation enables the pad (activate_tower), so unlocked = tower activated, visited = endpoint discovered; the gold_room_connection_unlocker sign counts discovered endpoints. Each tower has its worlds_collide level (GOLD_* achievement) beside it activate_tower 0x1402487A0, data yes (data); visited flag on a MINOR pad not seen in the sample

Treasures

Treasure_Display i (treasure_index 0..5, plinths at (78..82, 52..56, 0)) shows acquired iff popcount(Status treasure_flags) > i (§6). Index 0 is the type default (missing from the data).

Dragon fire and PROTECTION

A hero with the PROTECTION skill stands in any cell a dragon's fire or a goblin attacks, over water or air: the game holds it there; nothing carries or pushes it. The fire ends at the hero.

Rule Function Confirmed
Fire comes from Monsters with constant_flags DRAGON (2); a GOBLIN (1) attacks next to it breath/attack pass 0x1403B9070 yes
Direction: the dragon's local +x axis rounded to a grid axis (±0.5), i.e. theta_target 0 E, 90 N, 180 W, -90 S 0x1403B9070 yes; 20 of 20 PROTECTION records in the default history: hero = dragon + facing × emitted_fire_length
The trace steps one cell at a time at the dragon's z (world wrapping, no length limit, over water). It stops at a solid voxel (0x14023BEA0), a frozen Freezer, a closed Gate, a Tree or Sign, its own dragon, a Wardrobe door (0x1402847D0). It hits and ends in the cell of a Gem, Guy, Rock, Promesst_Light, Monster, Friendly_Dragon or Rover. A Mirror turns it. Fences do not stop it trace 0x14035C050 yes
emitted_fire_length = the summed segment lengths; the hits go to 0x140221040: a Guy with pre_protected gets protected (+ its turn group); other Guys and Monsters hit are killed (the 6 non-hero records are two dragons killing each other at (72, 126) / (81, 126)) 0x1403B9070, 0x140221040 yes
pre_protected = the Guy has PROTECTION (16) in skill_flags, or stands within 1.1 of such a Guy; it and protected are cleared and recomputed every turn (runtime Guy fields, not saved) 0x1402DD840 yes
A protected Guy with no other support is supported anywhere (supported_at fallback) 0x1402E63A0 yes (water traced; air inferred from the same fallback)
A move into water is also allowed when the target is attacked (a goblin within 1.1, or on a dragon's traced segment) and the hero has PROTECTION, so a PROTECTION hero steps from the shore straight into the fire 0x1402E5000 → 0x1402DCC20 yes
PROTECTION comes from overworld Wardrobes (22 hold it). A Wardrobe door is the edge its +x axis points to: leaving through it sets skill_flags = (skill & ~SINGLE_PUSH) | wardrobe skill; entering through it resets to QUEEN | SINGLE_PUSH (258); a skill-less (inventory) Wardrobe leaves the skill alone 0x14035DAC0, wardrobeReset 0x1402829B0 yes; history: 272→258 next to the Wardrobe at (91, 148)
Some teleports also reset the skill 0x1402A8F60 (when Settings+1069 = 2) trigger not traced; one reset in the default history coincides with a MAJOR teleport
Dragon state changes in the save: position, theta_target, dead_t (dead dragons do not breathe), existence, CREATION records data

Reachability (model, not the game's code)

The model is implemented only in the viewer (walker() and reachability() in src/walk.js).

Support (supported_at, sub_1402E63A0): solid voxel below; in the target cell a Lily, Elevator_Platform or a Fence with blocking and is_floor (a floor at the bottom of its own cell, not solid); in the cell below a Rock/Sign/Tree/Gem/Rover/Judge/Promesst_Light/Treasure_Display, closed Gate, frozen Freezer, Monster or Guy; a Promesst beam under the position; a protected hero anywhere. Without water at the square, a position in a non-sky layer with z ≤ the layer base (0 on ground) is supported (implicit floor). The model uses solid voxels, floor fences, lilies, rocks and the implicit floor (ground layer only; adding the other entities below opened leaks).

  • A cell (i, j, z) is open if it is not solid, not a closed Freezer or Gate, and is supported.
  • Moves: to a 4-neighbour at the same z if open; otherwise, if the neighbour cell is not solid, a closed Freezer or a closed Gate, drop to the first open cell below (up to 12; stop at a blocked cell; a floor fence holds the hero). No climbing (1-step climbing reaches 299 extra levels and goes over walls).
  • Elevator frames: a cell in a frame column within 1 of the frame's z reaches the open cell at, above or below the other frame. MINOR pad (enabled) reaches its target. A discovered and enabled MAJOR pad reaches every other one.
  • Existence-group Wall_Sets are solid only while they exist (latest entity_flags CHANGE, bit CURRENTLY_EXISTS 1<<26). Rocks support at their latest position.
  • Dragon fire (above): the walk has two layers, any skill and PROTECTION. Attacked cells: each living dragon (latest position, theta_target, dead_t = 0, existing) fires from its cell along its facing at its z until a solid voxel or rock, a closed Gate or ice gate, a Tree/Sign/Gem/Rover/Friendly_Dragon/Mirror/Promesst_Light cell, another Monster or a Wardrobe door; each living goblin attacks its 4 neighbours. In the PROTECTION layer a move (or drop) into an attacked cell is always allowed and the hero stands there. Leaving through a PROTECTION Wardrobe door enters that layer; leaving through another skill's door, entering any skill door, or a teleport leaves it. The latest Guy skill_flags sets the current hero's layer; a kept hero position seeds the PROTECTION layer when the Guy's latest skill_flags change at or before that record holds PROTECTION.
  • Not modelled: Mirrors turning dragon fire (the fire stops there; 5 overworld rays end at a mirror, none on the sea puzzles), a dragon stuck inside a wall (it breathes no fire), the teleports that keep the skill, Monster CREATION records (a Monster counts at its latest CHANGE position), the TRANSMUTE hero swapping places with a dragon (SWAP_WITH_DRAGON), Promesst beams and powers, magic-mirror paths, skipping stones, Lily pads drifting on rivers (a Lily counts only at its data and latest positions, below), monster rooms (monster gates keep their stored open), the implicit floor of the fast-travel map rooms, and walls the hero cannot see over. Without the dragon fire the walk left 41 of 470 default-save hero positions outside the fill: 19 protected hero (Guy skill_flags PROTECTION | QUEEN) in deep water under dragon fire (the record has a Monster emitted_fire_length aimed at the Guy: the sea at y 145-148, x 97-135, and (135-142, 105-109)) or next to the dragon at (103, 130); 10 Promesst (the red-beam pocket at (21-29, 63-73) behind the rank-1 sign gate at (25, 65), whose cell holds the ruby_beamcaster Promesst_Light, and the 5-cell walled pocket at (51-52, 68-70) of the green_beam_barrier Rocks); 8 skipping stones (shallow cells in the sea at (90-97, 32-55)); 4 on islets of the metamirror sea at (146-156, 60-61). The sign gate is closed in that save (Status rank 0 < 1, not in list A), as the model has it. Gates, Fences and water are blockers (below). From the hero alone the fill reaches 4 cells (the pocket by fire_and_flip, entered through a mirror jump): 2 of 147 entered levels.
  • So the recipe seeds the fill with every place the save shows the hero has been: the hero position of every kept undo record (the Promesst, skipping-stone and mirror places above), every Level_Entry whose level has Status state, every activated Area_Pin, every discovered teleporter. Every seed lands on the open cell at, above or below it, except an Area_Pin: it is touched, not stood on, so it lands only at or below (the new-game pin #63002081 at (76, 75, 0) is inside a wall; landing on its top let the walk roam the wall tops and drop into the heroes1 trench from the north). A record position that lands more than 0.1 under a water surface (the protected hero in the sea) is no seed: the fill would leave it on to any shore. Samples: BardOceanStoneSmash 1274 cells, 18 levels reachable, 0 of 160 hero positions outside; live default 4170 / 171 / 2 of 543, the TRANSMUTE hero at (97, 132) and (102, 130) beside the dragon at (103, 130) (without the dragon fire: 4073 / 171 / 19; seeding only the latest hero position: 4008 / 171 / 25, before 3912 / 171 / 41). Level roles do not change.
  • A level the fill does not reach is behind the closed ice gates on the edge of its own pocket (a fill from the level that stays out of the reached cells), e.g. pull_skip_twins_v3 on its island behind #86-7702 at (137, 146). "Leads to" of a gate counts only gates on the edge of the reached cells (the levels a fill through that gate alone reaches, plus the pockets it borders); the gates around farther pockets only name what a level sits behind.
  • The viewer names the mechanics the model leaves out or models only in part around a pocket (world.mechs, FORMAT.md): a DRAGON Monster (its 3 × 3 × 3 cells and its fire cells), a Promesst_Light or a beam blocker Rock, a Mirror or MIRRORABLE Rock, a SKIPPABLE Rock (3 × 3 × 3 cells around each). A pocket has one when a marked cell is in it or next to one of its cells. A level with no closed gate and no such mechanic has no known route. A pocket fill starts without PROTECTION, but a PROTECTION Wardrobe inside the pocket takes it over the fire.

Blockers from the game's move check (sub_1402A70D0, sub_14021B040): Fence edge blocking needs blocking and an axis-aligned rotation: it closes one edge of its cell, the side its rotated +x axis points to (a move out of the cell with axis · dir > 0.5 or into it with < -0.5 is refused); a Fence with blocking and is_floor is a floor in its own cell and blocks vertical moves through it; is_solid_wall has no game-logic reader (sub_1402A70D0, sub_1402E63A0). Solid-like entities (sub_14021B040): Sign, Treasure_Display, Rover, Tree, Promesst_Light, Gem, Rock, Judge always; Gate while open = 0; Freezer while unfrozen = 0. A Gate blocks its cell while closed. If any existing Switch shares the gate's flavor: open = (all of them pressed) xor notted; otherwise the stored open. Monster gates: open = (no living Monster in the gate's screen room, Gate.room_index) xor notted (sub_140330160, sub_140322590). pressed and the stored open are the latest CHANGE, else the data (the first CHANGE's old value of the two notted switch gates changed in the default save is 1 although the data says 0). A move into a water square (any Ocean_Map incl. PLAYER_BLOCKING_ONLY, same ground layer, map z ≤ z+2.1, bit 0) is allowed only if support exists at some z′ in [map z − 0.1, z] (sub_1402E5000); sink is not used. Exceptions: a may_jump_into_water Marker at the hero allowing that direction, a Promesst_Light below, or a protected hero in an attacked cell. In a water square the implicit floor applies only below the layer base (z < 0 on ground). The model takes the Marker box (position ± box_extents) as the cells it covers and lets a jump fall on to the first supported cell. A Lily is standable in its own cell, at its data position and its latest position. Sample saves: see the seed recipe above.

Level roles (viewer, not the game)

A level is Optional only when it is provably not needed for any goal (it then counts only for Completionist); every other level is Required, including the ones whose use the model cannot see. Goals are the tower levels that are not dropdowns and the levels with an achievement; they are always Required. Dropdowns are not goals (they only give a tower's gold), so a dropdown is Optional unless something Required needs it like any other level.

Needs, starting from the goals:

  • a level needs its unlocked_by levels (none for a level enabled in the level data). With an OR gate every option is needed, but only as "one of", and so is everything those options need in turn;
  • a level or ice gate needs the ice gates it sits behind. Behind several gates means any one of them, so they are needed as "one of";
  • an ice gate needs its unlocked_by levels (Sign rules are not levels), and an ice gate is needed by itself unless it is provably a shortcut (below).

So a non-goal level is Optional when everything it opens is optional: every level that lists it in unlocked_by is Optional and every ice gate that lists it is a shortcut with nothing Required behind it. A level that opens nothing is Optional.

The viewer computes the roles itself (staticRoles() in src/roles.js), once per page load after the first draw; until then levels and gates show no role.

"Provable" means the walk model above shows it, run on the level data: Gates, Fences, existence groups and Lily pads at their data state, seeded with the hero and the Area_Pins of a new game (data/fog.json start), every ice gate closed and level unlocks ignored. Then the closed gates on the edge of the reached cells open together, round after round. A level or gate first reached in a round is behind the gates of that round whose opening alone reaches it (all of that round's gates when only opening them together does). An ice gate is a shortcut when the walk reaches it, opening it reaches no cell that is not reached without it (in an earlier round, or in the same round through another gate), and the walk goes through it: from a cell that reached it, across its cell, to a reached cell on the opposite side. Every other gate is needed: one the walk never reaches (its far side is unknown), one that alone opens new cells (they may lead on through moves the walk lacks, even when no level is there; "one of" when several gates of the round open the same cells), and a dead end, pit or step up whose far side the walk never sees.

Limits: the walk lacks mirrors, skipping stones, climbing and drifting lilies, so it reaches few levels and most gates come out needed; that errs towards Required. It can err towards Optional only where it walks somewhere the game does not allow (it was fitted to the sample saves) or misses a second route the game does not have. Example: the three ice blocks opened by heroes1_4, heroes1_7_v2 and heroes1_10_inv_v4_b at (80, 86..88) chain the start area to the north part of heroes1 (rounds 1-3); with the pin seed on a wall top the walk entered that trench from both ends and called them shortcuts. Level data: 102 of 920 levels reached in 9 rounds; 67 of 757 gates reached, 26 shortcuts, 735 needed; 766 Required (27 of them one of several), 154 Optional; 2 of 12 dropdowns Required.

Cross-check with data/levels.json (data, not code)

  • Overworld: 920 Level_Entry. All 477 without unlocked_by_id* have enabled_t_target=1 in the data (open from the start); 439 of the 443 gated ones have the default 0, and 4 (falloff_dragons, skybridge, zap_bridge, detachment) have enabled_t_target=1, so they are open from the start too: update_unlocks evaluates a gate only while enabled_t_target == 0, and entering checks enabled_t, copied from enabled_t_target at load. No entry has solved_t_target set, none is broken or disfavored. 12 are dropdown, 161 have tower_flags. So COMPLETIONIST = all 920 overworld level names SOLVED. The 26 Level_Entries in intro and 1 in end3_promesst_red do not count.

  • achievement carriers: GOLD_HM worlds_collide_heroes_and_mirrors_v3, GOLD_HS worlds_collide_heroes_and_stones, GOLD_HP worlds_collide_heroes_and_promesst_v4, GOLD_MS worlds_collide_mirrors_and_stones_v2, GOLD_MP worlds_collide_alt, GOLD_SP worlds_collide_ps, HEROES heroes3_21_condensed, MIRRORS mundane_mirror, SKIPPING skipping_shores_v3, PROMISE promesst2_streamlined, ENDGAME1 end1, ENDGAME2 all_together_now, ENDGAME3 end3_pipeline_aesthetics.

  • demo_phase set on 106 entries; not read by the game code (see §1).

  • 920 level_names, all unique. In the sample save 147 of them have state, 144 SOLVED, while levels_solved=143 (the count is refreshed only on overworld passes).

FORMAT.md corrections

  • "u32 flag set (its popcount gates tower unlocks)" is wrong. It is treasure_flags (Skipping Stones treasures; popcount drives Treasure_Display). Towers use levels_required against per-level SOLVED flags.
  • Progress stage is rank 0..5, not 0..4. The s32×3 are levels_solved, tower_levels_solved and towers_activated.
  • Pid lists A and B hold opened Freezers (overworld / other levels). The string table is legacy.

Open

  • Mapping Level_Entry → Area_Pin / local area (per-section grouping) was not traced in code. It has to come from geometry (Area_Pin position, map_label_marker_id) or from the level data.

  • Names of the stat counters +244..+280 come from their increment sites; they are only written and saved, never read.

  • END1/END2/END3_STATUS sign hooks and the campaign limit value were not traced.