about summary refs log tree commit diff stats
path: root/src/tracker_frame.cpp
blob: d64e0d327fda034bfe6e8e39ae5cb06e19f7d09d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#include "tracker_frame.h"

#include <wx/choicebk.h>
#include <wx/webrequest.h>

#include <nlohmann/json.hpp>
#include <sstream>

#include "achievements_pane.h"
#include "ap_state.h"
#include "connection_dialog.h"
#include "settings_dialog.h"
#include "tracker_config.h"
#include "tracker_panel.h"
#include "version.h"

enum TrackerFrameIds {
  ID_CONNECT = 1,
  ID_CHECK_FOR_UPDATES = 2,
  ID_SETTINGS = 3
};

wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent);
wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);

TrackerFrame::TrackerFrame()
    : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition,
              wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxFULL_REPAINT_ON_RESIZE) {
  ::wxInitAllImageHandlers();

  AP_SetTrackerFrame(this);

  wxMenu *menuFile = new wxMenu();
  menuFile->Append(ID_CONNECT, "&Connect");
  menuFile->Append(ID_SETTINGS, "&Settings");
  menuFile->Append(wxID_EXIT);

  wxMenu *menuHelp = new wxMenu();
  menuHelp->Append(wxID_ABOUT);
  menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates");

  wxMenuBar *menuBar = new wxMenuBar();
  menuBar->Append(menuFile, "&File");
  menuBar->Append(menuHelp, "&Help");

  SetMenuBar(menuBar);

  CreateStatusBar();
  SetStatusText("Not connected to Archipelago.");

  Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT);
  Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT);
  Bind(wxEVT_MENU, &TrackerFrame::OnConnect, this, ID_CONNECT);
  Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS);
  Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this,
       ID_CHECK_FOR_UPDATES);
  Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
  Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);

  wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY);
  achievements_pane_ = new AchievementsPane(this);
  choicebook->AddPage(achievements_pane_, "Achievements");

  tracker_panel_ = new TrackerPanel(this);

  wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL);
  top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1));
  top_sizer->Add(tracker_panel_, wxSizerFlags().Expand().Proportion(3));

  SetSizerAndFit(top_sizer);
  SetSize(1280, 728);

  if (!GetTrackerConfig().asked_to_check_for_updates) {
    GetTrackerConfig().asked_to_check_for_updates = true;

    if (wxMessageBox(
            "Check for updates automatically when the tracker is opened?",
            "Lingo AP Tracker", wxYES_NO) == wxYES) {
      GetTrackerConfig().should_check_for_updates = true;
    } else {
      GetTrackerConfig().should_check_for_updates = false;
    }

    GetTrackerConfig().Save();
  }

  if (GetTrackerConfig().should_check_for_updates) {
    CheckForUpdates(/*manual=*/false);
  }
}

void TrackerFrame::SetStatusMessage(std::string message) {
  wxCommandEvent *event = new wxCommandEvent(STATUS_CHANGED);
  event->SetString(message.c_str());

  QueueEvent(event);
}

void TrackerFrame::UpdateIndicators() {
  QueueEvent(new wxCommandEvent(STATE_CHANGED));
}

void TrackerFrame::OnAbout(wxCommandEvent &event) {
  std::ostringstream message_text;
  message_text << "Lingo Archipelago Tracker " << kTrackerVersion
               << " by hatkirby";

  wxMessageBox(message_text.str(), "About lingo-ap-tracker",
               wxOK | wxICON_INFORMATION);
}

void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); }

void TrackerFrame::OnConnect(wxCommandEvent &event) {
  ConnectionDialog dlg;

  if (dlg.ShowModal() == wxID_OK) {
    GetTrackerConfig().connection_details.ap_server = dlg.GetServerValue();
    GetTrackerConfig().connection_details.ap_player = dlg.GetPlayerValue();
    GetTrackerConfig().connection_details.ap_password = dlg.GetPasswordValue();

    std::deque<ConnectionDetails> new_history;
    new_history.push_back(GetTrackerConfig().connection_details);

    for (const ConnectionDetails& details : GetTrackerConfig().connection_history) {
      if (details != GetTrackerConfig().connection_details) {
        new_history.push_back(details);
      }
    }

    while (new_history.size() > 5) {
      new_history.pop_back();
    }

    GetTrackerConfig().connection_history = std::move(new_history);
    GetTrackerConfig().Save();

    AP_Connect(dlg.GetServerValue(), dlg.GetPlayerValue(),
               dlg.GetPasswordValue());
  }
}

void TrackerFrame::OnSettings(wxCommandEvent &event) {
  SettingsDialog dlg;

  if (dlg.ShowModal() == wxID_OK) {
    GetTrackerConfig().should_check_for_updates =
        dlg.GetShouldCheckForUpdates();
    GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas();
    GetTrackerConfig().show_hunt_panels = dlg.GetShowHuntPanels();
    GetTrackerConfig().Save();

    UpdateIndicators();
  }
}

void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) {
  CheckForUpdates(/*manual=*/true);
}

void TrackerFrame::OnStateChanged(wxCommandEvent &event) {
  tracker_panel_->UpdateIndicators();
  achievements_pane_->UpdateIndicators();
  Refresh();
}

void TrackerFrame::OnStatusChanged(wxCommandEvent &event) {
  SetStatusText(event.GetString());
}

void TrackerFrame::CheckForUpdates(bool manual) {
  wxWebRequest request = wxWebSession::GetDefault().CreateRequest(
      this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION");

  if (!request.IsOk()) {
    if (manual) {
      wxMessageBox("Could not check for updates.", "Error",
                   wxOK | wxICON_ERROR);
    } else {
      SetStatusText("Could not check for updates.");
    }

    return;
  }

  Bind(wxEVT_WEBREQUEST_STATE, [this, manual](wxWebRequestEvent &evt) {
    if (evt.GetState() == wxWebRequest::State_Completed) {
      std::string response = evt.GetResponse().AsString().ToStdString();

      Version latest_version(response);
      if (kTrackerVersion < latest_version) {
        std::ostringstream message_text;
        message_text << "There is a newer version of Lingo AP Tracker "
                        "available. You have "
                     << kTrackerVersion << ", and the latest version is "
                     << latest_version << ". Would you like to update?";

        if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) ==
            wxYES) {
          wxLaunchDefaultBrowser(
              "https://code.fourisland.com/lingo-ap-tracker/about/"
              "CHANGELOG.md");
        }
      } else if (manual) {
        wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker",
                     wxOK);
      }
    } else if (evt.GetState() == wxWebRequest::State_Failed) {
      if (manual) {
        wxMessageBox("Could not check for updates.", "Error",
                     wxOK | wxICON_ERROR);
      } else {
        SetStatusText("Could not check for updates.");
      }
    }
  });

  request.Start();
}
class="kt">bool>> door_reports; }; enum Decision { kYes, kNo, kMaybe }; TrackerState& GetState() { static TrackerState* instance = new TrackerState(); return *instance; } class StateCalculator; struct StateCalculatorOptions { int start; bool pilgrimage = false; StateCalculator* parent = nullptr; }; class StateCalculator { public: StateCalculator() = default; explicit StateCalculator(StateCalculatorOptions options) : options_(options) {} void Calculate() { std::list<int> panel_boundary; std::list<Exit> flood_boundary; flood_boundary.push_back({.destination_room = options_.start}); bool reachable_changed = true; while (reachable_changed) { reachable_changed = false; std::list<int> 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<Exit> 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) && AP_GetCheckedPaintings().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; } // Now that we know the full reachable area, let's make sure all doors are // evaluated. for (const Door& door : GD_GetDoors()) { int discard = IsDoorReachable(door.id); } } const std::set<int>& GetReachableRooms() const { return reachable_rooms_; } const std::map<int, Decision>& GetDoorDecisions() const { return door_decisions_; } const std::set<int>& GetSolveablePanels() const { return solveable_panels_; } const std::map<int, std::map<std::string, bool>>& GetDoorReports() const { return door_report_; } 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 AreRequirementsSatisfied( const Requirements& reqs, std::map<std::string, bool>* report = nullptr) { if (reqs.disabled) { return kNo; } Decision final_decision = kYes; for (int door_id : reqs.doors) { const Door& door_obj = GD_GetDoor(door_id); Decision decision = IsNonGroupedDoorReachable(door_obj); if (report) { (*report)[door_obj.item_name] = (decision == kYes); } if (decision != kYes) { final_decision = decision; } } for (int item_id : reqs.items) { bool has_item = AP_HasItem(item_id); if (report) { (*report)[AP_GetItemName(item_id)] = has_item; } if (!has_item) { final_decision = kNo; } } for (int room_id : reqs.rooms) { bool reachable = reachable_rooms_.count(room_id); if (report) { std::string report_name = "Reach \"" + GD_GetRoom(room_id).name + "\""; (*report)[report_name] = reachable; } if (!reachable && final_decision != kNo) { final_decision = kMaybe; } } if (reqs.mastery) { int achievements_accessible = 0; for (int achieve_id : GD_GetAchievementPanels()) { if (solveable_panels_.count(achieve_id)) { achievements_accessible++; if (achievements_accessible >= AP_GetMasteryRequirement()) { break; } } } bool can_mastery = (achievements_accessible >= AP_GetMasteryRequirement()); if (report) { (*report)["Mastery"] = can_mastery; } if (!can_mastery && final_decision != kNo) { final_decision = kMaybe; } } if (reqs.panel_hunt) { 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++; } } bool can_level2 = (counting_panels_accessible >= AP_GetLevel2Requirement() - 1); if (report) { std::string report_name = std::to_string(AP_GetLevel2Requirement()) + " Panels"; (*report)[report_name] = can_level2; } if (!can_level2 && final_decision != kNo) { final_decision = kMaybe; } } return final_decision; } Decision IsDoorReachable_Helper(int door_id) { if (door_report_.count(door_id)) { door_report_[door_id].clear(); } else { door_report_[door_id] = {}; } return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id), &door_report_[door_id]); } 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) { return AreRequirementsSatisfied(GetState().requirements.GetPanel(panel_id)); } 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; } } } std::vector<std::tuple<int, int>> pilgrimage_pairs; if (AP_IsSunwarpShuffle()) { pilgrimage_pairs = std::vector<std::tuple<int, int>>(5); for (const auto& [start_index, mapping] : AP_GetSunwarpMapping()) { if (mapping.dots > 1) { std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index; } if (mapping.dots < 6) { std::get<0>(pilgrimage_pairs[mapping.dots - 1]) = mapping.exit_index; } } } else { pilgrimage_pairs = {{6, 1}, {8, 3}, {9, 4}, {10, 5}}; } for (const auto& [from_sunwarp, to_sunwarp] : pilgrimage_pairs) { StateCalculator pilgrimage_calculator( {.start = GD_GetRoomForSunwarp(from_sunwarp), .pilgrimage = true, .parent = this}); pilgrimage_calculator.Calculate(); if (!pilgrimage_calculator.GetReachableRooms().count( GD_GetRoomForSunwarp(to_sunwarp))) { 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<int> reachable_rooms_; std::map<int, Decision> door_decisions_; std::set<int> solveable_panels_; std::map<int, std::map<std::string, bool>> door_report_; }; } // namespace void ResetReachabilityRequirements() { std::lock_guard reachability_guard(GetState().reachability_mutex); GetState().requirements.Reset(); } void RecalculateReachability() { StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); state_calculator.Calculate(); const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); const std::set<int>& solveable_panels = state_calculator.GetSolveablePanels(); std::map<int, bool> 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::set<int> new_reachable_doors; for (const auto& [door_id, decision] : state_calculator.GetDoorDecisions()) { if (decision == kYes) { new_reachable_doors.insert(door_id); } } std::map<int, std::map<std::string, bool>> door_reports = state_calculator.GetDoorReports(); { std::lock_guard reachability_guard(GetState().reachability_mutex); std::swap(GetState().reachability, new_reachability); std::swap(GetState().reachable_doors, new_reachable_doors); std::swap(GetState().door_reports, door_reports); } } 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; } } bool IsDoorOpen(int door_id) { std::lock_guard reachability_guard(GetState().reachability_mutex); return GetState().reachable_doors.count(door_id); } const std::map<std::string, bool>& GetDoorRequirements(int door_id) { std::lock_guard reachability_guard(GetState().reachability_mutex); return GetState().door_reports[door_id]; }