#include "tracker_state.h" #include #include #include #include #include #include #include "ap_state.h" #include "game_data.h" namespace { struct TrackerState { std::map reachability; std::mutex reachability_mutex; }; enum Decision { kYes, kNo, kMaybe }; TrackerState& GetState() { static TrackerState* instance = new TrackerState(); return *instance; } class StateCalculator; struct StateCalculatorOptions { std::string start = "Menu"; bool pilgrimage = false; StateCalculator* parent = nullptr; }; class StateCalculator { public: StateCalculator() = default; explicit StateCalculator(StateCalculatorOptions options) : options_(options) {} void Calculate() { std::list panel_boundary; std::list flood_boundary; flood_boundary.push_back( {.destination_room = GD_GetRoomByName(options_.start)}); bool reachable_changed = true; while (reachable_changed) { reachable_changed = false; std::list new_panel_boundary; for (int panel_id : panel_boundary) { if (solveable_panels_.count(panel_id)) { continue; } Decision panel_reachable = IsPanelReachable(panel_id); if (panel_reachable == kYes) { solveable_panels_.insert(panel_id); reachable_changed = true; } else if (panel_reachable == kMaybe) { new_panel_boundary.push_back(panel_id); } } std::list new_boundary; for (const Exit& room_exit : flood_boundary) { if (reachable_rooms_.count(room_exit.destination_room)) { continue; } bool valid_transition = false; Decision exit_usable = IsExitUsable(room_exit); if (exit_usable == kYes) { valid_transition = true; } else if (exit_usable == kMaybe) { new_boundary.push_back(room_exit); } if (valid_transition) { reachable_rooms_.insert(room_exit.destination_room); reachable_changed = true; const Room& room_obj = GD_GetRoom(room_exit.destination_room); for (const Exit& out_edge : room_obj.exits) { if (out_edge.type == EntranceType::kPainting && AP_IsPaintingShuffle()) { continue; } if (out_edge.type == EntranceType::kSunwarp && AP_IsSunwarpShuffle()) { continue; } new_boundary.push_back(out_edge); } if (AP_IsPaintingShuffle()) { for (const PaintingExit& out_edge : room_obj.paintings) { if (AP_GetPaintingMapping().count(out_edge.id)) { Exit painting_exit; painting_exit.destination_room = GD_GetRoomForPainting( AP_GetPaintingMapping().at(out_edge.id)); painting_exit.door = out_edge.door; new_boundary.push_back(painting_exit); } } } if (AP_IsSunwarpShuffle()) { for (int index : room_obj.sunwarps) { if (AP_GetSunwarpMapping().count(index)) { const SunwarpMapping& sm = AP_GetSunwarpMapping().at(index); Exit sunwarp_exit; sunwarp_exit.destination_room = GD_GetRoomForSunwarp(sm.exit_index); sunwarp_exit.door = GD_GetSunwarpDoors().at(sm.dots - 1); new_boundary.push_back(sunwarp_exit); } } } if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") { new_boundary.push_back( {.destination_room = GD_GetRoomByName("Outside The Undeterred"), .type = EntranceType::kPainting}); } if (AP_IsPilgrimageEnabled()) { if (room_obj.name == "Hub Room") { new_boundary.push_back( {.destination_room = GD_GetRoomByName("Pilgrim Antechamber"), .type = EntranceType::kPilgrimage}); } } else { if (room_obj.name == "Starting Room") { new_boundary.push_back( {.destination_room = GD_GetRoomByName("Pilgrim Antechamber"), .door = GD_GetDoorByName("Pilgrim Antechamber - Sun Painting"), .type = EntranceType::kPainting}); } } for (int panel_id : room_obj.panels) { new_panel_boundary.push_back(panel_id); } } } flood_boundary = new_boundary; panel_boundary = new_panel_boundary; } } const std::set& GetReachableRooms() const { return reachable_rooms_; } const std::map& GetDoorDecisions() const { return door_decisions_; } const std::set& GetSolveablePanels() const { return solveable_panels_; } private: Decision IsNonGroupedDoorReachable(const Door& door_obj) { bool has_item = AP_HasItem(door_obj.ap_item_id); if (!has_item) { for (const ProgressiveRequirement& prog_req : door_obj.progressives) { if (AP_HasItem(prog_req.ap_item_id, prog_req.quantity)) { has_item = true; break; } } } return has_item ? kYes : kNo; } Decision IsDoorReachable_Helper(int door_id) { const Door& door_obj = GD_GetDoor(door_id); if (!AP_IsPilgrimageEnabled() && door_obj.type == DoorType::kSunPainting) { return AP_HasItem(door_obj.ap_item_id) ? kYes : kNo; } else if (door_obj.type == DoorType::kSunwarp) { switch (AP_GetSunwarpAccess()) { case kSUNWARP_ACCESS_NORMAL: return kYes; case kSUNWARP_ACCESS_DISABLED: return kNo; case kSUNWARP_ACCESS_UNLOCK: return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo; case kSUNWARP_ACCESS_INDIVIDUAL: case kSUNWARP_ACCESS_PROGRESSIVE: return IsNonGroupedDoorReachable(door_obj); } } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) { if (!reachable_rooms_.count(door_obj.room)) { return kMaybe; } for (int panel_id : door_obj.panels) { if (!solveable_panels_.count(panel_id)) { return kMaybe; } } return kYes; } else if (AP_GetDoorShuffleMode() == kDOORS_MODE && AP_AreDoorsGrouped() && !door_obj.group_name.empty()) { return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo; } else { return IsNonGroupedDoorReachable(door_obj); } } Decision IsDoorReachable(int door_id) { if (options_.parent) { return options_.parent->IsDoorReachable(door_id); } if (door_decisions_.count(door_id)) { return door_decisions_.at(door_id); } Decision result = IsDoorReachable_Helper(door_id); if (result != kMaybe) { door_decisions_[door_id] = result; } return result; } Decision IsPanelReachable(int panel_id) { const Panel& panel_obj = GD_GetPanel(panel_id); if (!reachable_rooms_.count(panel_obj.room)) { return kMaybe; } if (panel_obj.name == "THE MASTER") { int achievements_accessible = 0; for (int achieve_id : GD_GetAchievementPanels()) { if (solveable_panels_.count(achieve_id)) { achievements_accessible++; if (achievements_accessible >= AP_GetMasteryRequirement()) { break; } } } return (achievements_accessible >= AP_GetMasteryRequirement()) ? kYes : kMaybe; } if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") && AP_GetLevel2Requirement() > 1) { int counting_panels_accessible = 0; for (int solved_panel_id : solveable_panels_) { const Panel& solved_panel = GD_GetPanel(solved_panel_id); if (!solved_panel.non_counting) { counting_panels_accessible++; } } return (counting_panels_accessible >= AP_GetLevel2Requirement() - 1) ? kYes : kMaybe; } for (int room_id : panel_obj.required_rooms) { if (!reachable_rooms_.count(room_id)) { return kMaybe; } } for (int door_id : panel_obj.required_doors) { Decision door_reachable = IsDoorReachable(door_id); if (door_reachable == kNo) { const Door& door_obj = GD_GetDoor(door_id); return (door_obj.is_event || AP_GetDoorShuffleMode() == kNO_DOORS) ? kMaybe : kNo; } else if (door_reachable == kMaybe) { return kMaybe; } } for (int panel_id : panel_obj.required_panels) { if (!solveable_panels_.count(panel_id)) { return kMaybe; } } if (AP_IsColorShuffle()) { for (LingoColor color : panel_obj.colors) { if (!AP_HasItem(GD_GetItemIdForColor(color))) { return kNo; } } } if (panel_obj.panel_door != -1 && AP_GetDoorShuffleMode() == kPANELS_MODE) { const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_obj.panel_door); if (AP_AreDoorsGrouped() && panel_door_obj.group_ap_item_id != -1) { return AP_HasItem(panel_door_obj.group_ap_item_id) ? kYes : kNo; } else { bool has_item = AP_HasItem(panel_door_obj.ap_item_id); if (!has_item) { for (const ProgressiveRequirement& prog_req : panel_door_obj.progressives) { if (AP_HasItem(prog_req.ap_item_id, prog_req.quantity)) { has_item = true; break; } } } return has_item ? kYes : kNo; } } return kYes; } Decision IsExitUsable(const Exit& room_exit) { if (room_exit.type == EntranceType::kPilgrimage) { if (options_.pilgrimage || !AP_IsPilgrimageEnabled()) { return kNo; } if (AP_GetSunwarpAccess() != kSUNWARP_ACCESS_NORMAL) { for (int door_id : GD_GetSunwarpDoors()) { Decision sub_decision = IsDoorReachable(door_id); if (sub_decision != kYes) { return sub_decision; } } } static const std::vector> pilgrimage_pairs = { {"Crossroads", "Hot Crusts Area"}, // {"Orange Tower Third Floor", "Orange Tower Third Floor"}, {"Outside The Initiated", "Orange Tower First Floor"}, {"Outside The Undeterred", "Orange Tower Fourth Floor"}, {"Color Hunt", "Outside The Agreeable"}}; for (const auto& [from_room, to_room] : pilgrimage_pairs) { StateCalculator pilgrimage_calculator( {.start = from_room, .pilgrimage = true, .parent = this}); pilgrimage_calculator.Calculate(); if (!pilgrimage_calculator.GetReachableRooms().count( GD_GetRoomByName(to_room))) { return kMaybe; } } return kYes; } if (options_.pilgrimage) { if (room_exit.type == EntranceType::kWarp || room_exit.type == EntranceType::kSunwarp) { return kNo; } if (room_exit.type == EntranceType::kCrossroadsRoofAccess && !AP_DoesPilgrimageAllowRoofAccess()) { return kNo; } if (room_exit.type == EntranceType::kPainting && !AP_DoesPilgrimageAllowPaintings()) { return kNo; } } if (room_exit.type == EntranceType::kSunwarp) { if (AP_GetSunwarpAccess() == kSUNWARP_ACCESS_NORMAL) { return kYes; } else if (AP_GetSunwarpAccess() == kSUNWARP_ACCESS_DISABLED) { return kNo; } } if (room_exit.door.has_value()) { return IsDoorReachable(*room_exit.door); } return kYes; } StateCalculatorOptions options_; std::set reachable_rooms_; std::map door_decisions_; std::set solveable_panels_; }; } // namespace void RecalculateReachability() { StateCalculator state_calculator; state_calculator.Calculate(); const std::set& reachable_rooms = state_calculator.GetReachableRooms(); const std::set& solveable_panels = state_calculator.GetSolveablePanels(); std::map new_reachability; for (const MapArea& map_area : GD_GetMapAreas()) { for (size_t section_id = 0; section_id < map_area.locations.size(); section_id++) { const Location& location_section = map_area.locations.at(section_id); bool reachable = reachable_rooms.count(location_section.room); if (reachable) { for (int panel_id : location_section.panels) { reachable &= (solveable_panels.count(panel_id) == 1); } } new_reachability[location_section.ap_location_id] = reachable; } } { std::lock_guard reachability_guard(GetState().reachability_mutex); std::swap(GetState().reachability, new_reachability); } } bool IsLocationReachable(int location_id) { std::lock_guard reachability_guard(GetState().reachability_mutex); if (GetState().reachability.count(location_id)) { return GetState().reachability.at(location_id); } else { return false; } }