diff options
Diffstat (limited to 'src')
50 files changed, 4681 insertions, 965 deletions
| diff --git a/src/achievements_pane.cpp b/src/achievements_pane.cpp index 8ec3727..d23c434 100644 --- a/src/achievements_pane.cpp +++ b/src/achievements_pane.cpp | |||
| @@ -8,23 +8,24 @@ AchievementsPane::AchievementsPane(wxWindow* parent) | |||
| 8 | AppendColumn("Achievement"); | 8 | AppendColumn("Achievement"); |
| 9 | 9 | ||
| 10 | for (int panel_id : GD_GetAchievementPanels()) { | 10 | for (int panel_id : GD_GetAchievementPanels()) { |
| 11 | achievement_names_.push_back(GD_GetPanel(panel_id).achievement_name); | 11 | const Panel& panel = GD_GetPanel(panel_id); |
| 12 | achievements_.emplace_back(panel.achievement_name, panel.solve_index); | ||
| 12 | } | 13 | } |
| 13 | 14 | ||
| 14 | std::sort(std::begin(achievement_names_), std::end(achievement_names_)); | 15 | std::sort(std::begin(achievements_), std::end(achievements_)); |
| 15 | 16 | ||
| 16 | for (int i = 0; i < achievement_names_.size(); i++) { | 17 | for (int i = 0; i < achievements_.size(); i++) { |
| 17 | InsertItem(i, achievement_names_.at(i)); | 18 | InsertItem(i, std::get<0>(achievements_.at(i))); |
| 18 | } | 19 | } |
| 19 | 20 | ||
| 20 | SetColumnWidth(0, wxLIST_AUTOSIZE); | 21 | SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER); |
| 21 | 22 | ||
| 22 | UpdateIndicators(); | 23 | UpdateIndicators(); |
| 23 | } | 24 | } |
| 24 | 25 | ||
| 25 | void AchievementsPane::UpdateIndicators() { | 26 | void AchievementsPane::UpdateIndicators() { |
| 26 | for (int i = 0; i < achievement_names_.size(); i++) { | 27 | for (int i = 0; i < achievements_.size(); i++) { |
| 27 | if (AP_HasAchievement(achievement_names_.at(i))) { | 28 | if (AP_IsPanelSolved(std::get<1>(achievements_.at(i)))) { |
| 28 | SetItemTextColour(i, *wxBLACK); | 29 | SetItemTextColour(i, *wxBLACK); |
| 29 | } else { | 30 | } else { |
| 30 | SetItemTextColour(i, *wxRED); | 31 | SetItemTextColour(i, *wxRED); |
| diff --git a/src/achievements_pane.h b/src/achievements_pane.h index ac88cac..941b5e3 100644 --- a/src/achievements_pane.h +++ b/src/achievements_pane.h | |||
| @@ -9,6 +9,10 @@ | |||
| 9 | 9 | ||
| 10 | #include <wx/listctrl.h> | 10 | #include <wx/listctrl.h> |
| 11 | 11 | ||
| 12 | #include <string> | ||
| 13 | #include <tuple> | ||
| 14 | #include <vector> | ||
| 15 | |||
| 12 | class AchievementsPane : public wxListView { | 16 | class AchievementsPane : public wxListView { |
| 13 | public: | 17 | public: |
| 14 | explicit AchievementsPane(wxWindow* parent); | 18 | explicit AchievementsPane(wxWindow* parent); |
| @@ -16,7 +20,7 @@ class AchievementsPane : public wxListView { | |||
| 16 | void UpdateIndicators(); | 20 | void UpdateIndicators(); |
| 17 | 21 | ||
| 18 | private: | 22 | private: |
| 19 | std::vector<std::string> achievement_names_; | 23 | std::vector<std::tuple<std::string, int>> achievements_; // name, solve index |
| 20 | }; | 24 | }; |
| 21 | 25 | ||
| 22 | #endif /* end of include guard: ACHIEVEMENTS_PANE_H_C320D0B8 */ \ No newline at end of file | 26 | #endif /* end of include guard: ACHIEVEMENTS_PANE_H_C320D0B8 */ \ No newline at end of file |
| diff --git a/src/ap_state.cpp b/src/ap_state.cpp index f245c2b..8438649 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp | |||
| @@ -4,11 +4,13 @@ | |||
| 4 | #define _WEBSOCKETPP_CPP11_STRICT_ | 4 | #define _WEBSOCKETPP_CPP11_STRICT_ |
| 5 | #pragma comment(lib, "crypt32") | 5 | #pragma comment(lib, "crypt32") |
| 6 | 6 | ||
| 7 | #include <fmt/core.h> | ||
| 7 | #include <hkutil/string.h> | 8 | #include <hkutil/string.h> |
| 8 | 9 | ||
| 9 | #include <any> | 10 | #include <any> |
| 10 | #include <apclient.hpp> | 11 | #include <apclient.hpp> |
| 11 | #include <apuuid.hpp> | 12 | #include <apuuid.hpp> |
| 13 | #include <bitset> | ||
| 12 | #include <chrono> | 14 | #include <chrono> |
| 13 | #include <exception> | 15 | #include <exception> |
| 14 | #include <filesystem> | 16 | #include <filesystem> |
| @@ -21,42 +23,73 @@ | |||
| 21 | #include <tuple> | 23 | #include <tuple> |
| 22 | 24 | ||
| 23 | #include "game_data.h" | 25 | #include "game_data.h" |
| 26 | #include "ipc_state.h" | ||
| 27 | #include "logger.h" | ||
| 24 | #include "tracker_frame.h" | 28 | #include "tracker_frame.h" |
| 25 | #include "tracker_state.h" | 29 | #include "tracker_state.h" |
| 26 | 30 | ||
| 27 | constexpr int AP_MAJOR = 0; | 31 | constexpr int AP_MAJOR = 0; |
| 28 | constexpr int AP_MINOR = 4; | 32 | constexpr int AP_MINOR = 6; |
| 29 | constexpr int AP_REVISION = 5; | 33 | constexpr int AP_REVISION = 1; |
| 30 | 34 | ||
| 31 | constexpr const char* CERT_STORE_PATH = "cacert.pem"; | 35 | constexpr const char* CERT_STORE_PATH = "cacert.pem"; |
| 32 | constexpr int ITEM_HANDLING = 7; // <- all | 36 | constexpr int ITEM_HANDLING = 7; // <- all |
| 33 | 37 | ||
| 38 | constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds | ||
| 39 | constexpr int CONNECTION_BACKOFF_INTERVAL = 100; | ||
| 40 | |||
| 41 | constexpr int PANEL_COUNT = 803; | ||
| 42 | constexpr int PANEL_BITFIELD_LENGTH = 48; | ||
| 43 | constexpr int PANEL_BITFIELDS = 17; | ||
| 44 | |||
| 34 | namespace { | 45 | namespace { |
| 35 | 46 | ||
| 36 | struct APState { | 47 | const std::set<long> kNonProgressionItems = { |
| 37 | std::unique_ptr<APClient> apclient; | 48 | 444409, // :) |
| 49 | 444575, // The Feeling of Being Lost | ||
| 50 | 444576, // Wanderlust | ||
| 51 | 444577, // Empty White Hallways | ||
| 52 | 444410, // Slowness Trap | ||
| 53 | 444411, // Iceland Trap | ||
| 54 | 444412, // Atbash Trap | ||
| 55 | 444413, // Puzzle Skip | ||
| 56 | 444680, // Speed Boost | ||
| 57 | }; | ||
| 38 | 58 | ||
| 59 | struct APState { | ||
| 60 | // Initialized on main thread | ||
| 39 | bool initialized = false; | 61 | bool initialized = false; |
| 40 | |||
| 41 | TrackerFrame* tracker_frame = nullptr; | 62 | TrackerFrame* tracker_frame = nullptr; |
| 63 | std::list<std::string> tracked_data_storage_keys; | ||
| 42 | 64 | ||
| 43 | bool client_active = false; | 65 | // Client |
| 44 | std::mutex client_mutex; | 66 | std::mutex client_mutex; |
| 67 | std::unique_ptr<APClient> apclient; | ||
| 68 | |||
| 69 | // Protected state | ||
| 70 | std::mutex state_mutex; | ||
| 71 | |||
| 72 | std::string status_message = "Not connected to Archipelago."; | ||
| 45 | 73 | ||
| 46 | bool connected = false; | 74 | bool connected = false; |
| 47 | bool has_connection_result = false; | 75 | std::string connection_failure; |
| 76 | int remaining_loops = 0; | ||
| 48 | 77 | ||
| 49 | std::string data_storage_prefix; | 78 | std::string data_storage_prefix; |
| 50 | std::list<std::string> tracked_data_storage_keys; | ||
| 51 | std::string victory_data_storage_key; | 79 | std::string victory_data_storage_key; |
| 52 | 80 | ||
| 81 | std::string save_name; | ||
| 82 | |||
| 53 | std::map<int64_t, int> inventory; | 83 | std::map<int64_t, int> inventory; |
| 54 | std::set<int64_t> checked_locations; | 84 | std::set<int64_t> checked_locations; |
| 55 | std::map<std::string, std::any> data_storage; | 85 | std::map<std::string, std::any> data_storage; |
| 56 | std::optional<std::tuple<int, int>> player_pos; | 86 | std::optional<std::tuple<int, int>> player_pos; |
| 87 | std::bitset<PANEL_COUNT> solved_panels; | ||
| 57 | 88 | ||
| 58 | DoorShuffleMode door_shuffle_mode = kNO_DOORS; | 89 | DoorShuffleMode door_shuffle_mode = kNO_DOORS; |
| 90 | bool group_doors = false; | ||
| 59 | bool color_shuffle = false; | 91 | bool color_shuffle = false; |
| 92 | PanelShuffleMode panel_shuffle_mode = kNO_PANELS; | ||
| 60 | bool painting_shuffle = false; | 93 | bool painting_shuffle = false; |
| 61 | int mastery_requirement = 21; | 94 | int mastery_requirement = 21; |
| 62 | int level_2_requirement = 223; | 95 | int level_2_requirement = 223; |
| @@ -68,37 +101,197 @@ struct APState { | |||
| 68 | bool pilgrimage_allows_paintings = false; | 101 | bool pilgrimage_allows_paintings = false; |
| 69 | SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; | 102 | SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; |
| 70 | bool sunwarp_shuffle = false; | 103 | bool sunwarp_shuffle = false; |
| 104 | bool postgame_shuffle = true; | ||
| 71 | 105 | ||
| 72 | std::map<std::string, std::string> painting_mapping; | 106 | std::map<std::string, std::string> painting_mapping; |
| 107 | std::set<std::string> painting_codomain; | ||
| 73 | std::map<int, SunwarpMapping> sunwarp_mapping; | 108 | std::map<int, SunwarpMapping> sunwarp_mapping; |
| 74 | 109 | ||
| 75 | void Connect(std::string server, std::string player, std::string password) { | 110 | void Connect(std::string server, std::string player, std::string password) { |
| 76 | if (!initialized) { | 111 | Initialize(); |
| 77 | wxLogVerbose("Initializing APState..."); | ||
| 78 | 112 | ||
| 79 | std::thread([this]() { | 113 | { |
| 80 | for (;;) { | 114 | std::lock_guard state_guard(state_mutex); |
| 81 | { | 115 | SetStatusMessage("Connecting to Archipelago server...."); |
| 82 | std::lock_guard client_guard(client_mutex); | 116 | } |
| 83 | if (apclient) { | 117 | TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server)); |
| 84 | apclient->poll(); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | 118 | ||
| 88 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | 119 | // Creating and setting up the client has to all be done while holding the |
| 89 | } | 120 | // client mutex, so that the other thread doesn't try to poll before we add |
| 90 | }).detach(); | 121 | // handlers, etc. |
| 122 | { | ||
| 123 | TrackerLog("Destroying old AP client..."); | ||
| 124 | |||
| 125 | std::lock_guard client_guard(client_mutex); | ||
| 91 | 126 | ||
| 92 | for (int panel_id : GD_GetAchievementPanels()) { | 127 | if (apclient) { |
| 93 | tracked_data_storage_keys.push_back( | 128 | DestroyClient(); |
| 94 | "Achievement|" + GD_GetPanel(panel_id).achievement_name); | ||
| 95 | } | 129 | } |
| 96 | 130 | ||
| 97 | for (const MapArea& map_area : GD_GetMapAreas()) { | 131 | std::string cert_store = ""; |
| 98 | for (const Location& location : map_area.locations) { | 132 | if (std::filesystem::exists(CERT_STORE_PATH)) { |
| 99 | tracked_data_storage_keys.push_back( | 133 | cert_store = CERT_STORE_PATH; |
| 100 | "Hunt|" + std::to_string(location.ap_location_id)); | 134 | } |
| 101 | } | 135 | |
| 136 | apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server, | ||
| 137 | cert_store); | ||
| 138 | |||
| 139 | { | ||
| 140 | std::lock_guard state_guard(state_mutex); | ||
| 141 | |||
| 142 | connected = false; | ||
| 143 | connection_failure.clear(); | ||
| 144 | remaining_loops = CONNECTION_TIMEOUT / CONNECTION_BACKOFF_INTERVAL; | ||
| 145 | |||
| 146 | save_name.clear(); | ||
| 147 | inventory.clear(); | ||
| 148 | checked_locations.clear(); | ||
| 149 | data_storage.clear(); | ||
| 150 | player_pos = std::nullopt; | ||
| 151 | solved_panels.reset(); | ||
| 152 | victory_data_storage_key.clear(); | ||
| 153 | door_shuffle_mode = kNO_DOORS; | ||
| 154 | group_doors = false; | ||
| 155 | color_shuffle = false; | ||
| 156 | panel_shuffle_mode = kNO_PANELS; | ||
| 157 | painting_shuffle = false; | ||
| 158 | painting_mapping.clear(); | ||
| 159 | painting_codomain.clear(); | ||
| 160 | mastery_requirement = 21; | ||
| 161 | level_2_requirement = 223; | ||
| 162 | location_checks = kNORMAL_LOCATIONS; | ||
| 163 | victory_condition = kTHE_END; | ||
| 164 | early_color_hallways = false; | ||
| 165 | pilgrimage_enabled = false; | ||
| 166 | pilgrimage_allows_roof_access = false; | ||
| 167 | pilgrimage_allows_paintings = false; | ||
| 168 | sunwarp_access = kSUNWARP_ACCESS_NORMAL; | ||
| 169 | sunwarp_shuffle = false; | ||
| 170 | sunwarp_mapping.clear(); | ||
| 171 | postgame_shuffle = true; | ||
| 172 | } | ||
| 173 | |||
| 174 | apclient->set_room_info_handler( | ||
| 175 | [this, player, password]() { OnRoomInfo(player, password); }); | ||
| 176 | |||
| 177 | apclient->set_location_checked_handler( | ||
| 178 | [this](const std::list<int64_t>& locations) { | ||
| 179 | OnLocationChecked(locations); | ||
| 180 | }); | ||
| 181 | |||
| 182 | apclient->set_slot_disconnected_handler( | ||
| 183 | [this]() { OnSlotDisconnected(); }); | ||
| 184 | |||
| 185 | apclient->set_socket_disconnected_handler( | ||
| 186 | [this]() { OnSocketDisconnected(); }); | ||
| 187 | |||
| 188 | apclient->set_items_received_handler( | ||
| 189 | [this](const std::list<APClient::NetworkItem>& items) { | ||
| 190 | OnItemsReceived(items); | ||
| 191 | }); | ||
| 192 | |||
| 193 | apclient->set_retrieved_handler( | ||
| 194 | [this](const std::map<std::string, nlohmann::json>& data) { | ||
| 195 | OnRetrieved(data); | ||
| 196 | }); | ||
| 197 | |||
| 198 | apclient->set_set_reply_handler( | ||
| 199 | [this](const std::string& key, const nlohmann::json& value, | ||
| 200 | const nlohmann::json&) { OnSetReply(key, value); }); | ||
| 201 | |||
| 202 | apclient->set_slot_connected_handler( | ||
| 203 | [this, player, server](const nlohmann::json& slot_data) { | ||
| 204 | OnSlotConnected(player, server, slot_data); | ||
| 205 | }); | ||
| 206 | |||
| 207 | apclient->set_slot_refused_handler( | ||
| 208 | [this](const std::list<std::string>& errors) { | ||
| 209 | OnSlotRefused(errors); | ||
| 210 | }); | ||
| 211 | } | ||
| 212 | } | ||
| 213 | |||
| 214 | std::string GetStatusMessage() { | ||
| 215 | std::lock_guard state_guard(state_mutex); | ||
| 216 | |||
| 217 | return status_message; | ||
| 218 | } | ||
| 219 | |||
| 220 | bool HasCheckedGameLocation(int location_id) { | ||
| 221 | std::lock_guard state_guard(state_mutex); | ||
| 222 | |||
| 223 | return checked_locations.count(location_id); | ||
| 224 | } | ||
| 225 | |||
| 226 | bool HasItem(int item_id, int quantity) { | ||
| 227 | return inventory.count(item_id) && inventory.at(item_id) >= quantity; | ||
| 228 | } | ||
| 229 | |||
| 230 | bool HasItemSafe(int item_id, int quantity) { | ||
| 231 | std::lock_guard state_guard(state_mutex); | ||
| 232 | return HasItem(item_id, quantity); | ||
| 233 | } | ||
| 234 | |||
| 235 | const std::set<std::string>& GetCheckedPaintings() { | ||
| 236 | std::lock_guard state_guard(state_mutex); | ||
| 237 | |||
| 238 | std::string key = fmt::format("{}Paintings", data_storage_prefix); | ||
| 239 | if (!data_storage.count(key)) { | ||
| 240 | data_storage[key] = std::set<std::string>(); | ||
| 241 | } | ||
| 242 | |||
| 243 | return std::any_cast<const std::set<std::string>&>(data_storage.at(key)); | ||
| 244 | } | ||
| 245 | |||
| 246 | bool IsPaintingChecked(const std::string& painting_id) { | ||
| 247 | const auto& checked_paintings = GetCheckedPaintings(); | ||
| 248 | |||
| 249 | std::lock_guard state_guard(state_mutex); | ||
| 250 | |||
| 251 | return checked_paintings.count(painting_id) || | ||
| 252 | (painting_mapping.count(painting_id) && | ||
| 253 | checked_paintings.count(painting_mapping.at(painting_id))); | ||
| 254 | } | ||
| 255 | |||
| 256 | void RevealPaintings() { | ||
| 257 | std::lock_guard state_guard(state_mutex); | ||
| 258 | |||
| 259 | std::vector<std::string> paintings; | ||
| 260 | for (const PaintingExit& painting : GD_GetPaintings()) { | ||
| 261 | paintings.push_back(painting.internal_id); | ||
| 262 | } | ||
| 263 | |||
| 264 | APClient::DataStorageOperation operation; | ||
| 265 | operation.operation = "replace"; | ||
| 266 | operation.value = paintings; | ||
| 267 | |||
| 268 | apclient->Set(fmt::format("{}Paintings", data_storage_prefix), "", true, | ||
| 269 | {operation}); | ||
| 270 | } | ||
| 271 | |||
| 272 | bool HasReachedGoal() { | ||
| 273 | std::lock_guard state_guard(state_mutex); | ||
| 274 | |||
| 275 | return data_storage.count(victory_data_storage_key) && | ||
| 276 | std::any_cast<int>(data_storage.at(victory_data_storage_key)) == | ||
| 277 | 30; // CLIENT_GOAL | ||
| 278 | } | ||
| 279 | |||
| 280 | bool IsPanelSolved(int solve_index) { | ||
| 281 | std::lock_guard state_guard(state_mutex); | ||
| 282 | |||
| 283 | return solved_panels.test(solve_index); | ||
| 284 | } | ||
| 285 | |||
| 286 | private: | ||
| 287 | void Initialize() { | ||
| 288 | if (!initialized) { | ||
| 289 | TrackerLog("Initializing APState..."); | ||
| 290 | |||
| 291 | std::thread([this]() { Thread(); }).detach(); | ||
| 292 | |||
| 293 | for (int i = 0; i < PANEL_BITFIELDS; i++) { | ||
| 294 | tracked_data_storage_keys.push_back(fmt::format("Panels_{}", i)); | ||
| 102 | } | 295 | } |
| 103 | 296 | ||
| 104 | tracked_data_storage_keys.push_back("PlayerPos"); | 297 | tracked_data_storage_keys.push_back("PlayerPos"); |
| @@ -106,125 +299,189 @@ struct APState { | |||
| 106 | 299 | ||
| 107 | initialized = true; | 300 | initialized = true; |
| 108 | } | 301 | } |
| 302 | } | ||
| 109 | 303 | ||
| 110 | tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); | 304 | void Thread() { |
| 111 | wxLogStatus("Connecting to Archipelago server (%s)...", server); | 305 | std::string display_error; |
| 112 | 306 | ||
| 113 | { | 307 | for (;;) { |
| 114 | wxLogVerbose("Destroying old AP client..."); | 308 | { |
| 309 | std::lock_guard client_guard(client_mutex); | ||
| 310 | if (apclient) { | ||
| 311 | apclient->poll(); | ||
| 115 | 312 | ||
| 116 | std::lock_guard client_guard(client_mutex); | 313 | { |
| 314 | std::lock_guard state_guard(state_mutex); | ||
| 117 | 315 | ||
| 118 | if (apclient) { | 316 | if (!connected) { |
| 119 | DestroyClient(); | 317 | if (!connection_failure.empty()) { |
| 318 | TrackerLog(connection_failure); | ||
| 319 | |||
| 320 | display_error = connection_failure; | ||
| 321 | connection_failure.clear(); | ||
| 322 | |||
| 323 | DestroyClient(); | ||
| 324 | } else { | ||
| 325 | remaining_loops--; | ||
| 326 | |||
| 327 | if (remaining_loops <= 0) { | ||
| 328 | DestroyClient(); | ||
| 329 | |||
| 330 | SetStatusMessage("Disconnected from Archipelago."); | ||
| 331 | TrackerLog("Timeout while connecting to Archipelago server."); | ||
| 332 | |||
| 333 | display_error = | ||
| 334 | "Timeout while connecting to Archipelago server."; | ||
| 335 | } | ||
| 336 | } | ||
| 337 | } | ||
| 338 | } | ||
| 339 | } | ||
| 120 | } | 340 | } |
| 121 | 341 | ||
| 122 | std::string cert_store = ""; | 342 | if (!display_error.empty()) { |
| 123 | if (std::filesystem::exists(CERT_STORE_PATH)) { | 343 | wxMessageBox(display_error, "Connection failed", wxOK | wxICON_ERROR); |
| 124 | cert_store = CERT_STORE_PATH; | 344 | display_error.clear(); |
| 125 | } | 345 | } |
| 126 | 346 | ||
| 127 | apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server, | 347 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
| 128 | cert_store); | ||
| 129 | } | 348 | } |
| 349 | } | ||
| 350 | |||
| 351 | void OnRoomInfo(std::string player, std::string password) { | ||
| 352 | { | ||
| 353 | std::lock_guard state_guard(state_mutex); | ||
| 130 | 354 | ||
| 131 | inventory.clear(); | ||
| 132 | checked_locations.clear(); | ||
| 133 | data_storage.clear(); | ||
| 134 | player_pos = std::nullopt; | ||
| 135 | victory_data_storage_key.clear(); | ||
| 136 | door_shuffle_mode = kNO_DOORS; | ||
| 137 | color_shuffle = false; | ||
| 138 | painting_shuffle = false; | ||
| 139 | painting_mapping.clear(); | ||
| 140 | mastery_requirement = 21; | ||
| 141 | level_2_requirement = 223; | ||
| 142 | location_checks = kNORMAL_LOCATIONS; | ||
| 143 | victory_condition = kTHE_END; | ||
| 144 | early_color_hallways = false; | ||
| 145 | pilgrimage_enabled = false; | ||
| 146 | pilgrimage_allows_roof_access = false; | ||
| 147 | pilgrimage_allows_paintings = false; | ||
| 148 | sunwarp_access = kSUNWARP_ACCESS_NORMAL; | ||
| 149 | sunwarp_shuffle = false; | ||
| 150 | sunwarp_mapping.clear(); | ||
| 151 | |||
| 152 | connected = false; | ||
| 153 | has_connection_result = false; | ||
| 154 | |||
| 155 | apclient->set_room_info_handler([this, player, password]() { | ||
| 156 | inventory.clear(); | 355 | inventory.clear(); |
| 157 | 356 | ||
| 158 | wxLogStatus("Connected to Archipelago server. Authenticating as %s %s", | 357 | SetStatusMessage("Connected to Archipelago server. Authenticating..."); |
| 159 | player, | 358 | } |
| 160 | (password.empty() ? "without password" | ||
| 161 | : "with password " + password)); | ||
| 162 | tracker_frame->SetStatusMessage( | ||
| 163 | "Connected to Archipelago server. Authenticating..."); | ||
| 164 | |||
| 165 | apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"}, | ||
| 166 | {AP_MAJOR, AP_MINOR, AP_REVISION}); | ||
| 167 | }); | ||
| 168 | |||
| 169 | apclient->set_location_checked_handler( | ||
| 170 | [this](const std::list<int64_t>& locations) { | ||
| 171 | for (const int64_t location_id : locations) { | ||
| 172 | checked_locations.insert(location_id); | ||
| 173 | wxLogVerbose("Location: %lld", location_id); | ||
| 174 | } | ||
| 175 | 359 | ||
| 176 | RefreshTracker(false); | 360 | TrackerLog(fmt::format( |
| 177 | }); | 361 | "Connected to Archipelago server. Authenticating as {} {}", player, |
| 178 | 362 | (password.empty() ? "without password" : "with password " + password))); | |
| 179 | apclient->set_slot_disconnected_handler([this]() { | ||
| 180 | tracker_frame->SetStatusMessage( | ||
| 181 | "Disconnected from Archipelago. Attempting to reconnect..."); | ||
| 182 | wxLogStatus( | ||
| 183 | "Slot disconnected from Archipelago. Attempting to reconnect..."); | ||
| 184 | }); | ||
| 185 | |||
| 186 | apclient->set_socket_disconnected_handler([this]() { | ||
| 187 | tracker_frame->SetStatusMessage( | ||
| 188 | "Disconnected from Archipelago. Attempting to reconnect..."); | ||
| 189 | wxLogStatus( | ||
| 190 | "Socket disconnected from Archipelago. Attempting to reconnect..."); | ||
| 191 | }); | ||
| 192 | |||
| 193 | apclient->set_items_received_handler( | ||
| 194 | [this](const std::list<APClient::NetworkItem>& items) { | ||
| 195 | for (const APClient::NetworkItem& item : items) { | ||
| 196 | inventory[item.item]++; | ||
| 197 | wxLogVerbose("Item: %lld", item.item); | ||
| 198 | } | ||
| 199 | 363 | ||
| 200 | RefreshTracker(false); | 364 | apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"}, |
| 201 | }); | 365 | {AP_MAJOR, AP_MINOR, AP_REVISION}); |
| 366 | } | ||
| 202 | 367 | ||
| 203 | apclient->set_retrieved_handler( | 368 | void OnLocationChecked(const std::list<int64_t>& locations) { |
| 204 | [this](const std::map<std::string, nlohmann::json>& data) { | 369 | { |
| 205 | for (const auto& [key, value] : data) { | 370 | std::lock_guard state_guard(state_mutex); |
| 206 | HandleDataStorage(key, value); | 371 | |
| 207 | } | 372 | for (const int64_t location_id : locations) { |
| 373 | checked_locations.insert(location_id); | ||
| 374 | TrackerLog(fmt::format("Location: {}", location_id)); | ||
| 375 | } | ||
| 376 | } | ||
| 377 | |||
| 378 | RefreshTracker(StateUpdate{.cleared_locations = true}); | ||
| 379 | } | ||
| 380 | |||
| 381 | void OnSlotDisconnected() { | ||
| 382 | std::lock_guard state_guard(state_mutex); | ||
| 383 | |||
| 384 | SetStatusMessage( | ||
| 385 | "Disconnected from Archipelago. Attempting to reconnect..."); | ||
| 386 | TrackerLog( | ||
| 387 | "Slot disconnected from Archipelago. Attempting to reconnect..."); | ||
| 388 | } | ||
| 389 | |||
| 390 | void OnSocketDisconnected() { | ||
| 391 | std::lock_guard state_guard(state_mutex); | ||
| 392 | |||
| 393 | SetStatusMessage( | ||
| 394 | "Disconnected from Archipelago. Attempting to reconnect..."); | ||
| 395 | TrackerLog( | ||
| 396 | "Socket disconnected from Archipelago. Attempting to reconnect..."); | ||
| 397 | } | ||
| 398 | |||
| 399 | void OnItemsReceived(const std::list<APClient::NetworkItem>& items) { | ||
| 400 | std::vector<ItemState> item_states; | ||
| 401 | bool progression_items = false; | ||
| 402 | |||
| 403 | { | ||
| 404 | std::lock_guard state_guard(state_mutex); | ||
| 405 | |||
| 406 | std::map<int64_t, int> index_by_item; | ||
| 407 | |||
| 408 | for (const APClient::NetworkItem& item : items) { | ||
| 409 | inventory[item.item]++; | ||
| 410 | TrackerLog(fmt::format("Item: {}", item.item)); | ||
| 411 | |||
| 412 | index_by_item[item.item] = item.index; | ||
| 413 | |||
| 414 | if (!kNonProgressionItems.count(item.item)) { | ||
| 415 | progression_items = true; | ||
| 416 | } | ||
| 417 | } | ||
| 418 | |||
| 419 | for (const auto& [item_id, item_index] : index_by_item) { | ||
| 420 | item_states.push_back(ItemState{.name = GD_GetItemName(item_id), | ||
| 421 | .amount = inventory[item_id], | ||
| 422 | .index = item_index}); | ||
| 423 | } | ||
| 424 | } | ||
| 425 | |||
| 426 | RefreshTracker(StateUpdate{.items = item_states, | ||
| 427 | .progression_items = progression_items}); | ||
| 428 | } | ||
| 429 | |||
| 430 | void OnRetrieved(const std::map<std::string, nlohmann::json>& data) { | ||
| 431 | StateUpdate state_update; | ||
| 432 | |||
| 433 | { | ||
| 434 | std::lock_guard state_guard(state_mutex); | ||
| 435 | |||
| 436 | for (const auto& [key, value] : data) { | ||
| 437 | HandleDataStorage(key, value, state_update); | ||
| 438 | } | ||
| 439 | } | ||
| 208 | 440 | ||
| 209 | RefreshTracker(false); | 441 | RefreshTracker(state_update); |
| 210 | }); | 442 | } |
| 443 | |||
| 444 | void OnSetReply(const std::string& key, const nlohmann::json& value) { | ||
| 445 | StateUpdate state_update; | ||
| 446 | |||
| 447 | { | ||
| 448 | std::lock_guard state_guard(state_mutex); | ||
| 449 | HandleDataStorage(key, value, state_update); | ||
| 450 | } | ||
| 451 | |||
| 452 | RefreshTracker(state_update); | ||
| 453 | } | ||
| 454 | |||
| 455 | void OnSlotConnected(std::string player, std::string server, | ||
| 456 | const nlohmann::json& slot_data) { | ||
| 457 | IPC_SetTrackerSlot(server, player); | ||
| 458 | |||
| 459 | TrackerLog("Connected to Archipelago!"); | ||
| 211 | 460 | ||
| 212 | apclient->set_set_reply_handler([this](const std::string& key, | 461 | { |
| 213 | const nlohmann::json& value, | 462 | std::lock_guard state_guard(state_mutex); |
| 214 | const nlohmann::json&) { | ||
| 215 | HandleDataStorage(key, value); | ||
| 216 | RefreshTracker(false); | ||
| 217 | }); | ||
| 218 | 463 | ||
| 219 | apclient->set_slot_connected_handler([this]( | 464 | SetStatusMessage( |
| 220 | const nlohmann::json& slot_data) { | 465 | fmt::format("Connected to Archipelago! ({}@{}).", player, server)); |
| 221 | tracker_frame->SetStatusMessage("Connected to Archipelago!"); | ||
| 222 | wxLogStatus("Connected to Archipelago!"); | ||
| 223 | 466 | ||
| 467 | save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(), | ||
| 468 | apclient->get_player_number()); | ||
| 224 | data_storage_prefix = | 469 | data_storage_prefix = |
| 225 | "Lingo_" + std::to_string(apclient->get_player_number()) + "_"; | 470 | fmt::format("Lingo_{}_", apclient->get_player_number()); |
| 226 | door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>(); | 471 | door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>(); |
| 472 | if (slot_data.contains("group_doors")) { | ||
| 473 | group_doors = slot_data.contains("group_doors") && | ||
| 474 | slot_data["group_doors"].get<int>() == 1; | ||
| 475 | } else { | ||
| 476 | // If group_doors doesn't exist yet, that means kPANELS_MODE is | ||
| 477 | // actually kSIMPLE_DOORS. | ||
| 478 | if (door_shuffle_mode == kPANELS_MODE) { | ||
| 479 | door_shuffle_mode = kDOORS_MODE; | ||
| 480 | group_doors = true; | ||
| 481 | } | ||
| 482 | } | ||
| 227 | color_shuffle = slot_data["shuffle_colors"].get<int>() == 1; | 483 | color_shuffle = slot_data["shuffle_colors"].get<int>() == 1; |
| 484 | panel_shuffle_mode = slot_data["shuffle_panels"].get<PanelShuffleMode>(); | ||
| 228 | painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; | 485 | painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; |
| 229 | mastery_requirement = slot_data["mastery_achievements"].get<int>(); | 486 | mastery_requirement = slot_data["mastery_achievements"].get<int>(); |
| 230 | level_2_requirement = slot_data["level_2_requirement"].get<int>(); | 487 | level_2_requirement = slot_data["level_2_requirement"].get<int>(); |
| @@ -246,6 +503,9 @@ struct APState { | |||
| 246 | : kSUNWARP_ACCESS_NORMAL; | 503 | : kSUNWARP_ACCESS_NORMAL; |
| 247 | sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && | 504 | sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && |
| 248 | slot_data["shuffle_sunwarps"].get<int>() == 1; | 505 | slot_data["shuffle_sunwarps"].get<int>() == 1; |
| 506 | postgame_shuffle = slot_data.contains("shuffle_postgame") | ||
| 507 | ? (slot_data["shuffle_postgame"].get<int>() == 1) | ||
| 508 | : true; | ||
| 249 | 509 | ||
| 250 | if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { | 510 | if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { |
| 251 | painting_mapping.clear(); | 511 | painting_mapping.clear(); |
| @@ -253,6 +513,7 @@ struct APState { | |||
| 253 | for (const auto& mapping_it : | 513 | for (const auto& mapping_it : |
| 254 | slot_data["painting_entrance_to_exit"].items()) { | 514 | slot_data["painting_entrance_to_exit"].items()) { |
| 255 | painting_mapping[mapping_it.key()] = mapping_it.value(); | 515 | painting_mapping[mapping_it.key()] = mapping_it.value(); |
| 516 | painting_codomain.insert(mapping_it.value()); | ||
| 256 | } | 517 | } |
| 257 | } | 518 | } |
| 258 | 519 | ||
| @@ -268,193 +529,165 @@ struct APState { | |||
| 268 | } | 529 | } |
| 269 | } | 530 | } |
| 270 | 531 | ||
| 271 | connected = true; | ||
| 272 | has_connection_result = true; | ||
| 273 | |||
| 274 | RefreshTracker(true); | ||
| 275 | |||
| 276 | std::list<std::string> corrected_keys; | 532 | std::list<std::string> corrected_keys; |
| 277 | for (const std::string& key : tracked_data_storage_keys) { | 533 | for (const std::string& key : tracked_data_storage_keys) { |
| 278 | corrected_keys.push_back(data_storage_prefix + key); | 534 | corrected_keys.push_back(data_storage_prefix + key); |
| 279 | } | 535 | } |
| 280 | 536 | ||
| 281 | { | 537 | victory_data_storage_key = |
| 282 | std::ostringstream vdsks; | 538 | fmt::format("_read_client_status_{}_{}", apclient->get_team_number(), |
| 283 | vdsks << "_read_client_status_" << apclient->get_team_number() << "_" | 539 | apclient->get_player_number()); |
| 284 | << apclient->get_player_number(); | ||
| 285 | victory_data_storage_key = vdsks.str(); | ||
| 286 | } | ||
| 287 | 540 | ||
| 288 | corrected_keys.push_back(victory_data_storage_key); | 541 | corrected_keys.push_back(victory_data_storage_key); |
| 289 | 542 | ||
| 290 | apclient->Get(corrected_keys); | 543 | apclient->Get(corrected_keys); |
| 291 | apclient->SetNotify(corrected_keys); | 544 | apclient->SetNotify(corrected_keys); |
| 292 | }); | ||
| 293 | |||
| 294 | apclient->set_slot_refused_handler( | ||
| 295 | [this](const std::list<std::string>& errors) { | ||
| 296 | connected = false; | ||
| 297 | has_connection_result = true; | ||
| 298 | |||
| 299 | tracker_frame->SetStatusMessage("Disconnected from Archipelago."); | ||
| 300 | |||
| 301 | std::vector<std::string> error_messages; | ||
| 302 | error_messages.push_back("Could not connect to Archipelago."); | ||
| 303 | |||
| 304 | for (const std::string& error : errors) { | ||
| 305 | if (error == "InvalidSlot") { | ||
| 306 | error_messages.push_back("Invalid player name."); | ||
| 307 | } else if (error == "InvalidGame") { | ||
| 308 | error_messages.push_back( | ||
| 309 | "The specified player is not playing Lingo."); | ||
| 310 | } else if (error == "IncompatibleVersion") { | ||
| 311 | error_messages.push_back( | ||
| 312 | "The Archipelago server is not the correct version for this " | ||
| 313 | "client."); | ||
| 314 | } else if (error == "InvalidPassword") { | ||
| 315 | error_messages.push_back("Incorrect password."); | ||
| 316 | } else if (error == "InvalidItemsHandling") { | ||
| 317 | error_messages.push_back( | ||
| 318 | "Invalid item handling flag. This is a bug with the tracker. " | ||
| 319 | "Please report it to the lingo-ap-tracker GitHub."); | ||
| 320 | } else { | ||
| 321 | error_messages.push_back("Unknown error."); | ||
| 322 | } | ||
| 323 | } | ||
| 324 | |||
| 325 | std::string full_message = hatkirby::implode(error_messages, " "); | ||
| 326 | wxLogError(wxString(full_message)); | ||
| 327 | |||
| 328 | wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR); | ||
| 329 | }); | ||
| 330 | |||
| 331 | client_active = true; | ||
| 332 | 545 | ||
| 333 | int timeout = 5000; // 5 seconds | 546 | connected = true; |
| 334 | int interval = 100; | 547 | } |
| 335 | int remaining_loops = timeout / interval; | ||
| 336 | while (!has_connection_result) { | ||
| 337 | if (interval == 0) { | ||
| 338 | connected = false; | ||
| 339 | has_connection_result = true; | ||
| 340 | 548 | ||
| 341 | DestroyClient(); | 549 | ResetReachabilityRequirements(); |
| 550 | RefreshTracker(std::nullopt); | ||
| 551 | } | ||
| 342 | 552 | ||
| 343 | tracker_frame->SetStatusMessage("Disconnected from Archipelago."); | 553 | void OnSlotRefused(const std::list<std::string>& errors) { |
| 344 | wxLogStatus("Timeout while connecting to Archipelago server."); | 554 | std::vector<std::string> error_messages; |
| 345 | wxMessageBox("Timeout while connecting to Archipelago server.", | 555 | error_messages.push_back("Could not connect to Archipelago."); |
| 346 | "Connection failed", wxOK | wxICON_ERROR); | 556 | |
| 557 | for (const std::string& error : errors) { | ||
| 558 | if (error == "InvalidSlot") { | ||
| 559 | error_messages.push_back("Invalid player name."); | ||
| 560 | } else if (error == "InvalidGame") { | ||
| 561 | error_messages.push_back("The specified player is not playing Lingo."); | ||
| 562 | } else if (error == "IncompatibleVersion") { | ||
| 563 | error_messages.push_back( | ||
| 564 | "The Archipelago server is not the correct version for this " | ||
| 565 | "client."); | ||
| 566 | } else if (error == "InvalidPassword") { | ||
| 567 | error_messages.push_back("Incorrect password."); | ||
| 568 | } else if (error == "InvalidItemsHandling") { | ||
| 569 | error_messages.push_back( | ||
| 570 | "Invalid item handling flag. This is a bug with the tracker. " | ||
| 571 | "Please report it to the lingo-ap-tracker GitHub."); | ||
| 572 | } else { | ||
| 573 | error_messages.push_back("Unknown error."); | ||
| 347 | } | 574 | } |
| 575 | } | ||
| 348 | 576 | ||
| 349 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | 577 | { |
| 578 | std::lock_guard state_guard(state_mutex); | ||
| 579 | connection_failure = hatkirby::implode(error_messages, " "); | ||
| 350 | 580 | ||
| 351 | interval--; | 581 | SetStatusMessage("Disconnected from Archipelago."); |
| 352 | } | 582 | } |
| 583 | } | ||
| 353 | 584 | ||
| 354 | if (connected) { | 585 | // Assumes state mutex is locked. |
| 355 | RefreshTracker(false); | 586 | void SetStatusMessage(std::string msg) { |
| 356 | } else { | 587 | status_message = std::move(msg); |
| 357 | client_active = false; | 588 | |
| 358 | } | 589 | tracker_frame->UpdateStatusMessage(); |
| 359 | } | 590 | } |
| 360 | 591 | ||
| 361 | void HandleDataStorage(const std::string& key, const nlohmann::json& value) { | 592 | // Assumes state mutex is locked. |
| 593 | void HandleDataStorage(const std::string& key, const nlohmann::json& value, StateUpdate& state_update) { | ||
| 362 | if (value.is_boolean()) { | 594 | if (value.is_boolean()) { |
| 363 | data_storage[key] = value.get<bool>(); | 595 | data_storage[key] = value.get<bool>(); |
| 364 | wxLogVerbose("Data storage %s retrieved as %s", key, | 596 | TrackerLog(fmt::format("Data storage {} retrieved as {}", key, |
| 365 | (value.get<bool>() ? "true" : "false")); | 597 | (value.get<bool>() ? "true" : "false"))); |
| 598 | |||
| 366 | } else if (value.is_number()) { | 599 | } else if (value.is_number()) { |
| 367 | data_storage[key] = value.get<int>(); | 600 | data_storage[key] = value.get<int>(); |
| 368 | wxLogVerbose("Data storage %s retrieved as %d", key, value.get<int>()); | 601 | TrackerLog(fmt::format("Data storage {} retrieved as {}", key, |
| 602 | value.get<int>())); | ||
| 603 | |||
| 604 | if (key == victory_data_storage_key) { | ||
| 605 | state_update.cleared_locations = true; | ||
| 606 | } else if (key.find("Panels_") != std::string::npos) { | ||
| 607 | int bitfield_num = | ||
| 608 | std::stoi(key.substr(data_storage_prefix.size() + 7)); | ||
| 609 | uint64_t bitfield_value = value.get<uint64_t>(); | ||
| 610 | for (int i = 0; i < PANEL_BITFIELD_LENGTH; i++) { | ||
| 611 | if ((bitfield_value & (1LL << i)) != 0) { | ||
| 612 | int solve_index = bitfield_num * PANEL_BITFIELD_LENGTH + i; | ||
| 613 | |||
| 614 | if (!solved_panels.test(solve_index)) { | ||
| 615 | state_update.panels.insert(solve_index); | ||
| 616 | } | ||
| 617 | |||
| 618 | solved_panels.set(solve_index); | ||
| 619 | } | ||
| 620 | } | ||
| 621 | } | ||
| 369 | } else if (value.is_object()) { | 622 | } else if (value.is_object()) { |
| 370 | if (key.ends_with("PlayerPos")) { | 623 | if (key.ends_with("PlayerPos")) { |
| 371 | auto map_value = value.get<std::map<std::string, int>>(); | 624 | auto map_value = value.get<std::map<std::string, int>>(); |
| 372 | player_pos = std::tuple<int, int>(map_value["x"], map_value["z"]); | 625 | player_pos = std::tuple<int, int>(map_value["x"], map_value["z"]); |
| 626 | state_update.player_position = true; | ||
| 373 | } else { | 627 | } else { |
| 374 | data_storage[key] = value.get<std::map<std::string, int>>(); | 628 | data_storage[key] = value.get<std::map<std::string, int>>(); |
| 375 | } | 629 | } |
| 376 | 630 | ||
| 377 | wxLogVerbose("Data storage %s retrieved as dictionary", key); | 631 | TrackerLog(fmt::format("Data storage {} retrieved as dictionary", key)); |
| 378 | } else if (value.is_null()) { | 632 | } else if (value.is_null()) { |
| 379 | if (key.ends_with("PlayerPos")) { | 633 | if (key.ends_with("PlayerPos")) { |
| 380 | player_pos = std::nullopt; | 634 | player_pos = std::nullopt; |
| 635 | state_update.player_position = true; | ||
| 381 | } else { | 636 | } else { |
| 382 | data_storage.erase(key); | 637 | data_storage.erase(key); |
| 383 | } | 638 | } |
| 384 | 639 | ||
| 385 | wxLogVerbose("Data storage %s retrieved as null", key); | 640 | TrackerLog(fmt::format("Data storage {} retrieved as null", key)); |
| 386 | } else if (value.is_array()) { | 641 | } else if (value.is_array()) { |
| 387 | auto list_value = value.get<std::vector<std::string>>(); | 642 | auto list_value = value.get<std::vector<std::string>>(); |
| 388 | 643 | ||
| 389 | if (key.ends_with("Paintings")) { | 644 | if (key.ends_with("Paintings")) { |
| 390 | data_storage[key] = | 645 | data_storage[key] = |
| 391 | std::set<std::string>(list_value.begin(), list_value.end()); | 646 | std::set<std::string>(list_value.begin(), list_value.end()); |
| 647 | state_update.paintings = | ||
| 648 | std::vector<std::string>(list_value.begin(), list_value.end()); | ||
| 392 | } else { | 649 | } else { |
| 393 | data_storage[key] = list_value; | 650 | data_storage[key] = list_value; |
| 394 | } | 651 | } |
| 395 | 652 | ||
| 396 | wxLogVerbose("Data storage %s retrieved as list: [%s]", key, | 653 | TrackerLog(fmt::format("Data storage {} retrieved as list: [{}]", key, |
| 397 | hatkirby::implode(list_value, ", ")); | 654 | hatkirby::implode(list_value, ", "))); |
| 398 | } | 655 | } |
| 399 | } | 656 | } |
| 400 | 657 | ||
| 401 | bool HasCheckedGameLocation(int location_id) { | 658 | // State mutex should NOT be locked. |
| 402 | return checked_locations.count(location_id); | 659 | // nullopt state_update indicates a reset. |
| 403 | } | 660 | void RefreshTracker(std::optional<StateUpdate> state_update) { |
| 661 | TrackerLog("Refreshing display..."); | ||
| 404 | 662 | ||
| 405 | bool HasCheckedHuntPanel(int location_id) { | 663 | if (!state_update || state_update->progression_items || |
| 406 | std::string key = | 664 | !state_update->paintings.empty()) { |
| 407 | data_storage_prefix + "Hunt|" + std::to_string(location_id); | 665 | std::string prev_msg; |
| 408 | return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); | 666 | { |
| 409 | } | 667 | std::lock_guard state_guard(state_mutex); |
| 410 | |||
| 411 | bool HasItem(int item_id, int quantity) { | ||
| 412 | return inventory.count(item_id) && inventory.at(item_id) >= quantity; | ||
| 413 | } | ||
| 414 | |||
| 415 | bool HasAchievement(const std::string& name) { | ||
| 416 | std::string key = data_storage_prefix + "Achievement|" + name; | ||
| 417 | return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); | ||
| 418 | } | ||
| 419 | 668 | ||
| 420 | const std::set<std::string>& GetCheckedPaintings() { | 669 | prev_msg = status_message; |
| 421 | std::string key = data_storage_prefix + "Paintings"; | 670 | SetStatusMessage(fmt::format("{} Recalculating...", status_message)); |
| 422 | if (!data_storage.count(key)) { | 671 | } |
| 423 | data_storage[key] = std::set<std::string>(); | ||
| 424 | } | ||
| 425 | 672 | ||
| 426 | return std::any_cast<const std::set<std::string>&>(data_storage.at(key)); | 673 | RecalculateReachability(); |
| 427 | } | ||
| 428 | 674 | ||
| 429 | void RefreshTracker(bool reset) { | 675 | { |
| 430 | wxLogVerbose("Refreshing display..."); | 676 | std::lock_guard state_guard(state_mutex); |
| 431 | 677 | ||
| 432 | RecalculateReachability(); | 678 | SetStatusMessage(prev_msg); |
| 679 | } | ||
| 680 | } | ||
| 681 | |||
| 433 | 682 | ||
| 434 | if (reset) { | 683 | if (!state_update) { |
| 435 | tracker_frame->ResetIndicators(); | 684 | tracker_frame->ResetIndicators(); |
| 436 | } else { | 685 | } else { |
| 437 | tracker_frame->UpdateIndicators(); | 686 | tracker_frame->UpdateIndicators(*state_update); |
| 438 | } | 687 | } |
| 439 | } | 688 | } |
| 440 | 689 | ||
| 441 | int64_t GetItemId(const std::string& item_name) { | ||
| 442 | int64_t ap_id = apclient->get_item_id(item_name); | ||
| 443 | if (ap_id == APClient::INVALID_NAME_ID) { | ||
| 444 | wxLogError("Could not find AP item ID for %s", item_name); | ||
| 445 | } | ||
| 446 | |||
| 447 | return ap_id; | ||
| 448 | } | ||
| 449 | |||
| 450 | bool HasReachedGoal() { | ||
| 451 | return data_storage.count(victory_data_storage_key) && | ||
| 452 | std::any_cast<int>(data_storage.at(victory_data_storage_key)) == | ||
| 453 | 30; // CLIENT_GOAL | ||
| 454 | } | ||
| 455 | |||
| 456 | void DestroyClient() { | 690 | void DestroyClient() { |
| 457 | client_active = false; | ||
| 458 | apclient->reset(); | 691 | apclient->reset(); |
| 459 | apclient.reset(); | 692 | apclient.reset(); |
| 460 | } | 693 | } |
| @@ -473,79 +706,179 @@ void AP_Connect(std::string server, std::string player, std::string password) { | |||
| 473 | GetState().Connect(server, player, password); | 706 | GetState().Connect(server, player, password); |
| 474 | } | 707 | } |
| 475 | 708 | ||
| 476 | bool AP_HasCheckedGameLocation(int location_id) { | 709 | std::string AP_GetStatusMessage() { return GetState().GetStatusMessage(); } |
| 477 | return GetState().HasCheckedGameLocation(location_id); | 710 | |
| 711 | std::string AP_GetSaveName() { | ||
| 712 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 713 | |||
| 714 | return GetState().save_name; | ||
| 478 | } | 715 | } |
| 479 | 716 | ||
| 480 | bool AP_HasCheckedHuntPanel(int location_id) { | 717 | bool AP_HasCheckedGameLocation(int location_id) { |
| 481 | return GetState().HasCheckedHuntPanel(location_id); | 718 | return GetState().HasCheckedGameLocation(location_id); |
| 482 | } | 719 | } |
| 483 | 720 | ||
| 484 | bool AP_HasItem(int item_id, int quantity) { | 721 | bool AP_HasItem(int item_id, int quantity) { |
| 485 | return GetState().HasItem(item_id, quantity); | 722 | return GetState().HasItem(item_id, quantity); |
| 486 | } | 723 | } |
| 487 | 724 | ||
| 488 | DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } | 725 | bool AP_HasItemSafe(int item_id, int quantity) { |
| 726 | return GetState().HasItemSafe(item_id, quantity); | ||
| 727 | } | ||
| 728 | |||
| 729 | DoorShuffleMode AP_GetDoorShuffleMode() { | ||
| 730 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 489 | 731 | ||
| 490 | bool AP_IsColorShuffle() { return GetState().color_shuffle; } | 732 | return GetState().door_shuffle_mode; |
| 733 | } | ||
| 734 | |||
| 735 | bool AP_AreDoorsGrouped() { | ||
| 736 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 737 | |||
| 738 | return GetState().group_doors; | ||
| 739 | } | ||
| 740 | |||
| 741 | bool AP_IsColorShuffle() { | ||
| 742 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 491 | 743 | ||
| 492 | bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } | 744 | return GetState().color_shuffle; |
| 745 | } | ||
| 746 | |||
| 747 | bool AP_IsPaintingShuffle() { | ||
| 748 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 749 | |||
| 750 | return GetState().painting_shuffle; | ||
| 751 | } | ||
| 752 | |||
| 753 | std::map<std::string, std::string> AP_GetPaintingMapping() { | ||
| 754 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 493 | 755 | ||
| 494 | const std::map<std::string, std::string>& AP_GetPaintingMapping() { | ||
| 495 | return GetState().painting_mapping; | 756 | return GetState().painting_mapping; |
| 496 | } | 757 | } |
| 497 | 758 | ||
| 498 | const std::set<std::string>& AP_GetCheckedPaintings() { | 759 | bool AP_IsPaintingMappedTo(const std::string& painting_id) { |
| 760 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 761 | |||
| 762 | return GetState().painting_codomain.count(painting_id); | ||
| 763 | } | ||
| 764 | |||
| 765 | std::set<std::string> AP_GetCheckedPaintings() { | ||
| 499 | return GetState().GetCheckedPaintings(); | 766 | return GetState().GetCheckedPaintings(); |
| 500 | } | 767 | } |
| 501 | 768 | ||
| 502 | int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } | 769 | bool AP_IsPaintingChecked(const std::string& painting_id) { |
| 770 | return GetState().IsPaintingChecked(painting_id); | ||
| 771 | } | ||
| 772 | |||
| 773 | void AP_RevealPaintings() { GetState().RevealPaintings(); } | ||
| 774 | |||
| 775 | int AP_GetMasteryRequirement() { | ||
| 776 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 777 | |||
| 778 | return GetState().mastery_requirement; | ||
| 779 | } | ||
| 780 | |||
| 781 | int AP_GetLevel2Requirement() { | ||
| 782 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 783 | |||
| 784 | return GetState().level_2_requirement; | ||
| 785 | } | ||
| 503 | 786 | ||
| 504 | int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } | 787 | LocationChecks AP_GetLocationsChecks() { |
| 788 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 789 | |||
| 790 | return GetState().location_checks; | ||
| 791 | } | ||
| 505 | 792 | ||
| 506 | bool AP_IsLocationVisible(int classification) { | 793 | bool AP_IsLocationVisible(int classification) { |
| 794 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 795 | |||
| 796 | int world_state = 0; | ||
| 797 | |||
| 507 | switch (GetState().location_checks) { | 798 | switch (GetState().location_checks) { |
| 508 | case kNORMAL_LOCATIONS: | 799 | case kNORMAL_LOCATIONS: |
| 509 | return classification & kLOCATION_NORMAL; | 800 | world_state = kLOCATION_NORMAL; |
| 801 | break; | ||
| 510 | case kREDUCED_LOCATIONS: | 802 | case kREDUCED_LOCATIONS: |
| 511 | return classification & kLOCATION_REDUCED; | 803 | world_state = kLOCATION_REDUCED; |
| 804 | break; | ||
| 512 | case kPANELSANITY: | 805 | case kPANELSANITY: |
| 513 | return classification & kLOCATION_INSANITY; | 806 | world_state = kLOCATION_INSANITY; |
| 807 | break; | ||
| 514 | default: | 808 | default: |
| 515 | return false; | 809 | return false; |
| 516 | } | 810 | } |
| 811 | |||
| 812 | if (GetState().door_shuffle_mode == kDOORS_MODE && | ||
| 813 | !GetState().early_color_hallways) { | ||
| 814 | world_state |= kLOCATION_SMALL_SPHERE_ONE; | ||
| 815 | } | ||
| 816 | |||
| 817 | return (world_state & classification); | ||
| 818 | } | ||
| 819 | |||
| 820 | PanelShuffleMode AP_GetPanelShuffleMode() { | ||
| 821 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 822 | |||
| 823 | return GetState().panel_shuffle_mode; | ||
| 517 | } | 824 | } |
| 518 | 825 | ||
| 519 | VictoryCondition AP_GetVictoryCondition() { | 826 | VictoryCondition AP_GetVictoryCondition() { |
| 827 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 828 | |||
| 520 | return GetState().victory_condition; | 829 | return GetState().victory_condition; |
| 521 | } | 830 | } |
| 522 | 831 | ||
| 523 | bool AP_HasAchievement(const std::string& achievement_name) { | 832 | bool AP_HasEarlyColorHallways() { |
| 524 | return GetState().HasAchievement(achievement_name); | 833 | std::lock_guard state_guard(GetState().state_mutex); |
| 834 | |||
| 835 | return GetState().early_color_hallways; | ||
| 525 | } | 836 | } |
| 526 | 837 | ||
| 527 | bool AP_HasEarlyColorHallways() { return GetState().early_color_hallways; } | 838 | bool AP_IsPilgrimageEnabled() { |
| 839 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 528 | 840 | ||
| 529 | bool AP_IsPilgrimageEnabled() { return GetState().pilgrimage_enabled; } | 841 | return GetState().pilgrimage_enabled; |
| 842 | } | ||
| 530 | 843 | ||
| 531 | bool AP_DoesPilgrimageAllowRoofAccess() { | 844 | bool AP_DoesPilgrimageAllowRoofAccess() { |
| 845 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 846 | |||
| 532 | return GetState().pilgrimage_allows_roof_access; | 847 | return GetState().pilgrimage_allows_roof_access; |
| 533 | } | 848 | } |
| 534 | 849 | ||
| 535 | bool AP_DoesPilgrimageAllowPaintings() { | 850 | bool AP_DoesPilgrimageAllowPaintings() { |
| 851 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 852 | |||
| 536 | return GetState().pilgrimage_allows_paintings; | 853 | return GetState().pilgrimage_allows_paintings; |
| 537 | } | 854 | } |
| 538 | 855 | ||
| 539 | SunwarpAccess AP_GetSunwarpAccess() { return GetState().sunwarp_access; } | 856 | SunwarpAccess AP_GetSunwarpAccess() { |
| 857 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 540 | 858 | ||
| 541 | bool AP_IsSunwarpShuffle() { return GetState().sunwarp_shuffle; } | 859 | return GetState().sunwarp_access; |
| 860 | } | ||
| 861 | |||
| 862 | bool AP_IsSunwarpShuffle() { | ||
| 863 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 864 | |||
| 865 | return GetState().sunwarp_shuffle; | ||
| 866 | } | ||
| 542 | 867 | ||
| 543 | const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() { | 868 | std::map<int, SunwarpMapping> AP_GetSunwarpMapping() { |
| 544 | return GetState().sunwarp_mapping; | 869 | return GetState().sunwarp_mapping; |
| 545 | } | 870 | } |
| 546 | 871 | ||
| 872 | bool AP_IsPostgameShuffle() { return GetState().postgame_shuffle; } | ||
| 873 | |||
| 547 | bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } | 874 | bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } |
| 548 | 875 | ||
| 549 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { | 876 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { |
| 877 | std::lock_guard state_guard(GetState().state_mutex); | ||
| 878 | |||
| 550 | return GetState().player_pos; | 879 | return GetState().player_pos; |
| 551 | } | 880 | } |
| 881 | |||
| 882 | bool AP_IsPanelSolved(int solve_index) { | ||
| 883 | return GetState().IsPanelSolved(solve_index); | ||
| 884 | } | ||
| diff --git a/src/ap_state.h b/src/ap_state.h index 5fbb720..a757d89 100644 --- a/src/ap_state.h +++ b/src/ap_state.h | |||
| @@ -11,7 +11,7 @@ | |||
| 11 | 11 | ||
| 12 | class TrackerFrame; | 12 | class TrackerFrame; |
| 13 | 13 | ||
| 14 | enum DoorShuffleMode { kNO_DOORS = 0, kSIMPLE_DOORS = 1, kCOMPLEX_DOORS = 2 }; | 14 | enum DoorShuffleMode { kNO_DOORS = 0, kPANELS_MODE = 1, kDOORS_MODE = 2 }; |
| 15 | 15 | ||
| 16 | enum VictoryCondition { | 16 | enum VictoryCondition { |
| 17 | kTHE_END = 0, | 17 | kTHE_END = 0, |
| @@ -26,6 +26,8 @@ enum LocationChecks { | |||
| 26 | kPANELSANITY = 2 | 26 | kPANELSANITY = 2 |
| 27 | }; | 27 | }; |
| 28 | 28 | ||
| 29 | enum PanelShuffleMode { kNO_PANELS = 0, kREARRANGE_PANELS = 1 }; | ||
| 30 | |||
| 29 | enum SunwarpAccess { | 31 | enum SunwarpAccess { |
| 30 | kSUNWARP_ACCESS_NORMAL = 0, | 32 | kSUNWARP_ACCESS_NORMAL = 0, |
| 31 | kSUNWARP_ACCESS_DISABLED = 1, | 33 | kSUNWARP_ACCESS_DISABLED = 1, |
| @@ -39,35 +41,57 @@ struct SunwarpMapping { | |||
| 39 | int exit_index; | 41 | int exit_index; |
| 40 | }; | 42 | }; |
| 41 | 43 | ||
| 44 | struct ItemState { | ||
| 45 | std::string name; | ||
| 46 | int amount = 0; | ||
| 47 | int index = 0; | ||
| 48 | }; | ||
| 49 | |||
| 42 | void AP_SetTrackerFrame(TrackerFrame* tracker_frame); | 50 | void AP_SetTrackerFrame(TrackerFrame* tracker_frame); |
| 43 | 51 | ||
| 44 | void AP_Connect(std::string server, std::string player, std::string password); | 52 | void AP_Connect(std::string server, std::string player, std::string password); |
| 45 | 53 | ||
| 46 | bool AP_HasCheckedGameLocation(int location_id); | 54 | std::string AP_GetStatusMessage(); |
| 47 | 55 | ||
| 48 | bool AP_HasCheckedHuntPanel(int location_id); | 56 | std::string AP_GetSaveName(); |
| 49 | 57 | ||
| 58 | bool AP_HasCheckedGameLocation(int location_id); | ||
| 59 | |||
| 60 | // This doesn't lock the state mutex, for speed, so it must ONLY be called from | ||
| 61 | // RecalculateReachability, which is only called from the APState thread anyway. | ||
| 50 | bool AP_HasItem(int item_id, int quantity = 1); | 62 | bool AP_HasItem(int item_id, int quantity = 1); |
| 51 | 63 | ||
| 64 | bool AP_HasItemSafe(int item_id, int quantity = 1); | ||
| 65 | |||
| 52 | DoorShuffleMode AP_GetDoorShuffleMode(); | 66 | DoorShuffleMode AP_GetDoorShuffleMode(); |
| 53 | 67 | ||
| 68 | bool AP_AreDoorsGrouped(); | ||
| 69 | |||
| 54 | bool AP_IsColorShuffle(); | 70 | bool AP_IsColorShuffle(); |
| 55 | 71 | ||
| 56 | bool AP_IsPaintingShuffle(); | 72 | bool AP_IsPaintingShuffle(); |
| 57 | 73 | ||
| 58 | const std::map<std::string, std::string>& AP_GetPaintingMapping(); | 74 | std::map<std::string, std::string> AP_GetPaintingMapping(); |
| 59 | 75 | ||
| 60 | const std::set<std::string>& AP_GetCheckedPaintings(); | 76 | bool AP_IsPaintingMappedTo(const std::string& painting_id); |
| 77 | |||
| 78 | std::set<std::string> AP_GetCheckedPaintings(); | ||
| 79 | |||
| 80 | bool AP_IsPaintingChecked(const std::string& painting_id); | ||
| 81 | |||
| 82 | void AP_RevealPaintings(); | ||
| 61 | 83 | ||
| 62 | int AP_GetMasteryRequirement(); | 84 | int AP_GetMasteryRequirement(); |
| 63 | 85 | ||
| 64 | int AP_GetLevel2Requirement(); | 86 | int AP_GetLevel2Requirement(); |
| 65 | 87 | ||
| 88 | LocationChecks AP_GetLocationsChecks(); | ||
| 89 | |||
| 66 | bool AP_IsLocationVisible(int classification); | 90 | bool AP_IsLocationVisible(int classification); |
| 67 | 91 | ||
| 68 | VictoryCondition AP_GetVictoryCondition(); | 92 | PanelShuffleMode AP_GetPanelShuffleMode(); |
| 69 | 93 | ||
| 70 | bool AP_HasAchievement(const std::string& achievement_name); | 94 | VictoryCondition AP_GetVictoryCondition(); |
| 71 | 95 | ||
| 72 | bool AP_HasEarlyColorHallways(); | 96 | bool AP_HasEarlyColorHallways(); |
| 73 | 97 | ||
| @@ -81,10 +105,14 @@ SunwarpAccess AP_GetSunwarpAccess(); | |||
| 81 | 105 | ||
| 82 | bool AP_IsSunwarpShuffle(); | 106 | bool AP_IsSunwarpShuffle(); |
| 83 | 107 | ||
| 84 | const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping(); | 108 | std::map<int, SunwarpMapping> AP_GetSunwarpMapping(); |
| 109 | |||
| 110 | bool AP_IsPostgameShuffle(); | ||
| 85 | 111 | ||
| 86 | bool AP_HasReachedGoal(); | 112 | bool AP_HasReachedGoal(); |
| 87 | 113 | ||
| 88 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); | 114 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); |
| 89 | 115 | ||
| 116 | bool AP_IsPanelSolved(int solve_index); | ||
| 117 | |||
| 90 | #endif /* end of include guard: AP_STATE_H_664A4180 */ | 118 | #endif /* end of include guard: AP_STATE_H_664A4180 */ |
| diff --git a/src/area_popup.cpp b/src/area_popup.cpp index 6e70315..c95e492 100644 --- a/src/area_popup.cpp +++ b/src/area_popup.cpp | |||
| @@ -2,62 +2,68 @@ | |||
| 2 | 2 | ||
| 3 | #include <wx/dcbuffer.h> | 3 | #include <wx/dcbuffer.h> |
| 4 | 4 | ||
| 5 | #include <algorithm> | ||
| 6 | |||
| 5 | #include "ap_state.h" | 7 | #include "ap_state.h" |
| 6 | #include "game_data.h" | 8 | #include "game_data.h" |
| 7 | #include "global.h" | 9 | #include "global.h" |
| 10 | #include "icons.h" | ||
| 8 | #include "tracker_config.h" | 11 | #include "tracker_config.h" |
| 12 | #include "tracker_panel.h" | ||
| 9 | #include "tracker_state.h" | 13 | #include "tracker_state.h" |
| 10 | 14 | ||
| 11 | AreaPopup::AreaPopup(wxWindow* parent, int area_id) | 15 | AreaPopup::AreaPopup(wxWindow* parent, int area_id) |
| 12 | : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { | 16 | : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { |
| 13 | SetBackgroundStyle(wxBG_STYLE_PAINT); | 17 | SetBackgroundStyle(wxBG_STYLE_PAINT); |
| 14 | 18 | ||
| 15 | unchecked_eye_ = | 19 | LoadIcons(); |
| 16 | wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(), | ||
| 17 | wxBITMAP_TYPE_PNG) | ||
| 18 | .Scale(32, 32)); | ||
| 19 | checked_eye_ = wxBitmap( | ||
| 20 | wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG) | ||
| 21 | .Scale(32, 32)); | ||
| 22 | 20 | ||
| 21 | // TODO: This is slow on high-DPI screens. | ||
| 23 | SetScrollRate(5, 5); | 22 | SetScrollRate(5, 5); |
| 24 | 23 | ||
| 25 | SetBackgroundColour(*wxBLACK); | 24 | SetBackgroundColour(*wxBLACK); |
| 26 | Hide(); | 25 | Hide(); |
| 27 | 26 | ||
| 28 | Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this); | 27 | Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this); |
| 28 | Bind(wxEVT_DPI_CHANGED, &AreaPopup::OnDPIChanged, this); | ||
| 29 | 29 | ||
| 30 | UpdateIndicators(); | 30 | ResetIndicators(); |
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | void AreaPopup::UpdateIndicators() { | 33 | void AreaPopup::ResetIndicators() { |
| 34 | indicators_.clear(); | ||
| 35 | |||
| 34 | const MapArea& map_area = GD_GetMapArea(area_id_); | 36 | const MapArea& map_area = GD_GetMapArea(area_id_); |
| 37 | wxFont the_font = GetFont().Scale(GetDPIScaleFactor()); | ||
| 38 | TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent()); | ||
| 35 | 39 | ||
| 36 | // Start calculating extents. | 40 | // Start calculating extents. |
| 37 | wxMemoryDC mem_dc; | 41 | wxMemoryDC mem_dc; |
| 38 | mem_dc.SetFont(GetFont().Bold()); | 42 | mem_dc.SetFont(the_font.Bold()); |
| 39 | wxSize header_extent = mem_dc.GetTextExtent(map_area.name); | 43 | header_extent_ = mem_dc.GetTextExtent(map_area.name); |
| 40 | 44 | ||
| 41 | int acc_height = header_extent.GetHeight() + 20; | 45 | int acc_height = header_extent_.GetHeight() + FromDIP(20); |
| 42 | int col_width = 0; | 46 | int col_width = 0; |
| 43 | 47 | ||
| 44 | mem_dc.SetFont(GetFont()); | 48 | mem_dc.SetFont(the_font); |
| 45 | |||
| 46 | std::vector<int> real_locations; | ||
| 47 | 49 | ||
| 48 | for (int section_id = 0; section_id < map_area.locations.size(); | 50 | for (int section_id = 0; section_id < map_area.locations.size(); |
| 49 | section_id++) { | 51 | section_id++) { |
| 50 | const Location& location = map_area.locations.at(section_id); | 52 | const Location& location = map_area.locations.at(section_id); |
| 51 | 53 | if ((!AP_IsLocationVisible(location.classification) || | |
| 52 | if (!AP_IsLocationVisible(location.classification) && | 54 | IsLocationPostgame(location.ap_location_id)) && |
| 53 | !(location.hunt && GetTrackerConfig().show_hunt_panels)) { | 55 | !(location.hunt && |
| 56 | GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) && | ||
| 57 | !(location.single_panel && | ||
| 58 | GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS)) { | ||
| 54 | continue; | 59 | continue; |
| 55 | } | 60 | } |
| 56 | 61 | ||
| 57 | real_locations.push_back(section_id); | 62 | indicators_.emplace_back(section_id, kLOCATION, acc_height); |
| 58 | 63 | ||
| 59 | wxSize item_extent = mem_dc.GetTextExtent(location.name); | 64 | wxSize item_extent = mem_dc.GetTextExtent(location.name); |
| 60 | int item_height = std::max(32, item_extent.GetHeight()) + 10; | 65 | int item_height = |
| 66 | std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10); | ||
| 61 | acc_height += item_height; | 67 | acc_height += item_height; |
| 62 | 68 | ||
| 63 | if (item_extent.GetWidth() > col_width) { | 69 | if (item_extent.GetWidth() > col_width) { |
| @@ -65,49 +71,107 @@ void AreaPopup::UpdateIndicators() { | |||
| 65 | } | 71 | } |
| 66 | } | 72 | } |
| 67 | 73 | ||
| 68 | int item_width = col_width + 10 + 32; | 74 | if (AP_IsPaintingShuffle()) { |
| 69 | int full_width = std::max(header_extent.GetWidth(), item_width) + 20; | 75 | for (int painting_id : map_area.paintings) { |
| 70 | 76 | if (IsPaintingPostgame(painting_id)) { | |
| 71 | Fit(); | 77 | continue; |
| 72 | SetVirtualSize(full_width, acc_height); | 78 | } |
| 73 | |||
| 74 | rendered_ = wxBitmap(full_width, acc_height); | ||
| 75 | mem_dc.SelectObject(rendered_); | ||
| 76 | mem_dc.SetPen(*wxTRANSPARENT_PEN); | ||
| 77 | mem_dc.SetBrush(*wxBLACK_BRUSH); | ||
| 78 | mem_dc.DrawRectangle({0, 0}, {full_width, acc_height}); | ||
| 79 | 79 | ||
| 80 | mem_dc.SetFont(GetFont().Bold()); | 80 | indicators_.emplace_back(painting_id, kPAINTING, acc_height); |
| 81 | mem_dc.SetTextForeground(*wxWHITE); | ||
| 82 | mem_dc.DrawText(map_area.name, | ||
| 83 | {(full_width - header_extent.GetWidth()) / 2, 10}); | ||
| 84 | 81 | ||
| 85 | int cur_height = header_extent.GetHeight() + 20; | 82 | const PaintingExit& painting = GD_GetPaintingExit(painting_id); |
| 83 | wxSize item_extent = mem_dc.GetTextExtent(painting.display_name); | ||
| 84 | int item_height = | ||
| 85 | std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10); | ||
| 86 | acc_height += item_height; | ||
| 86 | 87 | ||
| 87 | mem_dc.SetFont(GetFont()); | 88 | if (item_extent.GetWidth() > col_width) { |
| 89 | col_width = item_extent.GetWidth(); | ||
| 90 | } | ||
| 91 | } | ||
| 92 | } | ||
| 88 | 93 | ||
| 89 | for (int section_id : real_locations) { | 94 | int item_width = col_width + FromDIP(10 + 32); |
| 90 | const Location& location = map_area.locations.at(section_id); | 95 | full_width_ = std::max(header_extent_.GetWidth(), item_width) + FromDIP(20); |
| 96 | full_height_ = acc_height; | ||
| 91 | 97 | ||
| 92 | bool checked = | 98 | Fit(); |
| 93 | AP_HasCheckedGameLocation(location.ap_location_id) || | 99 | SetVirtualSize(full_width_, full_height_); |
| 94 | (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id)) || | ||
| 95 | (IsLocationWinCondition(location) && AP_HasReachedGoal()); | ||
| 96 | 100 | ||
| 97 | wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; | 101 | UpdateIndicators(); |
| 102 | } | ||
| 98 | 103 | ||
| 99 | mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); | 104 | void AreaPopup::UpdateIndicators() { |
| 105 | const MapArea& map_area = GD_GetMapArea(area_id_); | ||
| 106 | wxFont the_font = GetFont().Scale(GetDPIScaleFactor()); | ||
| 107 | TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent()); | ||
| 100 | 108 | ||
| 101 | bool reachable = IsLocationReachable(location.ap_location_id); | 109 | rendered_ = wxBitmap(full_width_, full_height_); |
| 102 | const wxColour* text_color = reachable ? wxWHITE : wxRED; | ||
| 103 | mem_dc.SetTextForeground(*text_color); | ||
| 104 | 110 | ||
| 105 | wxSize item_extent = mem_dc.GetTextExtent(location.name); | 111 | wxMemoryDC mem_dc; |
| 106 | mem_dc.DrawText( | 112 | mem_dc.SelectObject(rendered_); |
| 107 | location.name, | 113 | mem_dc.SetPen(*wxTRANSPARENT_PEN); |
| 108 | {10 + 32 + 10, cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); | 114 | mem_dc.SetBrush(*wxBLACK_BRUSH); |
| 115 | mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_}); | ||
| 109 | 116 | ||
| 110 | cur_height += 10 + 32; | 117 | mem_dc.SetFont(the_font.Bold()); |
| 118 | mem_dc.SetTextForeground(*wxWHITE); | ||
| 119 | mem_dc.DrawText(map_area.name, | ||
| 120 | {(full_width_ - header_extent_.GetWidth()) / 2, FromDIP(10)}); | ||
| 121 | |||
| 122 | mem_dc.SetFont(the_font); | ||
| 123 | |||
| 124 | for (const IndicatorInfo& indicator : indicators_) { | ||
| 125 | switch (indicator.type) { | ||
| 126 | case kLOCATION: { | ||
| 127 | const Location& location = map_area.locations.at(indicator.id); | ||
| 128 | |||
| 129 | bool checked = false; | ||
| 130 | if (IsLocationWinCondition(location)) { | ||
| 131 | checked = AP_HasReachedGoal(); | ||
| 132 | } else { | ||
| 133 | checked = AP_HasCheckedGameLocation(location.ap_location_id) || | ||
| 134 | (location.single_panel && | ||
| 135 | AP_IsPanelSolved( | ||
| 136 | GD_GetPanel(*location.single_panel).solve_index)); | ||
| 137 | } | ||
| 138 | |||
| 139 | const wxBitmap* eye_ptr = checked ? checked_eye_ : unchecked_eye_; | ||
| 140 | |||
| 141 | mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y}); | ||
| 142 | |||
| 143 | bool reachable = IsLocationReachable(location.ap_location_id); | ||
| 144 | const wxColour* text_color = reachable ? wxWHITE : wxRED; | ||
| 145 | mem_dc.SetTextForeground(*text_color); | ||
| 146 | |||
| 147 | wxSize item_extent = mem_dc.GetTextExtent(location.name); | ||
| 148 | mem_dc.DrawText( | ||
| 149 | location.name, | ||
| 150 | {FromDIP(10 + 32 + 10), | ||
| 151 | indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2}); | ||
| 152 | |||
| 153 | break; | ||
| 154 | } | ||
| 155 | case kPAINTING: { | ||
| 156 | const PaintingExit& painting = GD_GetPaintingExit(indicator.id); | ||
| 157 | |||
| 158 | bool reachable = IsPaintingReachable(indicator.id); | ||
| 159 | const wxColour* text_color = reachable ? wxWHITE : wxRED; | ||
| 160 | mem_dc.SetTextForeground(*text_color); | ||
| 161 | |||
| 162 | bool checked = reachable && AP_IsPaintingChecked(painting.internal_id); | ||
| 163 | const wxBitmap* eye_ptr = checked ? checked_owl_ : unchecked_owl_; | ||
| 164 | mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y}); | ||
| 165 | |||
| 166 | wxSize item_extent = mem_dc.GetTextExtent(painting.display_name); | ||
| 167 | mem_dc.DrawText( | ||
| 168 | painting.display_name, | ||
| 169 | {FromDIP(10 + 32 + 10), | ||
| 170 | indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2}); | ||
| 171 | |||
| 172 | break; | ||
| 173 | } | ||
| 174 | } | ||
| 111 | } | 175 | } |
| 112 | } | 176 | } |
| 113 | 177 | ||
| @@ -118,3 +182,21 @@ void AreaPopup::OnPaint(wxPaintEvent& event) { | |||
| 118 | 182 | ||
| 119 | event.Skip(); | 183 | event.Skip(); |
| 120 | } | 184 | } |
| 185 | |||
| 186 | void AreaPopup::OnDPIChanged(wxDPIChangedEvent& event) { | ||
| 187 | LoadIcons(); | ||
| 188 | ResetIndicators(); | ||
| 189 | |||
| 190 | event.Skip(); | ||
| 191 | } | ||
| 192 | |||
| 193 | void AreaPopup::LoadIcons() { | ||
| 194 | unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png", | ||
| 195 | FromDIP(wxSize{32, 32})); | ||
| 196 | checked_eye_ = | ||
| 197 | GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32})); | ||
| 198 | unchecked_owl_ = | ||
| 199 | GetTheIconCache().GetIcon("assets/owl.png", FromDIP(wxSize{32, 32})); | ||
| 200 | checked_owl_ = GetTheIconCache().GetIcon("assets/checked_owl.png", | ||
| 201 | FromDIP(wxSize{32, 32})); | ||
| 202 | } | ||
| diff --git a/src/area_popup.h b/src/area_popup.h index 00c644d..f8a2355 100644 --- a/src/area_popup.h +++ b/src/area_popup.h | |||
| @@ -7,19 +7,53 @@ | |||
| 7 | #include <wx/wx.h> | 7 | #include <wx/wx.h> |
| 8 | #endif | 8 | #endif |
| 9 | 9 | ||
| 10 | #include <vector> | ||
| 11 | |||
| 10 | class AreaPopup : public wxScrolledCanvas { | 12 | class AreaPopup : public wxScrolledCanvas { |
| 11 | public: | 13 | public: |
| 12 | AreaPopup(wxWindow* parent, int area_id); | 14 | AreaPopup(wxWindow* parent, int area_id); |
| 13 | 15 | ||
| 16 | void ResetIndicators(); | ||
| 14 | void UpdateIndicators(); | 17 | void UpdateIndicators(); |
| 15 | 18 | ||
| 19 | int GetFullWidth() const { return full_width_; } | ||
| 20 | int GetFullHeight() const { return full_height_; } | ||
| 21 | |||
| 16 | private: | 22 | private: |
| 23 | enum IndicatorType { | ||
| 24 | kLOCATION, | ||
| 25 | kPAINTING, | ||
| 26 | }; | ||
| 27 | |||
| 28 | struct IndicatorInfo { | ||
| 29 | // For locations, the id is an index into the map area's locations list. | ||
| 30 | // For paintings, it is a real painting id. | ||
| 31 | int id; | ||
| 32 | IndicatorType type; | ||
| 33 | int y; | ||
| 34 | |||
| 35 | IndicatorInfo(int id, IndicatorType type, int y) | ||
| 36 | : id(id), type(type), y(y) {} | ||
| 37 | }; | ||
| 38 | |||
| 17 | void OnPaint(wxPaintEvent& event); | 39 | void OnPaint(wxPaintEvent& event); |
| 40 | void OnDPIChanged(wxDPIChangedEvent& event); | ||
| 41 | |||
| 42 | void LoadIcons(); | ||
| 18 | 43 | ||
| 19 | int area_id_; | 44 | int area_id_; |
| 20 | 45 | ||
| 21 | wxBitmap unchecked_eye_; | 46 | const wxBitmap* unchecked_eye_; |
| 22 | wxBitmap checked_eye_; | 47 | const wxBitmap* checked_eye_; |
| 48 | const wxBitmap* unchecked_owl_; | ||
| 49 | const wxBitmap* checked_owl_; | ||
| 50 | |||
| 51 | int full_width_ = 0; | ||
| 52 | int full_height_ = 0; | ||
| 53 | wxSize header_extent_; | ||
| 54 | |||
| 55 | std::vector<IndicatorInfo> indicators_; | ||
| 56 | |||
| 23 | wxBitmap rendered_; | 57 | wxBitmap rendered_; |
| 24 | }; | 58 | }; |
| 25 | 59 | ||
| diff --git a/src/connection_dialog.cpp b/src/connection_dialog.cpp index 64fee98..b55a138 100644 --- a/src/connection_dialog.cpp +++ b/src/connection_dialog.cpp | |||
| @@ -4,17 +4,21 @@ | |||
| 4 | 4 | ||
| 5 | ConnectionDialog::ConnectionDialog() | 5 | ConnectionDialog::ConnectionDialog() |
| 6 | : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") { | 6 | : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") { |
| 7 | server_box_ = | 7 | server_box_ = new wxTextCtrl( |
| 8 | new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_server, | 8 | this, -1, |
| 9 | wxDefaultPosition, {300, -1}); | 9 | wxString::FromUTF8(GetTrackerConfig().connection_details.ap_server), |
| 10 | player_box_ = | 10 | wxDefaultPosition, FromDIP(wxSize{300, -1})); |
| 11 | new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_player, | 11 | player_box_ = new wxTextCtrl( |
| 12 | wxDefaultPosition, {300, -1}); | 12 | this, -1, |
| 13 | wxString::FromUTF8(GetTrackerConfig().connection_details.ap_player), | ||
| 14 | wxDefaultPosition, FromDIP(wxSize{300, -1})); | ||
| 13 | password_box_ = new wxTextCtrl( | 15 | password_box_ = new wxTextCtrl( |
| 14 | this, -1, GetTrackerConfig().connection_details.ap_password, | 16 | this, -1, |
| 15 | wxDefaultPosition, {300, -1}); | 17 | wxString::FromUTF8(GetTrackerConfig().connection_details.ap_password), |
| 18 | wxDefaultPosition, FromDIP(wxSize{300, -1})); | ||
| 16 | 19 | ||
| 17 | wxFlexGridSizer* form_sizer = new wxFlexGridSizer(2, 10, 10); | 20 | wxFlexGridSizer* form_sizer = |
| 21 | new wxFlexGridSizer(2, FromDIP(10), FromDIP(10)); | ||
| 18 | 22 | ||
| 19 | form_sizer->Add( | 23 | form_sizer->Add( |
| 20 | new wxStaticText(this, -1, "Server:"), | 24 | new wxStaticText(this, -1, "Server:"), |
| @@ -30,17 +34,19 @@ ConnectionDialog::ConnectionDialog() | |||
| 30 | form_sizer->Add(password_box_, wxSizerFlags().Expand()); | 34 | form_sizer->Add(password_box_, wxSizerFlags().Expand()); |
| 31 | 35 | ||
| 32 | history_list_ = new wxListBox(this, -1); | 36 | history_list_ = new wxListBox(this, -1); |
| 33 | for (const ConnectionDetails& details : GetTrackerConfig().connection_history) { | 37 | for (const ConnectionDetails& details : |
| 38 | GetTrackerConfig().connection_history) { | ||
| 34 | wxString display_text; | 39 | wxString display_text; |
| 35 | display_text << details.ap_player; | 40 | display_text << wxString::FromUTF8(details.ap_player); |
| 36 | display_text << " ("; | 41 | display_text << " ("; |
| 37 | display_text << details.ap_server; | 42 | display_text << wxString::FromUTF8(details.ap_server); |
| 38 | display_text << ")"; | 43 | display_text << ")"; |
| 39 | 44 | ||
| 40 | history_list_->Append(display_text); | 45 | history_list_->Append(display_text); |
| 41 | } | 46 | } |
| 42 | 47 | ||
| 43 | history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen, this); | 48 | history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen, |
| 49 | this); | ||
| 44 | 50 | ||
| 45 | wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL); | 51 | wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL); |
| 46 | mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand()); | 52 | mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand()); |
| @@ -52,7 +58,8 @@ ConnectionDialog::ConnectionDialog() | |||
| 52 | this, -1, "Enter the details to connect to Archipelago."), | 58 | this, -1, "Enter the details to connect to Archipelago."), |
| 53 | wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder()); | 59 | wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder()); |
| 54 | top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand()); | 60 | top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand()); |
| 55 | top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Border().Center()); | 61 | top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), |
| 62 | wxSizerFlags().Border().Center()); | ||
| 56 | 63 | ||
| 57 | SetSizerAndFit(top_sizer); | 64 | SetSizerAndFit(top_sizer); |
| 58 | 65 | ||
| @@ -62,9 +69,10 @@ ConnectionDialog::ConnectionDialog() | |||
| 62 | 69 | ||
| 63 | void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) { | 70 | void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) { |
| 64 | if (e.IsSelection()) { | 71 | if (e.IsSelection()) { |
| 65 | const ConnectionDetails& details = GetTrackerConfig().connection_history.at(e.GetSelection()); | 72 | const ConnectionDetails& details = |
| 66 | server_box_->SetValue(details.ap_server); | 73 | GetTrackerConfig().connection_history.at(e.GetSelection()); |
| 67 | player_box_->SetValue(details.ap_player); | 74 | server_box_->SetValue(wxString::FromUTF8(details.ap_server)); |
| 68 | password_box_->SetValue(details.ap_password); | 75 | player_box_->SetValue(wxString::FromUTF8(details.ap_player)); |
| 76 | password_box_->SetValue(wxString::FromUTF8(details.ap_password)); | ||
| 69 | } | 77 | } |
| 70 | } | 78 | } |
| diff --git a/src/connection_dialog.h b/src/connection_dialog.h index 9fe62fd..ec2ee72 100644 --- a/src/connection_dialog.h +++ b/src/connection_dialog.h | |||
| @@ -14,12 +14,12 @@ class ConnectionDialog : public wxDialog { | |||
| 14 | public: | 14 | public: |
| 15 | ConnectionDialog(); | 15 | ConnectionDialog(); |
| 16 | 16 | ||
| 17 | std::string GetServerValue() { return server_box_->GetValue().ToStdString(); } | 17 | std::string GetServerValue() { return server_box_->GetValue().utf8_string(); } |
| 18 | 18 | ||
| 19 | std::string GetPlayerValue() { return player_box_->GetValue().ToStdString(); } | 19 | std::string GetPlayerValue() { return player_box_->GetValue().utf8_string(); } |
| 20 | 20 | ||
| 21 | std::string GetPasswordValue() { | 21 | std::string GetPasswordValue() { |
| 22 | return password_box_->GetValue().ToStdString(); | 22 | return password_box_->GetValue().utf8_string(); |
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | private: | 25 | private: |
| diff --git a/src/game_data.cpp b/src/game_data.cpp index 7c9564b..588ffc8 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp | |||
| @@ -1,11 +1,6 @@ | |||
| 1 | #include "game_data.h" | 1 | #include "game_data.h" |
| 2 | 2 | ||
| 3 | #include <wx/wxprec.h> | 3 | #include <fmt/core.h> |
| 4 | |||
| 5 | #ifndef WX_PRECOMP | ||
| 6 | #include <wx/wx.h> | ||
| 7 | #endif | ||
| 8 | |||
| 9 | #include <hkutil/string.h> | 4 | #include <hkutil/string.h> |
| 10 | #include <yaml-cpp/yaml.h> | 5 | #include <yaml-cpp/yaml.h> |
| 11 | 6 | ||
| @@ -13,51 +8,31 @@ | |||
| 13 | #include <sstream> | 8 | #include <sstream> |
| 14 | 9 | ||
| 15 | #include "global.h" | 10 | #include "global.h" |
| 11 | #include "logger.h" | ||
| 16 | 12 | ||
| 17 | namespace { | 13 | namespace { |
| 18 | 14 | ||
| 19 | LingoColor GetColorForString(const std::string &str) { | ||
| 20 | if (str == "black") { | ||
| 21 | return LingoColor::kBlack; | ||
| 22 | } else if (str == "red") { | ||
| 23 | return LingoColor::kRed; | ||
| 24 | } else if (str == "blue") { | ||
| 25 | return LingoColor::kBlue; | ||
| 26 | } else if (str == "yellow") { | ||
| 27 | return LingoColor::kYellow; | ||
| 28 | } else if (str == "orange") { | ||
| 29 | return LingoColor::kOrange; | ||
| 30 | } else if (str == "green") { | ||
| 31 | return LingoColor::kGreen; | ||
| 32 | } else if (str == "gray") { | ||
| 33 | return LingoColor::kGray; | ||
| 34 | } else if (str == "brown") { | ||
| 35 | return LingoColor::kBrown; | ||
| 36 | } else if (str == "purple") { | ||
| 37 | return LingoColor::kPurple; | ||
| 38 | } else { | ||
| 39 | wxLogError("Invalid color: %s", str); | ||
| 40 | |||
| 41 | return LingoColor::kNone; | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | struct GameData { | 15 | struct GameData { |
| 46 | std::vector<Room> rooms_; | 16 | std::vector<Room> rooms_; |
| 47 | std::vector<Door> doors_; | 17 | std::vector<Door> doors_; |
| 48 | std::vector<Panel> panels_; | 18 | std::vector<Panel> panels_; |
| 19 | std::vector<PanelDoor> panel_doors_; | ||
| 49 | std::vector<MapArea> map_areas_; | 20 | std::vector<MapArea> map_areas_; |
| 50 | std::vector<SubwayItem> subway_items_; | 21 | std::vector<SubwayItem> subway_items_; |
| 22 | std::vector<PaintingExit> paintings_; | ||
| 51 | 23 | ||
| 52 | std::map<std::string, int> room_by_id_; | 24 | std::map<std::string, int> room_by_id_; |
| 53 | std::map<std::string, int> door_by_id_; | 25 | std::map<std::string, int> door_by_id_; |
| 54 | std::map<std::string, int> panel_by_id_; | 26 | std::map<std::string, int> panel_by_id_; |
| 27 | std::map<std::string, int> panel_doors_by_id_; | ||
| 55 | std::map<std::string, int> area_by_id_; | 28 | std::map<std::string, int> area_by_id_; |
| 29 | std::map<std::string, int> painting_by_id_; | ||
| 56 | 30 | ||
| 57 | std::vector<int> door_definition_order_; | 31 | std::vector<int> door_definition_order_; |
| 58 | 32 | ||
| 59 | std::map<std::string, int> room_by_painting_; | 33 | std::map<std::string, int> room_by_painting_; |
| 60 | std::map<int, int> room_by_sunwarp_; | 34 | std::map<int, int> room_by_sunwarp_; |
| 35 | std::map<int, int> panel_by_solve_index_; | ||
| 61 | 36 | ||
| 62 | std::vector<int> achievement_panels_; | 37 | std::vector<int> achievement_panels_; |
| 63 | 38 | ||
| @@ -68,6 +43,8 @@ struct GameData { | |||
| 68 | std::map<std::string, int> subway_item_by_painting_; | 43 | std::map<std::string, int> subway_item_by_painting_; |
| 69 | std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; | 44 | std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; |
| 70 | 45 | ||
| 46 | std::map<int, std::string> item_by_ap_id_; | ||
| 47 | |||
| 71 | bool loaded_area_data_ = false; | 48 | bool loaded_area_data_ = false; |
| 72 | std::set<std::string> malconfigured_areas_; | 49 | std::set<std::string> malconfigured_areas_; |
| 73 | 50 | ||
| @@ -83,10 +60,10 @@ struct GameData { | |||
| 83 | ids_config["special_items"][color_name]) { | 60 | ids_config["special_items"][color_name]) { |
| 84 | std::string input_name = color_name; | 61 | std::string input_name = color_name; |
| 85 | input_name[0] = std::tolower(input_name[0]); | 62 | input_name[0] = std::tolower(input_name[0]); |
| 86 | ap_id_by_color_[GetColorForString(input_name)] = | 63 | ap_id_by_color_[GetLingoColorForString(input_name)] = |
| 87 | ids_config["special_items"][color_name].as<int>(); | 64 | ids_config["special_items"][color_name].as<int>(); |
| 88 | } else { | 65 | } else { |
| 89 | wxLogError("Missing AP item ID for color %s", color_name); | 66 | TrackerLog(fmt::format("Missing AP item ID for color {}", color_name)); |
| 90 | } | 67 | } |
| 91 | }; | 68 | }; |
| 92 | 69 | ||
| @@ -100,8 +77,18 @@ struct GameData { | |||
| 100 | init_color_id("Brown"); | 77 | init_color_id("Brown"); |
| 101 | init_color_id("Gray"); | 78 | init_color_id("Gray"); |
| 102 | 79 | ||
| 80 | if (ids_config["special_items"]) { | ||
| 81 | for (const auto& special_item_it : ids_config["special_items"]) | ||
| 82 | { | ||
| 83 | item_by_ap_id_[special_item_it.second.as<int>()] = | ||
| 84 | special_item_it.first.as<std::string>(); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 103 | rooms_.reserve(lingo_config.size() * 2); | 88 | rooms_.reserve(lingo_config.size() * 2); |
| 104 | 89 | ||
| 90 | std::vector<int> panel_location_ids; | ||
| 91 | |||
| 105 | for (const auto &room_it : lingo_config) { | 92 | for (const auto &room_it : lingo_config) { |
| 106 | int room_id = AddOrGetRoom(room_it.first.as<std::string>()); | 93 | int room_id = AddOrGetRoom(room_it.first.as<std::string>()); |
| 107 | 94 | ||
| @@ -111,6 +98,7 @@ struct GameData { | |||
| 111 | auto process_single_entrance = | 98 | auto process_single_entrance = |
| 112 | [this, room_id, from_room_id](const YAML::Node &option) { | 99 | [this, room_id, from_room_id](const YAML::Node &option) { |
| 113 | Exit exit_obj; | 100 | Exit exit_obj; |
| 101 | exit_obj.source_room = from_room_id; | ||
| 114 | exit_obj.destination_room = room_id; | 102 | exit_obj.destination_room = room_id; |
| 115 | 103 | ||
| 116 | if (option["door"]) { | 104 | if (option["door"]) { |
| @@ -139,13 +127,17 @@ struct GameData { | |||
| 139 | exit_obj.type = EntranceType::kCrossroadsRoofAccess; | 127 | exit_obj.type = EntranceType::kCrossroadsRoofAccess; |
| 140 | } | 128 | } |
| 141 | 129 | ||
| 130 | if (option["static_painting"] && option["static_painting"].as<bool>()) { | ||
| 131 | exit_obj.type = EntranceType::kStaticPainting; | ||
| 132 | } | ||
| 133 | |||
| 142 | rooms_[from_room_id].exits.push_back(exit_obj); | 134 | rooms_[from_room_id].exits.push_back(exit_obj); |
| 143 | }; | 135 | }; |
| 144 | 136 | ||
| 145 | switch (entrance_it.second.Type()) { | 137 | switch (entrance_it.second.Type()) { |
| 146 | case YAML::NodeType::Scalar: { | 138 | case YAML::NodeType::Scalar: { |
| 147 | // This is just "true". | 139 | // This is just "true". |
| 148 | rooms_[from_room_id].exits.push_back({.destination_room = room_id}); | 140 | rooms_[from_room_id].exits.push_back({.source_room = from_room_id, .destination_room = room_id}); |
| 149 | break; | 141 | break; |
| 150 | } | 142 | } |
| 151 | case YAML::NodeType::Map: { | 143 | case YAML::NodeType::Map: { |
| @@ -163,7 +155,8 @@ struct GameData { | |||
| 163 | // This shouldn't happen. | 155 | // This shouldn't happen. |
| 164 | std::ostringstream formatted; | 156 | std::ostringstream formatted; |
| 165 | formatted << entrance_it; | 157 | formatted << entrance_it; |
| 166 | wxLogError("Error reading game data: %s", formatted.str()); | 158 | TrackerLog( |
| 159 | fmt::format("Error reading game data: {}", formatted.str())); | ||
| 167 | break; | 160 | break; |
| 168 | } | 161 | } |
| 169 | } | 162 | } |
| @@ -177,12 +170,12 @@ struct GameData { | |||
| 177 | 170 | ||
| 178 | if (panel_it.second["colors"]) { | 171 | if (panel_it.second["colors"]) { |
| 179 | if (panel_it.second["colors"].IsScalar()) { | 172 | if (panel_it.second["colors"].IsScalar()) { |
| 180 | panels_[panel_id].colors.push_back(GetColorForString( | 173 | panels_[panel_id].colors.push_back(GetLingoColorForString( |
| 181 | panel_it.second["colors"].as<std::string>())); | 174 | panel_it.second["colors"].as<std::string>())); |
| 182 | } else { | 175 | } else { |
| 183 | for (const auto &color_node : panel_it.second["colors"]) { | 176 | for (const auto &color_node : panel_it.second["colors"]) { |
| 184 | panels_[panel_id].colors.push_back( | 177 | panels_[panel_id].colors.push_back( |
| 185 | GetColorForString(color_node.as<std::string>())); | 178 | GetLingoColorForString(color_node.as<std::string>())); |
| 186 | } | 179 | } |
| 187 | } | 180 | } |
| 188 | } | 181 | } |
| @@ -260,6 +253,16 @@ struct GameData { | |||
| 260 | achievement_panels_.push_back(panel_id); | 253 | achievement_panels_.push_back(panel_id); |
| 261 | } | 254 | } |
| 262 | 255 | ||
| 256 | if (panel_it.second["location_name"]) { | ||
| 257 | panels_[panel_id].location_name = | ||
| 258 | panel_it.second["location_name"].as<std::string>(); | ||
| 259 | } | ||
| 260 | |||
| 261 | if (panel_it.second["id"]) { | ||
| 262 | panels_[panel_id].nodepath = | ||
| 263 | panel_it.second["id"].as<std::string>(); | ||
| 264 | } | ||
| 265 | |||
| 263 | if (panel_it.second["hunt"]) { | 266 | if (panel_it.second["hunt"]) { |
| 264 | panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>(); | 267 | panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>(); |
| 265 | } | 268 | } |
| @@ -278,13 +281,15 @@ struct GameData { | |||
| 278 | ids_config["panels"][rooms_[room_id].name] && | 281 | ids_config["panels"][rooms_[room_id].name] && |
| 279 | ids_config["panels"][rooms_[room_id].name] | 282 | ids_config["panels"][rooms_[room_id].name] |
| 280 | [panels_[panel_id].name]) { | 283 | [panels_[panel_id].name]) { |
| 281 | panels_[panel_id].ap_location_id = | 284 | int location_id = ids_config["panels"][rooms_[room_id].name] |
| 282 | ids_config["panels"][rooms_[room_id].name] | 285 | [panels_[panel_id].name] |
| 283 | [panels_[panel_id].name] | 286 | .as<int>(); |
| 284 | .as<int>(); | 287 | panels_[panel_id].ap_location_id = location_id; |
| 288 | panel_location_ids.push_back(location_id); | ||
| 285 | } else { | 289 | } else { |
| 286 | wxLogError("Missing AP location ID for panel %s - %s", | 290 | TrackerLog(fmt::format("Missing AP location ID for panel {} - {}", |
| 287 | rooms_[room_id].name, panels_[panel_id].name); | 291 | rooms_[room_id].name, |
| 292 | panels_[panel_id].name)); | ||
| 288 | } | 293 | } |
| 289 | } | 294 | } |
| 290 | } | 295 | } |
| @@ -346,9 +351,13 @@ struct GameData { | |||
| 346 | ids_config["doors"][rooms_[room_id].name] | 351 | ids_config["doors"][rooms_[room_id].name] |
| 347 | [doors_[door_id].name]["item"] | 352 | [doors_[door_id].name]["item"] |
| 348 | .as<int>(); | 353 | .as<int>(); |
| 354 | |||
| 355 | item_by_ap_id_[doors_[door_id].ap_item_id] = | ||
| 356 | doors_[door_id].item_name; | ||
| 349 | } else { | 357 | } else { |
| 350 | wxLogError("Missing AP item ID for door %s - %s", | 358 | TrackerLog(fmt::format("Missing AP item ID for door {} - {}", |
| 351 | rooms_[room_id].name, doors_[door_id].name); | 359 | rooms_[room_id].name, |
| 360 | doors_[door_id].name)); | ||
| 352 | } | 361 | } |
| 353 | } | 362 | } |
| 354 | 363 | ||
| @@ -361,9 +370,12 @@ struct GameData { | |||
| 361 | doors_[door_id].group_ap_item_id = | 370 | doors_[door_id].group_ap_item_id = |
| 362 | ids_config["door_groups"][doors_[door_id].group_name] | 371 | ids_config["door_groups"][doors_[door_id].group_name] |
| 363 | .as<int>(); | 372 | .as<int>(); |
| 373 | |||
| 374 | item_by_ap_id_[doors_[door_id].group_ap_item_id] = | ||
| 375 | doors_[door_id].group_name; | ||
| 364 | } else { | 376 | } else { |
| 365 | wxLogError("Missing AP item ID for door group %s", | 377 | TrackerLog(fmt::format("Missing AP item ID for door group {}", |
| 366 | doors_[door_id].group_name); | 378 | doors_[door_id].group_name)); |
| 367 | } | 379 | } |
| 368 | } | 380 | } |
| 369 | 381 | ||
| @@ -373,11 +385,11 @@ struct GameData { | |||
| 373 | } else if (!door_it.second["skip_location"] && | 385 | } else if (!door_it.second["skip_location"] && |
| 374 | !door_it.second["event"]) { | 386 | !door_it.second["event"]) { |
| 375 | if (has_external_panels) { | 387 | if (has_external_panels) { |
| 376 | wxLogError( | 388 | TrackerLog(fmt::format( |
| 377 | "%s - %s has panels from other rooms but does not have an " | 389 | "{} - {} has panels from other rooms but does not have an " |
| 378 | "explicit location name and is not marked skip_location or " | 390 | "explicit location name and is not marked skip_location or " |
| 379 | "event", | 391 | "event", |
| 380 | rooms_[room_id].name, doors_[door_id].name); | 392 | rooms_[room_id].name, doors_[door_id].name)); |
| 381 | } | 393 | } |
| 382 | 394 | ||
| 383 | doors_[door_id].location_name = | 395 | doors_[door_id].location_name = |
| @@ -397,8 +409,9 @@ struct GameData { | |||
| 397 | [doors_[door_id].name]["location"] | 409 | [doors_[door_id].name]["location"] |
| 398 | .as<int>(); | 410 | .as<int>(); |
| 399 | } else { | 411 | } else { |
| 400 | wxLogError("Missing AP location ID for door %s - %s", | 412 | TrackerLog(fmt::format("Missing AP location ID for door {} - {}", |
| 401 | rooms_[room_id].name, doors_[door_id].name); | 413 | rooms_[room_id].name, |
| 414 | doors_[door_id].name)); | ||
| 402 | } | 415 | } |
| 403 | } | 416 | } |
| 404 | 417 | ||
| @@ -417,14 +430,107 @@ struct GameData { | |||
| 417 | } | 430 | } |
| 418 | } | 431 | } |
| 419 | 432 | ||
| 433 | if (room_it.second["panel_doors"]) { | ||
| 434 | for (const auto &panel_door_it : room_it.second["panel_doors"]) { | ||
| 435 | std::string panel_door_name = panel_door_it.first.as<std::string>(); | ||
| 436 | int panel_door_id = | ||
| 437 | AddOrGetPanelDoor(rooms_[room_id].name, panel_door_name); | ||
| 438 | |||
| 439 | std::map<std::string, std::vector<std::string>> panel_per_room; | ||
| 440 | int num_panels = 0; | ||
| 441 | for (const auto &panel_node : panel_door_it.second["panels"]) { | ||
| 442 | num_panels++; | ||
| 443 | |||
| 444 | int panel_id = -1; | ||
| 445 | |||
| 446 | if (panel_node.IsScalar()) { | ||
| 447 | panel_id = AddOrGetPanel(rooms_[room_id].name, | ||
| 448 | panel_node.as<std::string>()); | ||
| 449 | |||
| 450 | panel_per_room[rooms_[room_id].name].push_back( | ||
| 451 | panel_node.as<std::string>()); | ||
| 452 | } else { | ||
| 453 | panel_id = AddOrGetPanel(panel_node["room"].as<std::string>(), | ||
| 454 | panel_node["panel"].as<std::string>()); | ||
| 455 | |||
| 456 | panel_per_room[panel_node["room"].as<std::string>()].push_back( | ||
| 457 | panel_node["panel"].as<std::string>()); | ||
| 458 | } | ||
| 459 | |||
| 460 | Panel &panel = panels_[panel_id]; | ||
| 461 | panel.panel_door = panel_door_id; | ||
| 462 | } | ||
| 463 | |||
| 464 | if (panel_door_it.second["item_name"]) { | ||
| 465 | panel_doors_[panel_door_id].item_name = | ||
| 466 | panel_door_it.second["item_name"].as<std::string>(); | ||
| 467 | } else { | ||
| 468 | std::vector<std::string> room_strs; | ||
| 469 | for (const auto &[room_str, panels_str] : panel_per_room) { | ||
| 470 | room_strs.push_back(fmt::format( | ||
| 471 | "{} - {}", room_str, hatkirby::implode(panels_str, ", "))); | ||
| 472 | } | ||
| 473 | |||
| 474 | if (num_panels == 1) { | ||
| 475 | panel_doors_[panel_door_id].item_name = | ||
| 476 | fmt::format("{} (Panel)", room_strs[0]); | ||
| 477 | } else { | ||
| 478 | panel_doors_[panel_door_id].item_name = fmt::format( | ||
| 479 | "{} (Panels)", hatkirby::implode(room_strs, " and ")); | ||
| 480 | } | ||
| 481 | } | ||
| 482 | |||
| 483 | if (ids_config["panel_doors"] && | ||
| 484 | ids_config["panel_doors"][rooms_[room_id].name] && | ||
| 485 | ids_config["panel_doors"][rooms_[room_id].name] | ||
| 486 | [panel_door_name]) { | ||
| 487 | panel_doors_[panel_door_id].ap_item_id = | ||
| 488 | ids_config["panel_doors"][rooms_[room_id].name][panel_door_name] | ||
| 489 | .as<int>(); | ||
| 490 | |||
| 491 | item_by_ap_id_[panel_doors_[panel_door_id].ap_item_id] = | ||
| 492 | panel_doors_[panel_door_id].item_name; | ||
| 493 | } else { | ||
| 494 | TrackerLog(fmt::format("Missing AP item ID for panel door {} - {}", | ||
| 495 | rooms_[room_id].name, panel_door_name)); | ||
| 496 | } | ||
| 497 | |||
| 498 | if (panel_door_it.second["panel_group"]) { | ||
| 499 | std::string panel_group = | ||
| 500 | panel_door_it.second["panel_group"].as<std::string>(); | ||
| 501 | |||
| 502 | if (ids_config["panel_groups"] && | ||
| 503 | ids_config["panel_groups"][panel_group]) { | ||
| 504 | panel_doors_[panel_door_id].group_ap_item_id = | ||
| 505 | ids_config["panel_groups"][panel_group].as<int>(); | ||
| 506 | |||
| 507 | item_by_ap_id_[panel_doors_[panel_door_id].group_ap_item_id] = | ||
| 508 | panel_group; | ||
| 509 | } else { | ||
| 510 | TrackerLog(fmt::format( | ||
| 511 | "Missing AP item ID for panel door group {}", panel_group)); | ||
| 512 | } | ||
| 513 | } | ||
| 514 | } | ||
| 515 | } | ||
| 516 | |||
| 420 | if (room_it.second["paintings"]) { | 517 | if (room_it.second["paintings"]) { |
| 421 | for (const auto &painting : room_it.second["paintings"]) { | 518 | for (const auto &painting : room_it.second["paintings"]) { |
| 422 | std::string painting_id = painting["id"].as<std::string>(); | 519 | std::string internal_id = painting["id"].as<std::string>(); |
| 423 | room_by_painting_[painting_id] = room_id; | 520 | int painting_id = AddOrGetPainting(internal_id); |
| 521 | PaintingExit &painting_exit = paintings_[painting_id]; | ||
| 522 | painting_exit.room = room_id; | ||
| 523 | |||
| 524 | if (painting["display_name"]) { | ||
| 525 | painting_exit.display_name = | ||
| 526 | painting["display_name"].as<std::string>(); | ||
| 527 | } else { | ||
| 528 | painting_exit.display_name = painting_exit.internal_id; | ||
| 529 | } | ||
| 424 | 530 | ||
| 425 | if (!painting["exit_only"] || !painting["exit_only"].as<bool>()) { | 531 | if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) && |
| 426 | PaintingExit painting_exit; | 532 | (!painting["disable"] || !painting["disable"].as<bool>())) { |
| 427 | painting_exit.id = painting_id; | 533 | painting_exit.entrance = true; |
| 428 | 534 | ||
| 429 | if (painting["required_door"]) { | 535 | if (painting["required_door"]) { |
| 430 | std::string rd_room = rooms_[room_id].name; | 536 | std::string rd_room = rooms_[room_id].name; |
| @@ -435,9 +541,9 @@ struct GameData { | |||
| 435 | painting_exit.door = AddOrGetDoor( | 541 | painting_exit.door = AddOrGetDoor( |
| 436 | rd_room, painting["required_door"]["door"].as<std::string>()); | 542 | rd_room, painting["required_door"]["door"].as<std::string>()); |
| 437 | } | 543 | } |
| 438 | |||
| 439 | rooms_[room_id].paintings.push_back(painting_exit); | ||
| 440 | } | 544 | } |
| 545 | |||
| 546 | rooms_[room_id].paintings.push_back(painting_exit.id); | ||
| 441 | } | 547 | } |
| 442 | } | 548 | } |
| 443 | 549 | ||
| @@ -463,33 +569,74 @@ struct GameData { | |||
| 463 | ids_config["progression"][progressive_item_name]) { | 569 | ids_config["progression"][progressive_item_name]) { |
| 464 | progressive_item_id = | 570 | progressive_item_id = |
| 465 | ids_config["progression"][progressive_item_name].as<int>(); | 571 | ids_config["progression"][progressive_item_name].as<int>(); |
| 572 | |||
| 573 | item_by_ap_id_[progressive_item_id] = progressive_item_name; | ||
| 466 | } else { | 574 | } else { |
| 467 | wxLogError("Missing AP item ID for progressive item %s", | 575 | TrackerLog(fmt::format("Missing AP item ID for progressive item {}", |
| 468 | progressive_item_name); | 576 | progressive_item_name)); |
| 469 | } | 577 | } |
| 470 | 578 | ||
| 471 | int index = 1; | 579 | if (progression_it.second["doors"]) { |
| 472 | for (const auto &stage : progression_it.second) { | 580 | int index = 1; |
| 473 | int door_id = -1; | 581 | for (const auto &stage : progression_it.second["doors"]) { |
| 582 | int door_id = -1; | ||
| 583 | |||
| 584 | if (stage.IsScalar()) { | ||
| 585 | door_id = | ||
| 586 | AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>()); | ||
| 587 | } else { | ||
| 588 | door_id = AddOrGetDoor(stage["room"].as<std::string>(), | ||
| 589 | stage["door"].as<std::string>()); | ||
| 590 | } | ||
| 474 | 591 | ||
| 475 | if (stage.IsScalar()) { | 592 | doors_[door_id].progressives.push_back( |
| 476 | door_id = | 593 | {.item_name = progressive_item_name, |
| 477 | AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>()); | 594 | .ap_item_id = progressive_item_id, |
| 478 | } else { | 595 | .quantity = index}); |
| 479 | door_id = AddOrGetDoor(stage["room"].as<std::string>(), | 596 | index++; |
| 480 | stage["door"].as<std::string>()); | ||
| 481 | } | 597 | } |
| 598 | } | ||
| 482 | 599 | ||
| 483 | doors_[door_id].progressives.push_back( | 600 | if (progression_it.second["panel_doors"]) { |
| 484 | {.item_name = progressive_item_name, | 601 | int index = 1; |
| 485 | .ap_item_id = progressive_item_id, | 602 | for (const auto &stage : progression_it.second["panel_doors"]) { |
| 486 | .quantity = index}); | 603 | int panel_door_id = -1; |
| 487 | index++; | 604 | |
| 605 | if (stage.IsScalar()) { | ||
| 606 | panel_door_id = AddOrGetPanelDoor(rooms_[room_id].name, | ||
| 607 | stage.as<std::string>()); | ||
| 608 | } else { | ||
| 609 | panel_door_id = | ||
| 610 | AddOrGetPanelDoor(stage["room"].as<std::string>(), | ||
| 611 | stage["panel_door"].as<std::string>()); | ||
| 612 | } | ||
| 613 | |||
| 614 | panel_doors_[panel_door_id].progressives.push_back( | ||
| 615 | {.item_name = progressive_item_name, | ||
| 616 | .ap_item_id = progressive_item_id, | ||
| 617 | .quantity = index}); | ||
| 618 | index++; | ||
| 619 | } | ||
| 488 | } | 620 | } |
| 489 | } | 621 | } |
| 490 | } | 622 | } |
| 491 | } | 623 | } |
| 492 | 624 | ||
| 625 | // Determine the panel solve indices from the sorted location IDs. | ||
| 626 | std::sort(panel_location_ids.begin(), panel_location_ids.end()); | ||
| 627 | |||
| 628 | std::map<int, int> solve_index_by_location_id; | ||
| 629 | for (int i = 0; i < panel_location_ids.size(); i++) { | ||
| 630 | solve_index_by_location_id[panel_location_ids[i]] = i; | ||
| 631 | } | ||
| 632 | |||
| 633 | for (Panel &panel : panels_) { | ||
| 634 | if (panel.ap_location_id != -1) { | ||
| 635 | panel.solve_index = solve_index_by_location_id[panel.ap_location_id]; | ||
| 636 | panel_by_solve_index_[panel.solve_index] = panel.id; | ||
| 637 | } | ||
| 638 | } | ||
| 639 | |||
| 493 | map_areas_.reserve(areas_config.size()); | 640 | map_areas_.reserve(areas_config.size()); |
| 494 | 641 | ||
| 495 | std::map<std::string, int> fold_areas; | 642 | std::map<std::string, int> fold_areas; |
| @@ -510,13 +657,28 @@ struct GameData { | |||
| 510 | // Only locations for the panels are kept here. | 657 | // Only locations for the panels are kept here. |
| 511 | std::map<std::string, std::tuple<int, int>> locations_by_name; | 658 | std::map<std::string, std::tuple<int, int>> locations_by_name; |
| 512 | 659 | ||
| 513 | for (const Panel &panel : panels_) { | 660 | for (Panel &panel : panels_) { |
| 514 | int room_id = panel.room; | 661 | int room_id = panel.room; |
| 515 | std::string room_name = rooms_[room_id].name; | 662 | std::string room_name = rooms_[room_id].name; |
| 516 | 663 | ||
| 517 | std::string area_name = room_name; | 664 | std::string area_name = room_name; |
| 518 | if (fold_areas.count(room_name)) { | 665 | std::string section_name = panel.name; |
| 519 | int fold_area_id = fold_areas[room_name]; | 666 | std::string location_name = room_name + " - " + panel.name; |
| 667 | |||
| 668 | if (!panel.location_name.empty()) { | ||
| 669 | location_name = panel.location_name; | ||
| 670 | |||
| 671 | size_t divider_pos = location_name.find(" - "); | ||
| 672 | if (divider_pos != std::string::npos) { | ||
| 673 | area_name = location_name.substr(0, divider_pos); | ||
| 674 | section_name = location_name.substr(divider_pos + 3); | ||
| 675 | } | ||
| 676 | } else { | ||
| 677 | panel.location_name = location_name; | ||
| 678 | } | ||
| 679 | |||
| 680 | if (fold_areas.count(area_name)) { | ||
| 681 | int fold_area_id = fold_areas[area_name]; | ||
| 520 | area_name = map_areas_[fold_area_id].name; | 682 | area_name = map_areas_[fold_area_id].name; |
| 521 | } | 683 | } |
| 522 | 684 | ||
| @@ -528,19 +690,23 @@ struct GameData { | |||
| 528 | } | 690 | } |
| 529 | } | 691 | } |
| 530 | 692 | ||
| 693 | if (room_name == "Starting Room") { | ||
| 694 | classification |= kLOCATION_SMALL_SPHERE_ONE; | ||
| 695 | } | ||
| 696 | |||
| 531 | int area_id = AddOrGetArea(area_name); | 697 | int area_id = AddOrGetArea(area_name); |
| 532 | MapArea &map_area = map_areas_[area_id]; | 698 | MapArea &map_area = map_areas_[area_id]; |
| 533 | // room field should be the original room ID | 699 | // room field should be the original room ID |
| 534 | map_area.locations.push_back( | 700 | map_area.locations.push_back({.name = section_name, |
| 535 | {.name = panel.name, | 701 | .ap_location_name = location_name, |
| 536 | .ap_location_name = room_name + " - " + panel.name, | 702 | .ap_location_id = panel.ap_location_id, |
| 537 | .ap_location_id = panel.ap_location_id, | 703 | .room = panel.room, |
| 538 | .room = panel.room, | 704 | .panels = {panel.id}, |
| 539 | .panels = {panel.id}, | 705 | .classification = classification, |
| 540 | .classification = classification, | 706 | .hunt = panel.hunt, |
| 541 | .hunt = panel.hunt}); | 707 | .single_panel = panel.id}); |
| 542 | locations_by_name[map_area.locations.back().ap_location_name] = { | 708 | locations_by_name[location_name] = {area_id, |
| 543 | area_id, map_area.locations.size() - 1}; | 709 | map_area.locations.size() - 1}; |
| 544 | } | 710 | } |
| 545 | 711 | ||
| 546 | for (int door_id : door_definition_order_) { | 712 | for (int door_id : door_definition_order_) { |
| @@ -591,14 +757,36 @@ struct GameData { | |||
| 591 | for (const Location &location : map_area.locations) { | 757 | for (const Location &location : map_area.locations) { |
| 592 | map_area.classification |= location.classification; | 758 | map_area.classification |= location.classification; |
| 593 | map_area.hunt |= location.hunt; | 759 | map_area.hunt |= location.hunt; |
| 760 | map_area.has_single_panel |= location.single_panel.has_value(); | ||
| 761 | } | ||
| 762 | } | ||
| 763 | |||
| 764 | for (const Room &room : rooms_) { | ||
| 765 | std::string area_name = room.name; | ||
| 766 | if (fold_areas.count(room.name)) { | ||
| 767 | int fold_area_id = fold_areas[room.name]; | ||
| 768 | area_name = map_areas_[fold_area_id].name; | ||
| 769 | } | ||
| 770 | |||
| 771 | if (!room.paintings.empty()) { | ||
| 772 | int area_id = AddOrGetArea(area_name); | ||
| 773 | MapArea &map_area = map_areas_[area_id]; | ||
| 774 | |||
| 775 | for (int painting_id : room.paintings) { | ||
| 776 | PaintingExit &painting_obj = paintings_.at(painting_id); | ||
| 777 | painting_obj.map_area = area_id; | ||
| 778 | if (painting_obj.entrance) { | ||
| 779 | map_area.paintings.push_back(painting_id); | ||
| 780 | } | ||
| 781 | } | ||
| 594 | } | 782 | } |
| 595 | } | 783 | } |
| 596 | 784 | ||
| 597 | // Report errors. | 785 | // Report errors. |
| 598 | for (const std::string &area : malconfigured_areas_) { | 786 | for (const std::string &area : malconfigured_areas_) { |
| 599 | wxLogError("Area data not found for: %s", area); | 787 | TrackerLog(fmt::format("Area data not found for: {}", area)); |
| 600 | } | 788 | } |
| 601 | 789 | ||
| 602 | // Read in subway items. | 790 | // Read in subway items. |
| 603 | YAML::Node subway_config = | 791 | YAML::Node subway_config = |
| 604 | YAML::LoadFile(GetAbsolutePath("assets/subway.yaml")); | 792 | YAML::LoadFile(GetAbsolutePath("assets/subway.yaml")); |
| @@ -613,13 +801,10 @@ struct GameData { | |||
| 613 | subway_it["door"].as<std::string>()); | 801 | subway_it["door"].as<std::string>()); |
| 614 | } | 802 | } |
| 615 | 803 | ||
| 616 | if (subway_it["paintings"]) { | 804 | if (subway_it["painting"]) { |
| 617 | for (const auto &painting_it : subway_it["paintings"]) { | 805 | std::string painting_id = subway_it["painting"].as<std::string>(); |
| 618 | std::string painting_id = painting_it.as<std::string>(); | 806 | subway_item.painting = painting_id; |
| 619 | 807 | subway_item_by_painting_[painting_id] = subway_item.id; | |
| 620 | subway_item.paintings.push_back(painting_id); | ||
| 621 | subway_item_by_painting_[painting_id] = subway_item.id; | ||
| 622 | } | ||
| 623 | } | 808 | } |
| 624 | 809 | ||
| 625 | if (subway_it["tags"]) { | 810 | if (subway_it["tags"]) { |
| @@ -628,6 +813,18 @@ struct GameData { | |||
| 628 | } | 813 | } |
| 629 | } | 814 | } |
| 630 | 815 | ||
| 816 | if (subway_it["entrances"]) { | ||
| 817 | for (const auto &entrance_it : subway_it["entrances"]) { | ||
| 818 | subway_item.entrances.push_back(entrance_it.as<std::string>()); | ||
| 819 | } | ||
| 820 | } | ||
| 821 | |||
| 822 | if (subway_it["exits"]) { | ||
| 823 | for (const auto &exit_it : subway_it["exits"]) { | ||
| 824 | subway_item.exits.push_back(exit_it.as<std::string>()); | ||
| 825 | } | ||
| 826 | } | ||
| 827 | |||
| 631 | if (subway_it["sunwarp"]) { | 828 | if (subway_it["sunwarp"]) { |
| 632 | SubwaySunwarp sunwarp; | 829 | SubwaySunwarp sunwarp; |
| 633 | sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>(); | 830 | sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>(); |
| @@ -654,6 +851,10 @@ struct GameData { | |||
| 654 | subway_item.special = subway_it["special"].as<std::string>(); | 851 | subway_item.special = subway_it["special"].as<std::string>(); |
| 655 | } | 852 | } |
| 656 | 853 | ||
| 854 | if (subway_it["tilted"]) { | ||
| 855 | subway_item.tilted = subway_it["tilted"].as<bool>(); | ||
| 856 | } | ||
| 857 | |||
| 657 | subway_items_.push_back(subway_item); | 858 | subway_items_.push_back(subway_item); |
| 658 | } | 859 | } |
| 659 | 860 | ||
| @@ -667,7 +868,7 @@ struct GameData { | |||
| 667 | 868 | ||
| 668 | for (const auto &[tag, items] : subway_tags) { | 869 | for (const auto &[tag, items] : subway_tags) { |
| 669 | if (items.size() == 1) { | 870 | if (items.size() == 1) { |
| 670 | wxLogWarning("Singleton subway item tag: %s", tag); | 871 | TrackerLog(fmt::format("Singleton subway item tag: {}", tag)); |
| 671 | } | 872 | } |
| 672 | } | 873 | } |
| 673 | } | 874 | } |
| @@ -687,7 +888,8 @@ struct GameData { | |||
| 687 | if (!door_by_id_.count(full_name)) { | 888 | if (!door_by_id_.count(full_name)) { |
| 688 | int door_id = doors_.size(); | 889 | int door_id = doors_.size(); |
| 689 | door_by_id_[full_name] = doors_.size(); | 890 | door_by_id_[full_name] = doors_.size(); |
| 690 | doors_.push_back({.id = door_id, .room = AddOrGetRoom(room), .name = door}); | 891 | doors_.push_back( |
| 892 | {.id = door_id, .room = AddOrGetRoom(room), .name = door}); | ||
| 691 | } | 893 | } |
| 692 | 894 | ||
| 693 | return door_by_id_[full_name]; | 895 | return door_by_id_[full_name]; |
| @@ -706,6 +908,18 @@ struct GameData { | |||
| 706 | return panel_by_id_[full_name]; | 908 | return panel_by_id_[full_name]; |
| 707 | } | 909 | } |
| 708 | 910 | ||
| 911 | int AddOrGetPanelDoor(std::string room, std::string panel) { | ||
| 912 | std::string full_name = room + " - " + panel; | ||
| 913 | |||
| 914 | if (!panel_doors_by_id_.count(full_name)) { | ||
| 915 | int panel_door_id = panel_doors_.size(); | ||
| 916 | panel_doors_by_id_[full_name] = panel_door_id; | ||
| 917 | panel_doors_.push_back({}); | ||
| 918 | } | ||
| 919 | |||
| 920 | return panel_doors_by_id_[full_name]; | ||
| 921 | } | ||
| 922 | |||
| 709 | int AddOrGetArea(std::string area) { | 923 | int AddOrGetArea(std::string area) { |
| 710 | if (!area_by_id_.count(area)) { | 924 | if (!area_by_id_.count(area)) { |
| 711 | if (loaded_area_data_) { | 925 | if (loaded_area_data_) { |
| @@ -719,6 +933,16 @@ struct GameData { | |||
| 719 | 933 | ||
| 720 | return area_by_id_[area]; | 934 | return area_by_id_[area]; |
| 721 | } | 935 | } |
| 936 | |||
| 937 | int AddOrGetPainting(std::string internal_id) { | ||
| 938 | if (!painting_by_id_.count(internal_id)) { | ||
| 939 | int painting_id = paintings_.size(); | ||
| 940 | painting_by_id_[internal_id] = painting_id; | ||
| 941 | paintings_.push_back({.id = painting_id, .internal_id = internal_id}); | ||
| 942 | } | ||
| 943 | |||
| 944 | return painting_by_id_[internal_id]; | ||
| 945 | } | ||
| 722 | }; | 946 | }; |
| 723 | 947 | ||
| 724 | GameData &GetState() { | 948 | GameData &GetState() { |
| @@ -728,7 +952,12 @@ GameData &GetState() { | |||
| 728 | 952 | ||
| 729 | } // namespace | 953 | } // namespace |
| 730 | 954 | ||
| 731 | bool SubwaySunwarp::operator<(const SubwaySunwarp& rhs) const { | 955 | bool SubwayItem::HasWarps() const { |
| 956 | return !(this->tags.empty() && this->entrances.empty() && | ||
| 957 | this->exits.empty()); | ||
| 958 | } | ||
| 959 | |||
| 960 | bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const { | ||
| 732 | return std::tie(dots, type) < std::tie(rhs.dots, rhs.type); | 961 | return std::tie(dots, type) < std::tie(rhs.dots, rhs.type); |
| 733 | } | 962 | } |
| 734 | 963 | ||
| @@ -746,6 +975,10 @@ const std::vector<Door> &GD_GetDoors() { return GetState().doors_; } | |||
| 746 | 975 | ||
| 747 | const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); } | 976 | const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); } |
| 748 | 977 | ||
| 978 | const PanelDoor &GD_GetPanelDoor(int panel_door_id) { | ||
| 979 | return GetState().panel_doors_.at(panel_door_id); | ||
| 980 | } | ||
| 981 | |||
| 749 | int GD_GetDoorByName(const std::string &name) { | 982 | int GD_GetDoorByName(const std::string &name) { |
| 750 | return GetState().door_by_id_.at(name); | 983 | return GetState().door_by_id_.at(name); |
| 751 | } | 984 | } |
| @@ -754,8 +987,20 @@ const Panel &GD_GetPanel(int panel_id) { | |||
| 754 | return GetState().panels_.at(panel_id); | 987 | return GetState().panels_.at(panel_id); |
| 755 | } | 988 | } |
| 756 | 989 | ||
| 757 | int GD_GetRoomForPainting(const std::string &painting_id) { | 990 | int GD_GetPanelBySolveIndex(int solve_index) { |
| 758 | return GetState().room_by_painting_.at(painting_id); | 991 | return GetState().panel_by_solve_index_.at(solve_index); |
| 992 | } | ||
| 993 | |||
| 994 | const std::vector<PaintingExit> &GD_GetPaintings() { | ||
| 995 | return GetState().paintings_; | ||
| 996 | } | ||
| 997 | |||
| 998 | const PaintingExit &GD_GetPaintingExit(int painting_id) { | ||
| 999 | return GetState().paintings_.at(painting_id); | ||
| 1000 | } | ||
| 1001 | |||
| 1002 | int GD_GetPaintingByName(const std::string &name) { | ||
| 1003 | return GetState().painting_by_id_.at(name); | ||
| 759 | } | 1004 | } |
| 760 | 1005 | ||
| 761 | const std::vector<int> &GD_GetAchievementPanels() { | 1006 | const std::vector<int> &GD_GetAchievementPanels() { |
| @@ -782,15 +1027,48 @@ const SubwayItem &GD_GetSubwayItem(int id) { | |||
| 782 | return GetState().subway_items_.at(id); | 1027 | return GetState().subway_items_.at(id); |
| 783 | } | 1028 | } |
| 784 | 1029 | ||
| 785 | int GD_GetSubwayItemForPainting(const std::string& painting_id) { | 1030 | std::optional<int> GD_GetSubwayItemForPainting(const std::string &painting_id) { |
| 786 | #ifndef NDEBUG | 1031 | if (GetState().subway_item_by_painting_.count(painting_id)) { |
| 787 | if (!GetState().subway_item_by_painting_.count(painting_id)) { | 1032 | return GetState().subway_item_by_painting_.at(painting_id); |
| 788 | wxLogError("No subway item for painting %s", painting_id); | ||
| 789 | } | 1033 | } |
| 790 | #endif | 1034 | return std::nullopt; |
| 791 | return GetState().subway_item_by_painting_.at(painting_id); | ||
| 792 | } | 1035 | } |
| 793 | 1036 | ||
| 794 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { | 1037 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { |
| 795 | return GetState().subway_item_by_sunwarp_.at(sunwarp); | 1038 | return GetState().subway_item_by_sunwarp_.at(sunwarp); |
| 796 | } | 1039 | } |
| 1040 | |||
| 1041 | std::string GD_GetItemName(int id) { | ||
| 1042 | auto it = GetState().item_by_ap_id_.find(id); | ||
| 1043 | if (it != GetState().item_by_ap_id_.end()) { | ||
| 1044 | return it->second; | ||
| 1045 | } else { | ||
| 1046 | return "Unknown"; | ||
| 1047 | } | ||
| 1048 | } | ||
| 1049 | |||
| 1050 | LingoColor GetLingoColorForString(const std::string &str) { | ||
| 1051 | if (str == "black") { | ||
| 1052 | return LingoColor::kBlack; | ||
| 1053 | } else if (str == "red") { | ||
| 1054 | return LingoColor::kRed; | ||
| 1055 | } else if (str == "blue") { | ||
| 1056 | return LingoColor::kBlue; | ||
| 1057 | } else if (str == "yellow") { | ||
| 1058 | return LingoColor::kYellow; | ||
| 1059 | } else if (str == "orange") { | ||
| 1060 | return LingoColor::kOrange; | ||
| 1061 | } else if (str == "green") { | ||
| 1062 | return LingoColor::kGreen; | ||
| 1063 | } else if (str == "gray") { | ||
| 1064 | return LingoColor::kGray; | ||
| 1065 | } else if (str == "brown") { | ||
| 1066 | return LingoColor::kBrown; | ||
| 1067 | } else if (str == "purple") { | ||
| 1068 | return LingoColor::kPurple; | ||
| 1069 | } else { | ||
| 1070 | TrackerLog(fmt::format("Invalid color: {}", str)); | ||
| 1071 | |||
| 1072 | return LingoColor::kNone; | ||
| 1073 | } | ||
| 1074 | } | ||
| diff --git a/src/game_data.h b/src/game_data.h index 3afaec3..8d3db4b 100644 --- a/src/game_data.h +++ b/src/game_data.h | |||
| @@ -22,6 +22,7 @@ enum class LingoColor { | |||
| 22 | constexpr int kLOCATION_NORMAL = 1; | 22 | constexpr int kLOCATION_NORMAL = 1; |
| 23 | constexpr int kLOCATION_REDUCED = 2; | 23 | constexpr int kLOCATION_REDUCED = 2; |
| 24 | constexpr int kLOCATION_INSANITY = 4; | 24 | constexpr int kLOCATION_INSANITY = 4; |
| 25 | constexpr int kLOCATION_SMALL_SPHERE_ONE = 8; | ||
| 25 | 26 | ||
| 26 | enum class EntranceType { | 27 | enum class EntranceType { |
| 27 | kNormal, | 28 | kNormal, |
| @@ -30,6 +31,7 @@ enum class EntranceType { | |||
| 30 | kWarp, | 31 | kWarp, |
| 31 | kPilgrimage, | 32 | kPilgrimage, |
| 32 | kCrossroadsRoofAccess, | 33 | kCrossroadsRoofAccess, |
| 34 | kStaticPainting, | ||
| 33 | }; | 35 | }; |
| 34 | 36 | ||
| 35 | enum class DoorType { | 37 | enum class DoorType { |
| @@ -42,6 +44,7 @@ struct Panel { | |||
| 42 | int id; | 44 | int id; |
| 43 | int room; | 45 | int room; |
| 44 | std::string name; | 46 | std::string name; |
| 47 | std::string nodepath; | ||
| 45 | std::vector<LingoColor> colors; | 48 | std::vector<LingoColor> colors; |
| 46 | std::vector<int> required_rooms; | 49 | std::vector<int> required_rooms; |
| 47 | std::vector<int> required_doors; | 50 | std::vector<int> required_doors; |
| @@ -50,9 +53,12 @@ struct Panel { | |||
| 50 | bool exclude_reduce = false; | 53 | bool exclude_reduce = false; |
| 51 | bool achievement = false; | 54 | bool achievement = false; |
| 52 | std::string achievement_name; | 55 | std::string achievement_name; |
| 56 | std::string location_name; | ||
| 53 | bool non_counting = false; | 57 | bool non_counting = false; |
| 54 | int ap_location_id = -1; | 58 | int ap_location_id = -1; |
| 55 | bool hunt = false; | 59 | bool hunt = false; |
| 60 | int panel_door = -1; | ||
| 61 | int solve_index = -1; | ||
| 56 | }; | 62 | }; |
| 57 | 63 | ||
| 58 | struct ProgressiveRequirement { | 64 | struct ProgressiveRequirement { |
| @@ -80,21 +86,34 @@ struct Door { | |||
| 80 | DoorType type = DoorType::kNormal; | 86 | DoorType type = DoorType::kNormal; |
| 81 | }; | 87 | }; |
| 82 | 88 | ||
| 89 | struct PanelDoor { | ||
| 90 | int ap_item_id = -1; | ||
| 91 | int group_ap_item_id = -1; | ||
| 92 | std::vector<ProgressiveRequirement> progressives; | ||
| 93 | std::string item_name; | ||
| 94 | }; | ||
| 95 | |||
| 83 | struct Exit { | 96 | struct Exit { |
| 97 | int source_room; | ||
| 84 | int destination_room; | 98 | int destination_room; |
| 85 | std::optional<int> door; | 99 | std::optional<int> door; |
| 86 | EntranceType type = EntranceType::kNormal; | 100 | EntranceType type = EntranceType::kNormal; |
| 87 | }; | 101 | }; |
| 88 | 102 | ||
| 89 | struct PaintingExit { | 103 | struct PaintingExit { |
| 90 | std::string id; | 104 | int id; |
| 105 | int room; | ||
| 106 | std::string internal_id; | ||
| 107 | std::string display_name; | ||
| 91 | std::optional<int> door; | 108 | std::optional<int> door; |
| 109 | bool entrance = false; | ||
| 110 | int map_area; | ||
| 92 | }; | 111 | }; |
| 93 | 112 | ||
| 94 | struct Room { | 113 | struct Room { |
| 95 | std::string name; | 114 | std::string name; |
| 96 | std::vector<Exit> exits; | 115 | std::vector<Exit> exits; |
| 97 | std::vector<PaintingExit> paintings; | 116 | std::vector<int> paintings; |
| 98 | std::vector<int> sunwarps; | 117 | std::vector<int> sunwarps; |
| 99 | std::vector<int> panels; | 118 | std::vector<int> panels; |
| 100 | }; | 119 | }; |
| @@ -107,16 +126,19 @@ struct Location { | |||
| 107 | std::vector<int> panels; | 126 | std::vector<int> panels; |
| 108 | int classification = 0; | 127 | int classification = 0; |
| 109 | bool hunt = false; | 128 | bool hunt = false; |
| 129 | std::optional<int> single_panel; | ||
| 110 | }; | 130 | }; |
| 111 | 131 | ||
| 112 | struct MapArea { | 132 | struct MapArea { |
| 113 | int id; | 133 | int id; |
| 114 | std::string name; | 134 | std::string name; |
| 115 | std::vector<Location> locations; | 135 | std::vector<Location> locations; |
| 136 | std::vector<int> paintings; | ||
| 116 | int map_x; | 137 | int map_x; |
| 117 | int map_y; | 138 | int map_y; |
| 118 | int classification = 0; | 139 | int classification = 0; |
| 119 | bool hunt = false; | 140 | bool hunt = false; |
| 141 | bool has_single_panel = false; | ||
| 120 | }; | 142 | }; |
| 121 | 143 | ||
| 122 | enum class SubwaySunwarpType { | 144 | enum class SubwaySunwarpType { |
| @@ -136,11 +158,16 @@ struct SubwayItem { | |||
| 136 | int id; | 158 | int id; |
| 137 | int x; | 159 | int x; |
| 138 | int y; | 160 | int y; |
| 161 | bool tilted = false; | ||
| 139 | std::optional<int> door; | 162 | std::optional<int> door; |
| 140 | std::vector<std::string> paintings; | 163 | std::optional<std::string> painting; |
| 141 | std::vector<std::string> tags; | 164 | std::vector<std::string> tags; // 2-way teleports |
| 165 | std::vector<std::string> entrances; // teleport entrances | ||
| 166 | std::vector<std::string> exits; // teleport exits | ||
| 142 | std::optional<SubwaySunwarp> sunwarp; | 167 | std::optional<SubwaySunwarp> sunwarp; |
| 143 | std::optional<std::string> special; | 168 | std::optional<std::string> special; |
| 169 | |||
| 170 | bool HasWarps() const; | ||
| 144 | }; | 171 | }; |
| 145 | 172 | ||
| 146 | const std::vector<MapArea>& GD_GetMapAreas(); | 173 | const std::vector<MapArea>& GD_GetMapAreas(); |
| @@ -151,14 +178,21 @@ const std::vector<Door>& GD_GetDoors(); | |||
| 151 | const Door& GD_GetDoor(int door_id); | 178 | const Door& GD_GetDoor(int door_id); |
| 152 | int GD_GetDoorByName(const std::string& name); | 179 | int GD_GetDoorByName(const std::string& name); |
| 153 | const Panel& GD_GetPanel(int panel_id); | 180 | const Panel& GD_GetPanel(int panel_id); |
| 154 | int GD_GetRoomForPainting(const std::string& painting_id); | 181 | int GD_GetPanelBySolveIndex(int solve_index); |
| 182 | const PanelDoor& GD_GetPanelDoor(int panel_door_id); | ||
| 183 | const std::vector<PaintingExit>& GD_GetPaintings(); | ||
| 184 | const PaintingExit& GD_GetPaintingExit(int painting_id); | ||
| 185 | int GD_GetPaintingByName(const std::string& name); | ||
| 155 | const std::vector<int>& GD_GetAchievementPanels(); | 186 | const std::vector<int>& GD_GetAchievementPanels(); |
| 156 | int GD_GetItemIdForColor(LingoColor color); | 187 | int GD_GetItemIdForColor(LingoColor color); |
| 157 | const std::vector<int>& GD_GetSunwarpDoors(); | 188 | const std::vector<int>& GD_GetSunwarpDoors(); |
| 158 | int GD_GetRoomForSunwarp(int index); | 189 | int GD_GetRoomForSunwarp(int index); |
| 159 | const std::vector<SubwayItem>& GD_GetSubwayItems(); | 190 | const std::vector<SubwayItem>& GD_GetSubwayItems(); |
| 160 | const SubwayItem& GD_GetSubwayItem(int id); | 191 | const SubwayItem& GD_GetSubwayItem(int id); |
| 161 | int GD_GetSubwayItemForPainting(const std::string& painting_id); | 192 | std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id); |
| 162 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); | 193 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); |
| 194 | std::string GD_GetItemName(int id); | ||
| 195 | |||
| 196 | LingoColor GetLingoColorForString(const std::string& str); | ||
| 163 | 197 | ||
| 164 | #endif /* end of include guard: GAME_DATA_H_9C42AC51 */ | 198 | #endif /* end of include guard: GAME_DATA_H_9C42AC51 */ |
| diff --git a/src/global.cpp b/src/global.cpp index 1eb3f8d..63f4a19 100644 --- a/src/global.cpp +++ b/src/global.cpp | |||
| @@ -26,17 +26,19 @@ std::string GetAbsolutePath(std::string_view path) { | |||
| 26 | return (GetExecutableDirectory() / path).string(); | 26 | return (GetExecutableDirectory() / path).string(); |
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | bool IsLocationWinCondition(const Location& location) { | 29 | std::string GetWinCondition() { |
| 30 | switch (AP_GetVictoryCondition()) { | 30 | switch (AP_GetVictoryCondition()) { |
| 31 | case kTHE_END: | 31 | case kTHE_END: |
| 32 | return location.ap_location_name == | 32 | return "Orange Tower Seventh Floor - THE END"; |
| 33 | "Orange Tower Seventh Floor - THE END"; | ||
| 34 | case kTHE_MASTER: | 33 | case kTHE_MASTER: |
| 35 | return location.ap_location_name == | 34 | return "Orange Tower Seventh Floor - THE MASTER"; |
| 36 | "Orange Tower Seventh Floor - THE MASTER"; | ||
| 37 | case kLEVEL_2: | 35 | case kLEVEL_2: |
| 38 | return location.ap_location_name == "Second Room - LEVEL 2"; | 36 | return "Second Room - LEVEL 2"; |
| 39 | case kPILGRIMAGE: | 37 | case kPILGRIMAGE: |
| 40 | return location.ap_location_name == "Pilgrim Antechamber - PILGRIM"; | 38 | return "Pilgrim Antechamber - PILGRIM"; |
| 41 | } | 39 | } |
| 42 | } | 40 | } |
| 41 | |||
| 42 | bool IsLocationWinCondition(const Location& location) { | ||
| 43 | return location.ap_location_name == GetWinCondition(); | ||
| 44 | } | ||
| diff --git a/src/global.h b/src/global.h index 31ebde3..bdfa7ae 100644 --- a/src/global.h +++ b/src/global.h | |||
| @@ -10,6 +10,8 @@ const std::filesystem::path& GetExecutableDirectory(); | |||
| 10 | 10 | ||
| 11 | std::string GetAbsolutePath(std::string_view path); | 11 | std::string GetAbsolutePath(std::string_view path); |
| 12 | 12 | ||
| 13 | std::string GetWinCondition(); | ||
| 14 | |||
| 13 | bool IsLocationWinCondition(const Location& location); | 15 | bool IsLocationWinCondition(const Location& location); |
| 14 | 16 | ||
| 15 | #endif /* end of include guard: GLOBAL_H_44945DBA */ | 17 | #endif /* end of include guard: GLOBAL_H_44945DBA */ |
| diff --git a/src/icons.cpp b/src/icons.cpp new file mode 100644 index 0000000..87ba037 --- /dev/null +++ b/src/icons.cpp | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | #include "icons.h" | ||
| 2 | |||
| 3 | #include "global.h" | ||
| 4 | |||
| 5 | const wxBitmap* IconCache::GetIcon(const std::string& filename, wxSize size) { | ||
| 6 | std::tuple<std::string, int, int> key = {filename, size.x, size.y}; | ||
| 7 | |||
| 8 | if (!icons_.count(key)) { | ||
| 9 | icons_[key] = | ||
| 10 | wxBitmap(wxImage(GetAbsolutePath(filename).c_str(), | ||
| 11 | wxBITMAP_TYPE_PNG) | ||
| 12 | .Scale(size.x, size.y)); | ||
| 13 | } | ||
| 14 | |||
| 15 | return &icons_.at(key); | ||
| 16 | } | ||
| 17 | |||
| 18 | static IconCache* ICON_CACHE_INSTANCE = nullptr; | ||
| 19 | |||
| 20 | void SetTheIconCache(IconCache* instance) { ICON_CACHE_INSTANCE = instance; } | ||
| 21 | |||
| 22 | IconCache& GetTheIconCache() { return *ICON_CACHE_INSTANCE; } | ||
| diff --git a/src/icons.h b/src/icons.h new file mode 100644 index 0000000..23dca2a --- /dev/null +++ b/src/icons.h | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | #ifndef ICONS_H_B95159A6 | ||
| 2 | #define ICONS_H_B95159A6 | ||
| 3 | |||
| 4 | #include <wx/wxprec.h> | ||
| 5 | |||
| 6 | #ifndef WX_PRECOMP | ||
| 7 | #include <wx/wx.h> | ||
| 8 | #endif | ||
| 9 | |||
| 10 | #include <map> | ||
| 11 | #include <string> | ||
| 12 | #include <tuple> | ||
| 13 | |||
| 14 | class IconCache { | ||
| 15 | public: | ||
| 16 | const wxBitmap* GetIcon(const std::string& filename, wxSize size); | ||
| 17 | |||
| 18 | private: | ||
| 19 | std::map<std::tuple<std::string, int, int>, wxBitmap> icons_; | ||
| 20 | }; | ||
| 21 | |||
| 22 | void SetTheIconCache(IconCache* instance); | ||
| 23 | IconCache& GetTheIconCache(); | ||
| 24 | |||
| 25 | #endif /* end of include guard: ICONS_H_B95159A6 */ | ||
| diff --git a/src/ipc_dialog.cpp b/src/ipc_dialog.cpp new file mode 100644 index 0000000..6763b7f --- /dev/null +++ b/src/ipc_dialog.cpp | |||
| @@ -0,0 +1,54 @@ | |||
| 1 | #include "ipc_dialog.h" | ||
| 2 | |||
| 3 | #include "tracker_config.h" | ||
| 4 | |||
| 5 | constexpr const char* kDefaultIpcAddress = "ws://127.0.0.1:41253"; | ||
| 6 | |||
| 7 | IpcDialog::IpcDialog() : wxDialog(nullptr, wxID_ANY, "Connect to game") { | ||
| 8 | std::string address_value; | ||
| 9 | if (GetTrackerConfig().ipc_address.empty()) { | ||
| 10 | address_value = kDefaultIpcAddress; | ||
| 11 | } else { | ||
| 12 | address_value = GetTrackerConfig().ipc_address; | ||
| 13 | } | ||
| 14 | |||
| 15 | address_box_ = new wxTextCtrl(this, -1, wxString::FromUTF8(address_value), | ||
| 16 | wxDefaultPosition, FromDIP(wxSize{300, -1})); | ||
| 17 | |||
| 18 | wxButton* reset_button = new wxButton(this, -1, "Use Default"); | ||
| 19 | reset_button->Bind(wxEVT_BUTTON, &IpcDialog::OnResetClicked, this); | ||
| 20 | |||
| 21 | wxFlexGridSizer* form_sizer = | ||
| 22 | new wxFlexGridSizer(3, FromDIP(10), FromDIP(10)); | ||
| 23 | form_sizer->Add( | ||
| 24 | new wxStaticText(this, -1, "Address:"), | ||
| 25 | wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); | ||
| 26 | form_sizer->Add(address_box_, wxSizerFlags().Expand()); | ||
| 27 | form_sizer->Add(reset_button); | ||
| 28 | |||
| 29 | wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL); | ||
| 30 | wxStaticText* top_text = new wxStaticText(this, -1, ""); | ||
| 31 | top_sizer->Add(top_text, wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder().Expand()); | ||
| 32 | top_sizer->Add(form_sizer, wxSizerFlags().DoubleBorder().Expand()); | ||
| 33 | top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), | ||
| 34 | wxSizerFlags().Border().Center()); | ||
| 35 | |||
| 36 | SetSizer(top_sizer); | ||
| 37 | Layout(); | ||
| 38 | Fit(); | ||
| 39 | |||
| 40 | int width = top_text->GetClientSize().GetWidth(); | ||
| 41 | top_text->SetLabel( | ||
| 42 | "This allows you to connect to a running Lingo game and track non-multiworld " | ||
| 43 | "state, such as the player's position and what panels are solved. Unless " | ||
| 44 | "you are doing something weird, the default value for the address is " | ||
| 45 | "probably correct."); | ||
| 46 | top_text->Wrap(width); | ||
| 47 | |||
| 48 | Fit(); | ||
| 49 | Center(); | ||
| 50 | } | ||
| 51 | |||
| 52 | void IpcDialog::OnResetClicked(wxCommandEvent& event) { | ||
| 53 | address_box_->SetValue(kDefaultIpcAddress); | ||
| 54 | } | ||
| diff --git a/src/ipc_dialog.h b/src/ipc_dialog.h new file mode 100644 index 0000000..a8c4512 --- /dev/null +++ b/src/ipc_dialog.h | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | #ifndef IPC_DIALOG_H_F4C5680C | ||
| 2 | #define IPC_DIALOG_H_F4C5680C | ||
| 3 | |||
| 4 | #include <wx/wxprec.h> | ||
| 5 | |||
| 6 | #ifndef WX_PRECOMP | ||
| 7 | #include <wx/wx.h> | ||
| 8 | #endif | ||
| 9 | |||
| 10 | #include <string> | ||
| 11 | |||
| 12 | class IpcDialog : public wxDialog { | ||
| 13 | public: | ||
| 14 | IpcDialog(); | ||
| 15 | |||
| 16 | std::string GetIpcAddress() { return address_box_->GetValue().utf8_string(); } | ||
| 17 | |||
| 18 | private: | ||
| 19 | void OnResetClicked(wxCommandEvent& event); | ||
| 20 | |||
| 21 | wxTextCtrl* address_box_; | ||
| 22 | }; | ||
| 23 | |||
| 24 | #endif /* end of include guard: IPC_DIALOG_H_F4C5680C */ | ||
| diff --git a/src/ipc_state.cpp b/src/ipc_state.cpp new file mode 100644 index 0000000..6e2a440 --- /dev/null +++ b/src/ipc_state.cpp | |||
| @@ -0,0 +1,367 @@ | |||
| 1 | #include "ipc_state.h" | ||
| 2 | |||
| 3 | #define _WEBSOCKETPP_CPP11_STRICT_ | ||
| 4 | |||
| 5 | #include <fmt/core.h> | ||
| 6 | |||
| 7 | #include <chrono> | ||
| 8 | #include <memory> | ||
| 9 | #include <mutex> | ||
| 10 | #include <nlohmann/json.hpp> | ||
| 11 | #include <optional> | ||
| 12 | #include <set> | ||
| 13 | #include <string> | ||
| 14 | #include <thread> | ||
| 15 | #include <tuple> | ||
| 16 | #include <wswrap.hpp> | ||
| 17 | |||
| 18 | #include "ap_state.h" | ||
| 19 | #include "logger.h" | ||
| 20 | #include "tracker_frame.h" | ||
| 21 | |||
| 22 | namespace { | ||
| 23 | |||
| 24 | struct IPCState { | ||
| 25 | std::mutex state_mutex; | ||
| 26 | TrackerFrame* tracker_frame = nullptr; | ||
| 27 | |||
| 28 | // Protected state | ||
| 29 | bool initialized = false; | ||
| 30 | std::string address; | ||
| 31 | bool should_disconnect = false; | ||
| 32 | |||
| 33 | std::optional<std::string> status_message; | ||
| 34 | |||
| 35 | bool slot_matches = false; | ||
| 36 | std::string tracker_ap_server; | ||
| 37 | std::string tracker_ap_user; | ||
| 38 | std::string game_ap_server; | ||
| 39 | std::string game_ap_user; | ||
| 40 | |||
| 41 | std::optional<std::tuple<int, int>> player_position; | ||
| 42 | |||
| 43 | // Thread state | ||
| 44 | std::unique_ptr<wswrap::WS> ws; | ||
| 45 | bool connected = false; | ||
| 46 | |||
| 47 | void SetTrackerFrame(TrackerFrame* frame) { tracker_frame = frame; } | ||
| 48 | |||
| 49 | void Connect(std::string a) { | ||
| 50 | // This is the main concurrency concern, as it mutates protected state in an | ||
| 51 | // important way. Thread() is documented with how it interacts with this | ||
| 52 | // function. | ||
| 53 | std::lock_guard state_guard(state_mutex); | ||
| 54 | |||
| 55 | if (!initialized) { | ||
| 56 | std::thread([this]() { Thread(); }).detach(); | ||
| 57 | |||
| 58 | initialized = true; | ||
| 59 | } else if (address != a) { | ||
| 60 | should_disconnect = true; | ||
| 61 | } | ||
| 62 | |||
| 63 | address = a; | ||
| 64 | } | ||
| 65 | |||
| 66 | std::optional<std::string> GetStatusMessage() { | ||
| 67 | std::lock_guard state_guard(state_mutex); | ||
| 68 | |||
| 69 | return status_message; | ||
| 70 | } | ||
| 71 | |||
| 72 | void SetTrackerSlot(std::string server, std::string user) { | ||
| 73 | // This function is called from the APState thread, not the main thread, and | ||
| 74 | // it mutates protected state. It only really competes with OnMessage(), when | ||
| 75 | // a "Connect" message is received. If this is called right before, and the | ||
| 76 | // tracker slot does not match the old game slot, it will initiate a | ||
| 77 | // disconnect, and then the OnMessage() handler will see should_disconnect | ||
| 78 | // and stop processing the "Connect" message. If this is called right after | ||
| 79 | // and the slot does not match, IPC will disconnect, which is tolerable. | ||
| 80 | std::lock_guard state_guard(state_mutex); | ||
| 81 | |||
| 82 | tracker_ap_server = std::move(server); | ||
| 83 | tracker_ap_user = std::move(user); | ||
| 84 | |||
| 85 | CheckIfSlotMatches(); | ||
| 86 | |||
| 87 | if (!slot_matches) { | ||
| 88 | should_disconnect = true; | ||
| 89 | address.clear(); | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | bool IsConnected() { | ||
| 94 | std::lock_guard state_guard(state_mutex); | ||
| 95 | |||
| 96 | return slot_matches; | ||
| 97 | } | ||
| 98 | |||
| 99 | std::optional<std::tuple<int, int>> GetPlayerPosition() { | ||
| 100 | std::lock_guard state_guard(state_mutex); | ||
| 101 | |||
| 102 | return player_position; | ||
| 103 | } | ||
| 104 | |||
| 105 | private: | ||
| 106 | void Thread() { | ||
| 107 | for (;;) { | ||
| 108 | // initialized is definitely true because it is set to true when the thread | ||
| 109 | // is created and only set to false within this block, when the thread is | ||
| 110 | // killed. Thus, a call to Connect would always at most set | ||
| 111 | // should_disconnect and address. If this happens before this block, it is | ||
| 112 | // as if we are starting from a new thread anyway because should_disconnect | ||
| 113 | // is immediately reset. If a call to Connect happens after this block, | ||
| 114 | // then a connection attempt will be made to the wrong address, but the | ||
| 115 | // thread will grab the mutex right after this and back out the wrong | ||
| 116 | // connection. | ||
| 117 | std::string ipc_address; | ||
| 118 | { | ||
| 119 | std::lock_guard state_guard(state_mutex); | ||
| 120 | |||
| 121 | SetStatusMessage("Disconnected from game."); | ||
| 122 | |||
| 123 | should_disconnect = false; | ||
| 124 | |||
| 125 | slot_matches = false; | ||
| 126 | game_ap_server.clear(); | ||
| 127 | game_ap_user.clear(); | ||
| 128 | |||
| 129 | player_position = std::nullopt; | ||
| 130 | |||
| 131 | if (address.empty()) { | ||
| 132 | initialized = false; | ||
| 133 | return; | ||
| 134 | } | ||
| 135 | |||
| 136 | ipc_address = address; | ||
| 137 | |||
| 138 | SetStatusMessage("Connecting to game..."); | ||
| 139 | } | ||
| 140 | |||
| 141 | int backoff_amount = 0; | ||
| 142 | |||
| 143 | TrackerLog(fmt::format("Looking for game over IPC ({})...", ipc_address)); | ||
| 144 | |||
| 145 | while (!connected) { | ||
| 146 | if (TryConnect(ipc_address)) { | ||
| 147 | int backoff_limit = (backoff_amount + 1) * 10; | ||
| 148 | |||
| 149 | for (int i = 0; i < backoff_limit && !connected; i++) { | ||
| 150 | // If Connect is called right before this block, we will see and | ||
| 151 | // handle should_disconnect. If it is called right after, we will do | ||
| 152 | // one bad poll, one sleep, and then grab the mutex again right | ||
| 153 | // after. | ||
| 154 | { | ||
| 155 | std::lock_guard state_guard(state_mutex); | ||
| 156 | if (should_disconnect) { | ||
| 157 | break; | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | ws->poll(); | ||
| 162 | |||
| 163 | // Back off | ||
| 164 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | ||
| 165 | } | ||
| 166 | |||
| 167 | backoff_amount++; | ||
| 168 | } else { | ||
| 169 | std::lock_guard state_guard(state_mutex); | ||
| 170 | |||
| 171 | if (!should_disconnect) { | ||
| 172 | should_disconnect = true; | ||
| 173 | address.clear(); | ||
| 174 | |||
| 175 | SetStatusMessage("Disconnected from game."); | ||
| 176 | } | ||
| 177 | |||
| 178 | break; | ||
| 179 | } | ||
| 180 | |||
| 181 | // If Connect is called right before this block, we will see and handle | ||
| 182 | // should_disconnect. If it is called right after, and the connection | ||
| 183 | // was unsuccessful, we will grab the mutex after one bad connection | ||
| 184 | // attempt. If the connection was successful, we grab the mutex right | ||
| 185 | // after exiting the loop. | ||
| 186 | bool show_error = false; | ||
| 187 | { | ||
| 188 | std::lock_guard state_guard(state_mutex); | ||
| 189 | |||
| 190 | if (should_disconnect) { | ||
| 191 | break; | ||
| 192 | } else if (!connected) { | ||
| 193 | if (backoff_amount >= 10) { | ||
| 194 | should_disconnect = true; | ||
| 195 | address.clear(); | ||
| 196 | |||
| 197 | SetStatusMessage("Disconnected from game."); | ||
| 198 | |||
| 199 | show_error = true; | ||
| 200 | } else { | ||
| 201 | TrackerLog(fmt::format("Retrying IPC in {} second(s)...", | ||
| 202 | backoff_amount + 1)); | ||
| 203 | } | ||
| 204 | } | ||
| 205 | } | ||
| 206 | |||
| 207 | // We do this after giving up the mutex because otherwise we could | ||
| 208 | // deadlock with the main thread. | ||
| 209 | if (show_error) { | ||
| 210 | TrackerLog("Giving up on IPC."); | ||
| 211 | |||
| 212 | wxMessageBox("Connection to Lingo timed out.", "Connection failed", | ||
| 213 | wxOK | wxICON_ERROR); | ||
| 214 | break; | ||
| 215 | } | ||
| 216 | } | ||
| 217 | |||
| 218 | // Pretty much every lock guard in the thread is the same. We check for | ||
| 219 | // should_disconnect, and if it gets set directly after the block, we do | ||
| 220 | // minimal bad work before checking for it again. | ||
| 221 | { | ||
| 222 | std::lock_guard state_guard(state_mutex); | ||
| 223 | if (should_disconnect) { | ||
| 224 | ws.reset(); | ||
| 225 | continue; | ||
| 226 | } | ||
| 227 | } | ||
| 228 | |||
| 229 | while (connected) { | ||
| 230 | ws->poll(); | ||
| 231 | |||
| 232 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | ||
| 233 | |||
| 234 | { | ||
| 235 | std::lock_guard state_guard(state_mutex); | ||
| 236 | if (should_disconnect) { | ||
| 237 | ws.reset(); | ||
| 238 | break; | ||
| 239 | } | ||
| 240 | } | ||
| 241 | } | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | bool TryConnect(std::string ipc_address) { | ||
| 246 | try { | ||
| 247 | ws = std::make_unique<wswrap::WS>( | ||
| 248 | ipc_address, [this]() { OnConnect(); }, [this]() { OnClose(); }, | ||
| 249 | [this](const std::string& s) { OnMessage(s); }, | ||
| 250 | [this](const std::string& s) { OnError(s); }); | ||
| 251 | return true; | ||
| 252 | } catch (const std::exception& ex) { | ||
| 253 | TrackerLog(fmt::format("Error connecting to Lingo: {}", ex.what())); | ||
| 254 | wxMessageBox(ex.what(), "Error connecting to Lingo", wxOK | wxICON_ERROR); | ||
| 255 | ws.reset(); | ||
| 256 | return false; | ||
| 257 | } | ||
| 258 | } | ||
| 259 | |||
| 260 | void OnConnect() { | ||
| 261 | connected = true; | ||
| 262 | |||
| 263 | { | ||
| 264 | std::lock_guard state_guard(state_mutex); | ||
| 265 | |||
| 266 | slot_matches = false; | ||
| 267 | player_position = std::nullopt; | ||
| 268 | } | ||
| 269 | } | ||
| 270 | |||
| 271 | void OnClose() { | ||
| 272 | connected = false; | ||
| 273 | |||
| 274 | { | ||
| 275 | std::lock_guard state_guard(state_mutex); | ||
| 276 | |||
| 277 | slot_matches = false; | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | void OnMessage(const std::string& s) { | ||
| 282 | TrackerLog(s); | ||
| 283 | |||
| 284 | auto msg = nlohmann::json::parse(s); | ||
| 285 | |||
| 286 | if (msg["cmd"] == "Connect") { | ||
| 287 | std::lock_guard state_guard(state_mutex); | ||
| 288 | if (should_disconnect) { | ||
| 289 | return; | ||
| 290 | } | ||
| 291 | |||
| 292 | game_ap_server = msg["slot"]["server"]; | ||
| 293 | game_ap_user = msg["slot"]["player"]; | ||
| 294 | |||
| 295 | CheckIfSlotMatches(); | ||
| 296 | |||
| 297 | if (!slot_matches) { | ||
| 298 | tracker_frame->ConnectToAp(game_ap_server, game_ap_user, | ||
| 299 | msg["slot"]["password"]); | ||
| 300 | } | ||
| 301 | } else if (msg["cmd"] == "UpdatePosition") { | ||
| 302 | std::lock_guard state_guard(state_mutex); | ||
| 303 | |||
| 304 | player_position = | ||
| 305 | std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]); | ||
| 306 | |||
| 307 | tracker_frame->UpdateIndicators(StateUpdate{.player_position = true}); | ||
| 308 | } | ||
| 309 | } | ||
| 310 | |||
| 311 | void OnError(const std::string& s) {} | ||
| 312 | |||
| 313 | // Assumes mutex is locked. | ||
| 314 | void CheckIfSlotMatches() { | ||
| 315 | slot_matches = (tracker_ap_server == game_ap_server && | ||
| 316 | tracker_ap_user == game_ap_user); | ||
| 317 | |||
| 318 | if (slot_matches) { | ||
| 319 | SetStatusMessage("Connected to game."); | ||
| 320 | |||
| 321 | Sync(); | ||
| 322 | } else if (connected) { | ||
| 323 | SetStatusMessage("Local game doesn't match AP slot."); | ||
| 324 | } | ||
| 325 | } | ||
| 326 | |||
| 327 | // Assumes mutex is locked. | ||
| 328 | void SetStatusMessage(std::optional<std::string> msg) { | ||
| 329 | status_message = msg; | ||
| 330 | |||
| 331 | tracker_frame->UpdateStatusMessage(); | ||
| 332 | } | ||
| 333 | |||
| 334 | void Sync() { | ||
| 335 | nlohmann::json msg; | ||
| 336 | msg["cmd"] = "Sync"; | ||
| 337 | |||
| 338 | ws->send_text(msg.dump()); | ||
| 339 | } | ||
| 340 | }; | ||
| 341 | |||
| 342 | IPCState& GetState() { | ||
| 343 | static IPCState* instance = new IPCState(); | ||
| 344 | return *instance; | ||
| 345 | } | ||
| 346 | |||
| 347 | } // namespace | ||
| 348 | |||
| 349 | void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) { | ||
| 350 | GetState().SetTrackerFrame(tracker_frame); | ||
| 351 | } | ||
| 352 | |||
| 353 | void IPC_Connect(std::string address) { GetState().Connect(address); } | ||
| 354 | |||
| 355 | std::optional<std::string> IPC_GetStatusMessage() { | ||
| 356 | return GetState().GetStatusMessage(); | ||
| 357 | } | ||
| 358 | |||
| 359 | void IPC_SetTrackerSlot(std::string server, std::string user) { | ||
| 360 | GetState().SetTrackerSlot(server, user); | ||
| 361 | } | ||
| 362 | |||
| 363 | bool IPC_IsConnected() { return GetState().IsConnected(); } | ||
| 364 | |||
| 365 | std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() { | ||
| 366 | return GetState().GetPlayerPosition(); | ||
| 367 | } | ||
| diff --git a/src/ipc_state.h b/src/ipc_state.h new file mode 100644 index 0000000..0e6fa51 --- /dev/null +++ b/src/ipc_state.h | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | #ifndef IPC_STATE_H_6B3B0958 | ||
| 2 | #define IPC_STATE_H_6B3B0958 | ||
| 3 | |||
| 4 | #include <optional> | ||
| 5 | #include <set> | ||
| 6 | #include <string> | ||
| 7 | #include <tuple> | ||
| 8 | |||
| 9 | class TrackerFrame; | ||
| 10 | |||
| 11 | void IPC_SetTrackerFrame(TrackerFrame* tracker_frame); | ||
| 12 | |||
| 13 | void IPC_Connect(std::string address); | ||
| 14 | |||
| 15 | std::optional<std::string> IPC_GetStatusMessage(); | ||
| 16 | |||
| 17 | void IPC_SetTrackerSlot(std::string server, std::string user); | ||
| 18 | |||
| 19 | bool IPC_IsConnected(); | ||
| 20 | |||
| 21 | std::optional<std::tuple<int, int>> IPC_GetPlayerPosition(); | ||
| 22 | |||
| 23 | #endif /* end of include guard: IPC_STATE_H_6B3B0958 */ | ||
| diff --git a/src/items_pane.cpp b/src/items_pane.cpp new file mode 100644 index 0000000..055eec0 --- /dev/null +++ b/src/items_pane.cpp | |||
| @@ -0,0 +1,145 @@ | |||
| 1 | #include "items_pane.h" | ||
| 2 | |||
| 3 | #include <map> | ||
| 4 | |||
| 5 | namespace { | ||
| 6 | |||
| 7 | enum SortInstruction { | ||
| 8 | SI_NONE = 0, | ||
| 9 | SI_ASC = 1 << 0, | ||
| 10 | SI_DESC = 1 << 1, | ||
| 11 | SI_NAME = 1 << 2, | ||
| 12 | SI_AMOUNT = 1 << 3, | ||
| 13 | SI_ORDER = 1 << 4, | ||
| 14 | }; | ||
| 15 | |||
| 16 | inline SortInstruction operator|(SortInstruction lhs, SortInstruction rhs) { | ||
| 17 | return static_cast<SortInstruction>(static_cast<int>(lhs) | | ||
| 18 | static_cast<int>(rhs)); | ||
| 19 | } | ||
| 20 | |||
| 21 | template <typename T> | ||
| 22 | int ItemCompare(const T& lhs, const T& rhs, bool ascending) { | ||
| 23 | if (lhs < rhs) { | ||
| 24 | return ascending ? -1 : 1; | ||
| 25 | } else if (lhs > rhs) { | ||
| 26 | return ascending ? 1 : -1; | ||
| 27 | } else { | ||
| 28 | return 0; | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | int wxCALLBACK RowCompare(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData) { | ||
| 33 | const ItemState& lhs = *reinterpret_cast<const ItemState*>(item1); | ||
| 34 | const ItemState& rhs = *reinterpret_cast<const ItemState*>(item2); | ||
| 35 | SortInstruction instruction = static_cast<SortInstruction>(sortData); | ||
| 36 | |||
| 37 | bool ascending = (instruction & SI_ASC) != 0; | ||
| 38 | if ((instruction & SI_NAME) != 0) { | ||
| 39 | return ItemCompare(lhs.name, rhs.name, ascending); | ||
| 40 | } else if ((instruction & SI_AMOUNT) != 0) { | ||
| 41 | return ItemCompare(lhs.amount, rhs.amount, ascending); | ||
| 42 | } else if ((instruction & SI_ORDER) != 0) { | ||
| 43 | return ItemCompare(lhs.index, rhs.index, ascending); | ||
| 44 | } else { | ||
| 45 | return 0; | ||
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | } // namespace | ||
| 50 | |||
| 51 | ItemsPane::ItemsPane(wxWindow* parent) | ||
| 52 | : wxListView(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, | ||
| 53 | wxLC_REPORT | wxLC_SINGLE_SEL | wxLC_HRULES) { | ||
| 54 | AppendColumn("Item", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER); | ||
| 55 | AppendColumn("Amount", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER); | ||
| 56 | AppendColumn("Order", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER); | ||
| 57 | |||
| 58 | Bind(wxEVT_LIST_COL_CLICK, &ItemsPane::OnColClick, this); | ||
| 59 | Bind(wxEVT_DPI_CHANGED, &ItemsPane::OnDPIChanged, this); | ||
| 60 | } | ||
| 61 | |||
| 62 | void ItemsPane::ResetIndicators() { | ||
| 63 | DeleteAllItems(); | ||
| 64 | items_.clear(); | ||
| 65 | } | ||
| 66 | |||
| 67 | void ItemsPane::UpdateIndicators(const std::vector<ItemState>& items) { | ||
| 68 | std::map<std::string, ItemState> items_by_name; | ||
| 69 | |||
| 70 | for (const ItemState& item : items) { | ||
| 71 | items_by_name[item.name] = item; | ||
| 72 | } | ||
| 73 | |||
| 74 | for (int i = 0; i < GetItemCount(); i++) { | ||
| 75 | std::string item_name = GetItemText(i).utf8_string(); | ||
| 76 | auto it = items_by_name.find(item_name); | ||
| 77 | |||
| 78 | if (it != items_by_name.end()) { | ||
| 79 | SetItem(i, 1, std::to_string(it->second.amount)); | ||
| 80 | SetItem(i, 2, std::to_string(it->second.index)); | ||
| 81 | |||
| 82 | *reinterpret_cast<ItemState*>(GetItemData(i)) = it->second; | ||
| 83 | |||
| 84 | items_by_name.erase(item_name); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | for (const auto& [name, item] : items_by_name) { | ||
| 89 | int i = InsertItem(GetItemCount(), name); | ||
| 90 | SetItem(i, 1, std::to_string(item.amount)); | ||
| 91 | SetItem(i, 2, std::to_string(item.index)); | ||
| 92 | |||
| 93 | auto item_ptr = std::make_unique<ItemState>(item); | ||
| 94 | SetItemPtrData(i, reinterpret_cast<wxUIntPtr>(item_ptr.get())); | ||
| 95 | items_.push_back(std::move(item_ptr)); | ||
| 96 | } | ||
| 97 | |||
| 98 | SetColumnWidth(0, wxLIST_AUTOSIZE); | ||
| 99 | SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER); | ||
| 100 | SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER); | ||
| 101 | |||
| 102 | if (GetSortIndicator() != -1) { | ||
| 103 | DoSort(GetSortIndicator(), IsAscendingSortIndicator()); | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | void ItemsPane::OnColClick(wxListEvent& event) { | ||
| 108 | int col = event.GetColumn(); | ||
| 109 | if (col == -1) { | ||
| 110 | return; | ||
| 111 | } | ||
| 112 | |||
| 113 | bool ascending = GetUpdatedAscendingSortIndicator(col); | ||
| 114 | |||
| 115 | DoSort(col, ascending); | ||
| 116 | } | ||
| 117 | |||
| 118 | void ItemsPane::OnDPIChanged(wxDPIChangedEvent& event) { | ||
| 119 | SetColumnWidth(0, wxLIST_AUTOSIZE); | ||
| 120 | SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER); | ||
| 121 | SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER); | ||
| 122 | |||
| 123 | event.Skip(); | ||
| 124 | } | ||
| 125 | |||
| 126 | void ItemsPane::DoSort(int col, bool ascending) { | ||
| 127 | SortInstruction instruction = SI_NONE; | ||
| 128 | if (ascending) { | ||
| 129 | instruction = instruction | SI_ASC; | ||
| 130 | } else { | ||
| 131 | instruction = instruction | SI_DESC; | ||
| 132 | } | ||
| 133 | |||
| 134 | if (col == 0) { | ||
| 135 | instruction = instruction | SI_NAME; | ||
| 136 | } else if (col == 1) { | ||
| 137 | instruction = instruction | SI_AMOUNT; | ||
| 138 | } else if (col == 2) { | ||
| 139 | instruction = instruction | SI_ORDER; | ||
| 140 | } | ||
| 141 | |||
| 142 | if (SortItems(RowCompare, instruction)) { | ||
| 143 | ShowSortIndicator(col, ascending); | ||
| 144 | } | ||
| 145 | } | ||
| diff --git a/src/items_pane.h b/src/items_pane.h new file mode 100644 index 0000000..aa09c49 --- /dev/null +++ b/src/items_pane.h | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | #ifndef ITEMS_PANE_H_EB637EE3 | ||
| 2 | #define ITEMS_PANE_H_EB637EE3 | ||
| 3 | |||
| 4 | #include <wx/wxprec.h> | ||
| 5 | |||
| 6 | #ifndef WX_PRECOMP | ||
| 7 | #include <wx/wx.h> | ||
| 8 | #endif | ||
| 9 | |||
| 10 | #include <wx/listctrl.h> | ||
| 11 | |||
| 12 | #include <memory> | ||
| 13 | #include <vector> | ||
| 14 | |||
| 15 | #include "ap_state.h" | ||
| 16 | |||
| 17 | class ItemsPane : public wxListView { | ||
| 18 | public: | ||
| 19 | explicit ItemsPane(wxWindow* parent); | ||
| 20 | |||
| 21 | void ResetIndicators(); | ||
| 22 | void UpdateIndicators(const std::vector<ItemState>& items); | ||
| 23 | |||
| 24 | private: | ||
| 25 | void OnColClick(wxListEvent& event); | ||
| 26 | void OnDPIChanged(wxDPIChangedEvent& event); | ||
| 27 | |||
| 28 | void DoSort(int col, bool ascending); | ||
| 29 | |||
| 30 | std::vector<std::unique_ptr<ItemState>> items_; | ||
| 31 | }; | ||
| 32 | |||
| 33 | #endif /* end of include guard: ITEMS_PANE_H_EB637EE3 */ | ||
| diff --git a/src/log_dialog.cpp b/src/log_dialog.cpp new file mode 100644 index 0000000..3f0a8ad --- /dev/null +++ b/src/log_dialog.cpp | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | #include "log_dialog.h" | ||
| 2 | |||
| 3 | #include "logger.h" | ||
| 4 | |||
| 5 | wxDEFINE_EVENT(LOG_MESSAGE, wxCommandEvent); | ||
| 6 | |||
| 7 | LogDialog::LogDialog(wxWindow* parent) | ||
| 8 | : wxDialog(parent, wxID_ANY, "Debug Log", wxDefaultPosition, wxDefaultSize, | ||
| 9 | wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { | ||
| 10 | SetSize(FromDIP(wxSize{512, 280})); | ||
| 11 | |||
| 12 | text_area_ = | ||
| 13 | new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, | ||
| 14 | wxTE_MULTILINE | wxTE_READONLY | wxTE_DONTWRAP); | ||
| 15 | text_area_->SetValue(TrackerReadPastLog()); | ||
| 16 | |||
| 17 | wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL); | ||
| 18 | top_sizer->Add(text_area_, | ||
| 19 | wxSizerFlags().DoubleBorder().Expand().Proportion(1)); | ||
| 20 | |||
| 21 | SetSizer(top_sizer); | ||
| 22 | |||
| 23 | Bind(LOG_MESSAGE, &LogDialog::OnLogMessage, this); | ||
| 24 | } | ||
| 25 | |||
| 26 | void LogDialog::LogMessage(const std::string& message) { | ||
| 27 | wxCommandEvent* event = new wxCommandEvent(LOG_MESSAGE); | ||
| 28 | event->SetString(message); | ||
| 29 | QueueEvent(event); | ||
| 30 | } | ||
| 31 | |||
| 32 | void LogDialog::OnLogMessage(wxCommandEvent& event) { | ||
| 33 | if (!text_area_->IsEmpty()) { | ||
| 34 | text_area_->AppendText("\n"); | ||
| 35 | } | ||
| 36 | text_area_->AppendText(event.GetString()); | ||
| 37 | } | ||
| diff --git a/src/log_dialog.h b/src/log_dialog.h new file mode 100644 index 0000000..c29251f --- /dev/null +++ b/src/log_dialog.h | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | #ifndef LOG_DIALOG_H_EEFD45B6 | ||
| 2 | #define LOG_DIALOG_H_EEFD45B6 | ||
| 3 | |||
| 4 | #include <wx/wxprec.h> | ||
| 5 | |||
| 6 | #ifndef WX_PRECOMP | ||
| 7 | #include <wx/wx.h> | ||
| 8 | #endif | ||
| 9 | |||
| 10 | wxDECLARE_EVENT(LOG_MESSAGE, wxCommandEvent); | ||
| 11 | |||
| 12 | class LogDialog : public wxDialog { | ||
| 13 | public: | ||
| 14 | explicit LogDialog(wxWindow* parent); | ||
| 15 | |||
| 16 | void LogMessage(const std::string& message); | ||
| 17 | |||
| 18 | private: | ||
| 19 | void OnLogMessage(wxCommandEvent& event); | ||
| 20 | |||
| 21 | wxTextCtrl* text_area_; | ||
| 22 | }; | ||
| 23 | |||
| 24 | #endif /* end of include guard: LOG_DIALOG_H_EEFD45B6 */ | ||
| diff --git a/src/logger.cpp b/src/logger.cpp new file mode 100644 index 0000000..8a08b58 --- /dev/null +++ b/src/logger.cpp | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | #include "logger.h" | ||
| 2 | |||
| 3 | #include <chrono> | ||
| 4 | #include <fstream> | ||
| 5 | #include <mutex> | ||
| 6 | #include <sstream> | ||
| 7 | |||
| 8 | #include "global.h" | ||
| 9 | #include "log_dialog.h" | ||
| 10 | |||
| 11 | namespace { | ||
| 12 | |||
| 13 | class Logger { | ||
| 14 | public: | ||
| 15 | Logger() : logfile_(GetAbsolutePath("debug.log")) {} | ||
| 16 | |||
| 17 | void LogLine(const std::string& text) { | ||
| 18 | std::lock_guard guard(file_mutex_); | ||
| 19 | std::ostringstream line; | ||
| 20 | line << "[" << std::chrono::system_clock::now() << "] " << text; | ||
| 21 | |||
| 22 | logfile_ << line.str() << std::endl; | ||
| 23 | logfile_.flush(); | ||
| 24 | |||
| 25 | if (log_dialog_ != nullptr) { | ||
| 26 | log_dialog_->LogMessage(line.str()); | ||
| 27 | } | ||
| 28 | } | ||
| 29 | |||
| 30 | std::string GetContents() { | ||
| 31 | std::lock_guard guard(file_mutex_); | ||
| 32 | |||
| 33 | std::ifstream file_in(GetAbsolutePath("debug.log")); | ||
| 34 | std::ostringstream buffer; | ||
| 35 | buffer << file_in.rdbuf(); | ||
| 36 | |||
| 37 | return buffer.str(); | ||
| 38 | } | ||
| 39 | |||
| 40 | void SetLogDialog(LogDialog* log_dialog) { | ||
| 41 | std::lock_guard guard(file_mutex_); | ||
| 42 | log_dialog_ = log_dialog; | ||
| 43 | } | ||
| 44 | |||
| 45 | private: | ||
| 46 | std::ofstream logfile_; | ||
| 47 | std::mutex file_mutex_; | ||
| 48 | LogDialog* log_dialog_ = nullptr; | ||
| 49 | }; | ||
| 50 | |||
| 51 | Logger& GetLogger() { | ||
| 52 | static Logger* instance = new Logger(); | ||
| 53 | return *instance; | ||
| 54 | } | ||
| 55 | |||
| 56 | } // namespace | ||
| 57 | |||
| 58 | void TrackerLog(std::string text) { GetLogger().LogLine(text); } | ||
| 59 | |||
| 60 | std::string TrackerReadPastLog() { return GetLogger().GetContents(); } | ||
| 61 | |||
| 62 | void TrackerSetLogDialog(LogDialog* log_dialog) { | ||
| 63 | GetLogger().SetLogDialog(log_dialog); | ||
| 64 | } | ||
| diff --git a/src/logger.h b/src/logger.h new file mode 100644 index 0000000..f669790 --- /dev/null +++ b/src/logger.h | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | #ifndef LOGGER_H_9BDD07EA | ||
| 2 | #define LOGGER_H_9BDD07EA | ||
| 3 | |||
| 4 | #include <string> | ||
| 5 | |||
| 6 | class LogDialog; | ||
| 7 | |||
| 8 | void TrackerLog(std::string message); | ||
| 9 | |||
| 10 | std::string TrackerReadPastLog(); | ||
| 11 | |||
| 12 | void TrackerSetLogDialog(LogDialog* log_dialog); | ||
| 13 | |||
| 14 | #endif /* end of include guard: LOGGER_H_9BDD07EA */ | ||
| diff --git a/src/main.cpp b/src/main.cpp index 5b036ea..574b6df 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -4,31 +4,29 @@ | |||
| 4 | #include <wx/wx.h> | 4 | #include <wx/wx.h> |
| 5 | #endif | 5 | #endif |
| 6 | 6 | ||
| 7 | #include <fstream> | ||
| 8 | |||
| 9 | #include "global.h" | 7 | #include "global.h" |
| 10 | #include "tracker_config.h" | 8 | #include "tracker_config.h" |
| 11 | #include "tracker_frame.h" | 9 | #include "tracker_frame.h" |
| 12 | 10 | ||
| 13 | static std::ofstream* logfile; | ||
| 14 | |||
| 15 | class TrackerApp : public wxApp { | 11 | class TrackerApp : public wxApp { |
| 16 | public: | 12 | public: |
| 17 | virtual bool OnInit() { | 13 | virtual bool OnInit() override { |
| 18 | logfile = new std::ofstream(GetAbsolutePath("debug.log")); | ||
| 19 | wxLog::SetActiveTarget(new wxLogStream(logfile)); | ||
| 20 | |||
| 21 | #ifndef NDEBUG | ||
| 22 | wxLog::SetVerbose(true); | ||
| 23 | wxLog::SetActiveTarget(new wxLogWindow(nullptr, "Debug Log")); | ||
| 24 | #endif | ||
| 25 | |||
| 26 | GetTrackerConfig().Load(); | 14 | GetTrackerConfig().Load(); |
| 27 | 15 | ||
| 28 | TrackerFrame *frame = new TrackerFrame(); | 16 | TrackerFrame *frame = new TrackerFrame(); |
| 29 | frame->Show(true); | 17 | frame->Show(true); |
| 30 | return true; | 18 | return true; |
| 31 | } | 19 | } |
| 20 | |||
| 21 | bool OnExceptionInMainLoop() override { | ||
| 22 | try { | ||
| 23 | throw; | ||
| 24 | } catch (const std::exception& ex) { | ||
| 25 | wxLogError(ex.what()); | ||
| 26 | } | ||
| 27 | |||
| 28 | return false; | ||
| 29 | } | ||
| 32 | }; | 30 | }; |
| 33 | 31 | ||
| 34 | wxIMPLEMENT_APP(TrackerApp); | 32 | wxIMPLEMENT_APP(TrackerApp); |
| diff --git a/src/network_set.cpp b/src/network_set.cpp index 6d2a098..45911e3 100644 --- a/src/network_set.cpp +++ b/src/network_set.cpp | |||
| @@ -4,9 +4,8 @@ void NetworkSet::Clear() { | |||
| 4 | network_by_item_.clear(); | 4 | network_by_item_.clear(); |
| 5 | } | 5 | } |
| 6 | 6 | ||
| 7 | void NetworkSet::AddLink(int id1, int id2) { | 7 | void NetworkSet::AddLink(int id1, int id2, bool two_way) { |
| 8 | if (id2 > id1) { | 8 | if (two_way && id2 > id1) { |
| 9 | // Make sure id1 < id2 | ||
| 10 | std::swap(id1, id2); | 9 | std::swap(id1, id2); |
| 11 | } | 10 | } |
| 12 | 11 | ||
| @@ -17,14 +16,37 @@ void NetworkSet::AddLink(int id1, int id2) { | |||
| 17 | network_by_item_[id2] = {}; | 16 | network_by_item_[id2] = {}; |
| 18 | } | 17 | } |
| 19 | 18 | ||
| 20 | network_by_item_[id1].insert({id1, id2}); | 19 | NetworkNode node = {id1, id2, two_way}; |
| 21 | network_by_item_[id2].insert({id1, id2}); | 20 | |
| 21 | network_by_item_[id1].insert(node); | ||
| 22 | network_by_item_[id2].insert(node); | ||
| 23 | } | ||
| 24 | |||
| 25 | void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2, bool two_way) { | ||
| 26 | if (two_way && id2 > id1) { | ||
| 27 | std::swap(id1, id2); | ||
| 28 | } | ||
| 29 | |||
| 30 | if (!network_by_item_.count(network_id)) { | ||
| 31 | network_by_item_[network_id] = {}; | ||
| 32 | } | ||
| 33 | |||
| 34 | NetworkNode node = {id1, id2, two_way}; | ||
| 35 | |||
| 36 | network_by_item_[network_id].insert(node); | ||
| 22 | } | 37 | } |
| 23 | 38 | ||
| 24 | bool NetworkSet::IsItemInNetwork(int id) const { | 39 | bool NetworkSet::IsItemInNetwork(int id) const { |
| 25 | return network_by_item_.count(id); | 40 | return network_by_item_.count(id); |
| 26 | } | 41 | } |
| 27 | 42 | ||
| 28 | const std::set<std::pair<int, int>>& NetworkSet::GetNetworkGraph(int id) const { | 43 | const std::set<NetworkNode>& NetworkSet::GetNetworkGraph(int id) const { |
| 29 | return network_by_item_.at(id); | 44 | return network_by_item_.at(id); |
| 30 | } | 45 | } |
| 46 | |||
| 47 | bool NetworkNode::operator<(const NetworkNode& rhs) const { | ||
| 48 | if (entry != rhs.entry) return entry < rhs.entry; | ||
| 49 | if (exit != rhs.exit) return exit < rhs.exit; | ||
| 50 | if (two_way != rhs.two_way) return two_way < rhs.two_way; | ||
| 51 | return false; | ||
| 52 | } | ||
| diff --git a/src/network_set.h b/src/network_set.h index e6f0c07..0f72052 100644 --- a/src/network_set.h +++ b/src/network_set.h | |||
| @@ -7,19 +7,29 @@ | |||
| 7 | #include <utility> | 7 | #include <utility> |
| 8 | #include <vector> | 8 | #include <vector> |
| 9 | 9 | ||
| 10 | struct NetworkNode { | ||
| 11 | int entry; | ||
| 12 | int exit; | ||
| 13 | bool two_way; | ||
| 14 | |||
| 15 | bool operator<(const NetworkNode& rhs) const; | ||
| 16 | }; | ||
| 17 | |||
| 10 | class NetworkSet { | 18 | class NetworkSet { |
| 11 | public: | 19 | public: |
| 12 | void Clear(); | 20 | void Clear(); |
| 13 | 21 | ||
| 14 | void AddLink(int id1, int id2); | 22 | void AddLink(int id1, int id2, bool two_way); |
| 23 | |||
| 24 | void AddLinkToNetwork(int network_id, int id1, int id2, bool two_way); | ||
| 15 | 25 | ||
| 16 | bool IsItemInNetwork(int id) const; | 26 | bool IsItemInNetwork(int id) const; |
| 17 | 27 | ||
| 18 | const std::set<std::pair<int, int>>& GetNetworkGraph(int id) const; | 28 | const std::set<NetworkNode>& GetNetworkGraph(int id) const; |
| 19 | 29 | ||
| 20 | private: | 30 | private: |
| 21 | 31 | ||
| 22 | std::map<int, std::set<std::pair<int, int>>> network_by_item_; | 32 | std::map<int, std::set<NetworkNode>> network_by_item_; |
| 23 | }; | 33 | }; |
| 24 | 34 | ||
| 25 | #endif /* end of include guard: NETWORK_SET_H_3036B8E3 */ | 35 | #endif /* end of include guard: NETWORK_SET_H_3036B8E3 */ |
| diff --git a/src/options_pane.cpp b/src/options_pane.cpp new file mode 100644 index 0000000..844e145 --- /dev/null +++ b/src/options_pane.cpp | |||
| @@ -0,0 +1,71 @@ | |||
| 1 | #include "options_pane.h" | ||
| 2 | |||
| 3 | #include "ap_state.h" | ||
| 4 | |||
| 5 | namespace { | ||
| 6 | |||
| 7 | const char* kDoorShuffleLabels[] = {"None", "Panels", "Doors"}; | ||
| 8 | const char* kLocationChecksLabels[] = {"Normal", "Reduced", "Insanity"}; | ||
| 9 | const char* kPanelShuffleLabels[] = {"None", "Rearrange"}; | ||
| 10 | const char* kVictoryConditionLabels[] = {"The End", "The Master", "Level 2", | ||
| 11 | "Pilgrimage"}; | ||
| 12 | const char* kSunwarpAccessLabels[] = {"Normal", "Disabled", "Unlock", | ||
| 13 | "Individual", "Progressive"}; | ||
| 14 | |||
| 15 | void AddRow(wxDataViewListCtrl* list, const std::string& text) { | ||
| 16 | wxVector<wxVariant> data; | ||
| 17 | data.push_back(wxVariant{text + ": "}); | ||
| 18 | data.push_back(wxVariant{""}); | ||
| 19 | list->AppendItem(data); | ||
| 20 | } | ||
| 21 | |||
| 22 | } // namespace | ||
| 23 | |||
| 24 | OptionsPane::OptionsPane(wxWindow* parent) | ||
| 25 | : wxDataViewListCtrl(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, | ||
| 26 | wxDV_ROW_LINES) { | ||
| 27 | AppendTextColumn("Name", wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE); | ||
| 28 | AppendTextColumn("Value"); | ||
| 29 | AddRow(this, "Shuffle Doors"); | ||
| 30 | AddRow(this, "Group Doors"); | ||
| 31 | AddRow(this, "Location Checks"); | ||
| 32 | AddRow(this, "Shuffle Colors"); | ||
| 33 | AddRow(this, "Shuffle Panels"); | ||
| 34 | AddRow(this, "Shuffle Paintings"); | ||
| 35 | AddRow(this, "Victory Condition"); | ||
| 36 | AddRow(this, "Early Color Hallways"); | ||
| 37 | AddRow(this, "Shuffle Postgame"); | ||
| 38 | AddRow(this, "Enable Pilgrimage"); | ||
| 39 | AddRow(this, "Pilgrimage Roof Access"); | ||
| 40 | AddRow(this, "Pilgrimage Paintings"); | ||
| 41 | AddRow(this, "Sunwarp Access"); | ||
| 42 | AddRow(this, "Shuffle Sunwarps"); | ||
| 43 | AddRow(this, "Mastery Achievements"); | ||
| 44 | AddRow(this, "Level 2 Requirement"); | ||
| 45 | } | ||
| 46 | |||
| 47 | void OptionsPane::OnConnect() { | ||
| 48 | SetTextValue(kDoorShuffleLabels[static_cast<size_t>(AP_GetDoorShuffleMode())], | ||
| 49 | 0, 1); | ||
| 50 | SetTextValue(AP_AreDoorsGrouped() ? "Yes" : "No", 1, 1); | ||
| 51 | SetTextValue( | ||
| 52 | kLocationChecksLabels[static_cast<size_t>(AP_GetLocationsChecks())], 2, | ||
| 53 | 1); | ||
| 54 | SetTextValue(AP_IsColorShuffle() ? "Yes" : "No", 3, 1); | ||
| 55 | SetTextValue( | ||
| 56 | kPanelShuffleLabels[static_cast<size_t>(AP_GetPanelShuffleMode())], 4, 1); | ||
| 57 | SetTextValue(AP_IsPaintingShuffle() ? "Yes" : "No", 5, 1); | ||
| 58 | SetTextValue( | ||
| 59 | kVictoryConditionLabels[static_cast<size_t>(AP_GetVictoryCondition())], 6, | ||
| 60 | 1); | ||
| 61 | SetTextValue(AP_HasEarlyColorHallways() ? "Yes" : "No", 7, 1); | ||
| 62 | SetTextValue(AP_IsPostgameShuffle() ? "Yes" : "No", 8, 1); | ||
| 63 | SetTextValue(AP_IsPilgrimageEnabled() ? "Yes" : "No", 9, 1); | ||
| 64 | SetTextValue(AP_DoesPilgrimageAllowRoofAccess() ? "Yes" : "No", 10, 1); | ||
| 65 | SetTextValue(AP_DoesPilgrimageAllowPaintings() ? "Yes" : "No", 11, 1); | ||
| 66 | SetTextValue(kSunwarpAccessLabels[static_cast<size_t>(AP_GetSunwarpAccess())], | ||
| 67 | 12, 1); | ||
| 68 | SetTextValue(AP_IsSunwarpShuffle() ? "Yes" : "No", 13, 1); | ||
| 69 | SetTextValue(std::to_string(AP_GetMasteryRequirement()), 14, 1); | ||
| 70 | SetTextValue(std::to_string(AP_GetLevel2Requirement()), 15, 1); | ||
| 71 | } | ||
| diff --git a/src/options_pane.h b/src/options_pane.h new file mode 100644 index 0000000..e9df9f0 --- /dev/null +++ b/src/options_pane.h | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | #ifndef OPTIONS_PANE_H_026A0EC0 | ||
| 2 | #define OPTIONS_PANE_H_026A0EC0 | ||
| 3 | |||
| 4 | #include <wx/wxprec.h> | ||
| 5 | |||
| 6 | #ifndef WX_PRECOMP | ||
| 7 | #include <wx/wx.h> | ||
| 8 | #endif | ||
| 9 | |||
| 10 | #include <wx/dataview.h> | ||
| 11 | |||
| 12 | class OptionsPane : public wxDataViewListCtrl { | ||
| 13 | public: | ||
| 14 | explicit OptionsPane(wxWindow* parent); | ||
| 15 | |||
| 16 | void OnConnect(); | ||
| 17 | }; | ||
| 18 | |||
| 19 | #endif /* end of include guard: OPTIONS_PANE_H_026A0EC0 */ | ||
| diff --git a/src/paintings_pane.cpp b/src/paintings_pane.cpp new file mode 100644 index 0000000..bf5d71b --- /dev/null +++ b/src/paintings_pane.cpp | |||
| @@ -0,0 +1,86 @@ | |||
| 1 | #include "paintings_pane.h" | ||
| 2 | |||
| 3 | #include <fmt/core.h> | ||
| 4 | #include <wx/dataview.h> | ||
| 5 | |||
| 6 | #include <map> | ||
| 7 | #include <set> | ||
| 8 | |||
| 9 | #include "ap_state.h" | ||
| 10 | #include "game_data.h" | ||
| 11 | #include "tracker_state.h" | ||
| 12 | |||
| 13 | namespace { | ||
| 14 | |||
| 15 | std::string GetPaintingDisplayName(const std::string& id) { | ||
| 16 | const PaintingExit& painting = GD_GetPaintingExit(GD_GetPaintingByName(id)); | ||
| 17 | const MapArea& map_area = GD_GetMapArea(painting.map_area); | ||
| 18 | |||
| 19 | return fmt::format("{} - {}", map_area.name, painting.display_name); | ||
| 20 | } | ||
| 21 | |||
| 22 | } // namespace | ||
| 23 | |||
| 24 | PaintingsPane::PaintingsPane(wxWindow* parent) : wxPanel(parent, wxID_ANY) { | ||
| 25 | wxStaticText* label = new wxStaticText( | ||
| 26 | this, wxID_ANY, "Shuffled paintings grouped by destination:"); | ||
| 27 | tree_ctrl_ = new wxDataViewTreeCtrl(this, wxID_ANY); | ||
| 28 | |||
| 29 | reveal_btn_ = new wxButton(this, wxID_ANY, "Reveal shuffled paintings"); | ||
| 30 | reveal_btn_->Disable(); | ||
| 31 | |||
| 32 | wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL); | ||
| 33 | top_sizer->Add(label, wxSizerFlags().Border()); | ||
| 34 | top_sizer->Add(tree_ctrl_, wxSizerFlags().Expand().Proportion(1)); | ||
| 35 | top_sizer->Add(reveal_btn_, wxSizerFlags().Border().Expand()); | ||
| 36 | |||
| 37 | SetSizerAndFit(top_sizer); | ||
| 38 | |||
| 39 | tree_ctrl_->Bind(wxEVT_DATAVIEW_ITEM_START_EDITING, | ||
| 40 | &PaintingsPane::OnStartEditingCell, this); | ||
| 41 | reveal_btn_->Bind(wxEVT_BUTTON, &PaintingsPane::OnClickRevealPaintings, this); | ||
| 42 | } | ||
| 43 | |||
| 44 | void PaintingsPane::ResetIndicators() { | ||
| 45 | tree_ctrl_->DeleteAllItems(); | ||
| 46 | reveal_btn_->Enable(AP_IsPaintingShuffle()); | ||
| 47 | } | ||
| 48 | |||
| 49 | void PaintingsPane::UpdateIndicators(const std::vector<std::string>&) { | ||
| 50 | // TODO: Optimize this by using the paintings delta. | ||
| 51 | |||
| 52 | tree_ctrl_->DeleteAllItems(); | ||
| 53 | |||
| 54 | std::map<std::string, std::set<std::string>> grouped_paintings; | ||
| 55 | |||
| 56 | for (const auto& [from, to] : AP_GetPaintingMapping()) { | ||
| 57 | if (IsPaintingReachable(GD_GetPaintingByName(from)) && | ||
| 58 | AP_IsPaintingChecked(from)) { | ||
| 59 | grouped_paintings[GetPaintingDisplayName(to)].insert( | ||
| 60 | GetPaintingDisplayName(from)); | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | for (const auto& [to, froms] : grouped_paintings) { | ||
| 65 | wxDataViewItem tree_branch = | ||
| 66 | tree_ctrl_->AppendContainer(wxDataViewItem(0), to); | ||
| 67 | |||
| 68 | for (const std::string& from : froms) { | ||
| 69 | tree_ctrl_->AppendItem(tree_branch, from); | ||
| 70 | } | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | void PaintingsPane::OnClickRevealPaintings(wxCommandEvent& event) { | ||
| 75 | if (wxMessageBox("Clicking yes will reveal the mapping between all shuffled " | ||
| 76 | "paintings. This is usually considered a spoiler, and is " | ||
| 77 | "likely not allowed during competitions. This action is not " | ||
| 78 | "reversible. Are you sure you want to proceed?", | ||
| 79 | "Warning", wxYES_NO | wxICON_WARNING) == wxNO) { | ||
| 80 | return; | ||
| 81 | } | ||
| 82 | |||
| 83 | AP_RevealPaintings(); | ||
| 84 | } | ||
| 85 | |||
| 86 | void PaintingsPane::OnStartEditingCell(wxDataViewEvent& event) { event.Veto(); } | ||
| diff --git a/src/paintings_pane.h b/src/paintings_pane.h new file mode 100644 index 0000000..1d14510 --- /dev/null +++ b/src/paintings_pane.h | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | #ifndef PAINTINGS_PANE_H_815370D2 | ||
| 2 | #define PAINTINGS_PANE_H_815370D2 | ||
| 3 | |||
| 4 | #include <wx/wxprec.h> | ||
| 5 | |||
| 6 | #ifndef WX_PRECOMP | ||
| 7 | #include <wx/wx.h> | ||
| 8 | #endif | ||
| 9 | |||
| 10 | class wxDataViewEvent; | ||
| 11 | class wxDataViewTreeCtrl; | ||
| 12 | |||
| 13 | class PaintingsPane : public wxPanel { | ||
| 14 | public: | ||
| 15 | explicit PaintingsPane(wxWindow* parent); | ||
| 16 | |||
| 17 | void ResetIndicators(); | ||
| 18 | void UpdateIndicators(const std::vector<std::string>& paintings); | ||
| 19 | |||
| 20 | private: | ||
| 21 | void OnClickRevealPaintings(wxCommandEvent& event); | ||
| 22 | void OnStartEditingCell(wxDataViewEvent& event); | ||
| 23 | |||
| 24 | wxDataViewTreeCtrl* tree_ctrl_; | ||
| 25 | wxButton* reveal_btn_; | ||
| 26 | }; | ||
| 27 | |||
| 28 | #endif /* end of include guard: PAINTINGS_PANE_H_815370D2 */ | ||
| diff --git a/src/report_popup.cpp b/src/report_popup.cpp new file mode 100644 index 0000000..703e87f --- /dev/null +++ b/src/report_popup.cpp | |||
| @@ -0,0 +1,131 @@ | |||
| 1 | #include "report_popup.h" | ||
| 2 | |||
| 3 | #include <wx/dcbuffer.h> | ||
| 4 | |||
| 5 | #include <map> | ||
| 6 | #include <string> | ||
| 7 | |||
| 8 | #include "global.h" | ||
| 9 | #include "icons.h" | ||
| 10 | #include "tracker_state.h" | ||
| 11 | |||
| 12 | ReportPopup::ReportPopup(wxWindow* parent) | ||
| 13 | : wxScrolledCanvas(parent, wxID_ANY) { | ||
| 14 | SetBackgroundStyle(wxBG_STYLE_PAINT); | ||
| 15 | |||
| 16 | LoadIcons(); | ||
| 17 | |||
| 18 | // TODO: This is slow on high-DPI screens. | ||
| 19 | SetScrollRate(5, 5); | ||
| 20 | |||
| 21 | SetBackgroundColour(*wxBLACK); | ||
| 22 | Hide(); | ||
| 23 | |||
| 24 | Bind(wxEVT_PAINT, &ReportPopup::OnPaint, this); | ||
| 25 | Bind(wxEVT_DPI_CHANGED, &ReportPopup::OnDPIChanged, this); | ||
| 26 | } | ||
| 27 | |||
| 28 | void ReportPopup::SetDoorId(int door_id) { | ||
| 29 | door_id_ = door_id; | ||
| 30 | |||
| 31 | ResetIndicators(); | ||
| 32 | } | ||
| 33 | |||
| 34 | void ReportPopup::Reset() { | ||
| 35 | door_id_ = -1; | ||
| 36 | } | ||
| 37 | |||
| 38 | void ReportPopup::ResetIndicators() { | ||
| 39 | if (door_id_ == -1) { | ||
| 40 | return; | ||
| 41 | } | ||
| 42 | |||
| 43 | wxFont the_font = GetFont().Scale(GetDPIScaleFactor()); | ||
| 44 | const std::map<std::string, bool>& report = GetDoorRequirements(door_id_); | ||
| 45 | |||
| 46 | wxMemoryDC mem_dc; | ||
| 47 | mem_dc.SetFont(the_font); | ||
| 48 | |||
| 49 | int acc_height = FromDIP(10); | ||
| 50 | int col_width = 0; | ||
| 51 | |||
| 52 | for (const auto& [text, obtained] : report) { | ||
| 53 | wxSize item_extent = mem_dc.GetTextExtent(text); | ||
| 54 | int item_height = | ||
| 55 | std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10); | ||
| 56 | acc_height += item_height; | ||
| 57 | |||
| 58 | if (item_extent.GetWidth() > col_width) { | ||
| 59 | col_width = item_extent.GetWidth(); | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | int item_width = col_width + FromDIP(10 + 32); | ||
| 64 | full_width_ = item_width + FromDIP(20); | ||
| 65 | full_height_ = acc_height; | ||
| 66 | |||
| 67 | Fit(); | ||
| 68 | SetVirtualSize(full_width_, full_height_); | ||
| 69 | |||
| 70 | UpdateIndicators(); | ||
| 71 | } | ||
| 72 | |||
| 73 | void ReportPopup::UpdateIndicators() { | ||
| 74 | if (door_id_ == -1) { | ||
| 75 | return; | ||
| 76 | } | ||
| 77 | |||
| 78 | wxFont the_font = GetFont().Scale(GetDPIScaleFactor()); | ||
| 79 | const std::map<std::string, bool>& report = GetDoorRequirements(door_id_); | ||
| 80 | |||
| 81 | rendered_ = wxBitmap(full_width_, full_height_); | ||
| 82 | |||
| 83 | wxMemoryDC mem_dc; | ||
| 84 | mem_dc.SelectObject(rendered_); | ||
| 85 | mem_dc.SetPen(*wxTRANSPARENT_PEN); | ||
| 86 | mem_dc.SetBrush(*wxBLACK_BRUSH); | ||
| 87 | mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_}); | ||
| 88 | |||
| 89 | mem_dc.SetFont(the_font); | ||
| 90 | |||
| 91 | int cur_height = FromDIP(10); | ||
| 92 | |||
| 93 | for (const auto& [text, obtained] : report) { | ||
| 94 | const wxBitmap* eye_ptr = obtained ? checked_eye_ : unchecked_eye_; | ||
| 95 | |||
| 96 | mem_dc.DrawBitmap(*eye_ptr, wxPoint{FromDIP(10), cur_height}); | ||
| 97 | |||
| 98 | mem_dc.SetTextForeground(obtained ? *wxWHITE : *wxRED); | ||
| 99 | wxSize item_extent = mem_dc.GetTextExtent(text); | ||
| 100 | mem_dc.DrawText( | ||
| 101 | text, wxPoint{FromDIP(10 + 32 + 10), | ||
| 102 | cur_height + | ||
| 103 | (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2}); | ||
| 104 | |||
| 105 | cur_height += FromDIP(10 + 32); | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | void ReportPopup::OnPaint(wxPaintEvent& event) { | ||
| 110 | if (door_id_ != -1) { | ||
| 111 | wxBufferedPaintDC dc(this); | ||
| 112 | PrepareDC(dc); | ||
| 113 | dc.DrawBitmap(rendered_, 0, 0); | ||
| 114 | } | ||
| 115 | |||
| 116 | event.Skip(); | ||
| 117 | } | ||
| 118 | |||
| 119 | void ReportPopup::OnDPIChanged(wxDPIChangedEvent& event) { | ||
| 120 | LoadIcons(); | ||
| 121 | ResetIndicators(); | ||
| 122 | |||
| 123 | event.Skip(); | ||
| 124 | } | ||
| 125 | |||
| 126 | void ReportPopup::LoadIcons() { | ||
| 127 | unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png", | ||
| 128 | FromDIP(wxSize{32, 32})); | ||
| 129 | checked_eye_ = | ||
| 130 | GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32})); | ||
| 131 | } | ||
| diff --git a/src/report_popup.h b/src/report_popup.h new file mode 100644 index 0000000..bbb0bef --- /dev/null +++ b/src/report_popup.h | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | #ifndef REPORT_POPUP_H_E065BED4 | ||
| 2 | #define REPORT_POPUP_H_E065BED4 | ||
| 3 | |||
| 4 | #include <wx/wxprec.h> | ||
| 5 | |||
| 6 | #ifndef WX_PRECOMP | ||
| 7 | #include <wx/wx.h> | ||
| 8 | #endif | ||
| 9 | |||
| 10 | class ReportPopup : public wxScrolledCanvas { | ||
| 11 | public: | ||
| 12 | explicit ReportPopup(wxWindow* parent); | ||
| 13 | |||
| 14 | void SetDoorId(int door_id); | ||
| 15 | |||
| 16 | void Reset(); | ||
| 17 | |||
| 18 | void ResetIndicators(); | ||
| 19 | void UpdateIndicators(); | ||
| 20 | |||
| 21 | private: | ||
| 22 | void OnPaint(wxPaintEvent& event); | ||
| 23 | void OnDPIChanged(wxDPIChangedEvent& event); | ||
| 24 | |||
| 25 | void LoadIcons(); | ||
| 26 | |||
| 27 | int door_id_ = -1; | ||
| 28 | |||
| 29 | const wxBitmap* unchecked_eye_; | ||
| 30 | const wxBitmap* checked_eye_; | ||
| 31 | |||
| 32 | int full_width_ = 0; | ||
| 33 | int full_height_ = 0; | ||
| 34 | |||
| 35 | wxBitmap rendered_; | ||
| 36 | }; | ||
| 37 | |||
| 38 | #endif /* end of include guard: REPORT_POPUP_H_E065BED4 */ | ||
| diff --git a/src/settings_dialog.cpp b/src/settings_dialog.cpp index 0321b5a..95df577 100644 --- a/src/settings_dialog.cpp +++ b/src/settings_dialog.cpp | |||
| @@ -3,30 +3,43 @@ | |||
| 3 | #include "tracker_config.h" | 3 | #include "tracker_config.h" |
| 4 | 4 | ||
| 5 | SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") { | 5 | SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") { |
| 6 | should_check_for_updates_box_ = new wxCheckBox( | 6 | wxStaticBoxSizer* main_box = |
| 7 | this, wxID_ANY, "Check for updates when the tracker opens"); | 7 | new wxStaticBoxSizer(wxVERTICAL, this, "General settings"); |
| 8 | |||
| 9 | should_check_for_updates_box_ = | ||
| 10 | new wxCheckBox(main_box->GetStaticBox(), wxID_ANY, | ||
| 11 | "Check for updates when the tracker opens"); | ||
| 8 | hybrid_areas_box_ = new wxCheckBox( | 12 | hybrid_areas_box_ = new wxCheckBox( |
| 9 | this, wxID_ANY, | 13 | main_box->GetStaticBox(), wxID_ANY, |
| 10 | "Use two colors to show that an area has partial availability"); | 14 | "Use two colors to show that an area has partial availability"); |
| 11 | show_hunt_panels_box_ = new wxCheckBox(this, wxID_ANY, "Show hunt panels"); | 15 | track_position_box_ = new wxCheckBox(main_box->GetStaticBox(), wxID_ANY, |
| 16 | "Track player position"); | ||
| 12 | 17 | ||
| 13 | should_check_for_updates_box_->SetValue( | 18 | should_check_for_updates_box_->SetValue( |
| 14 | GetTrackerConfig().should_check_for_updates); | 19 | GetTrackerConfig().should_check_for_updates); |
| 15 | hybrid_areas_box_->SetValue(GetTrackerConfig().hybrid_areas); | 20 | hybrid_areas_box_->SetValue(GetTrackerConfig().hybrid_areas); |
| 16 | show_hunt_panels_box_->SetValue(GetTrackerConfig().show_hunt_panels); | 21 | track_position_box_->SetValue(GetTrackerConfig().track_position); |
| 22 | |||
| 23 | main_box->Add(should_check_for_updates_box_, wxSizerFlags().Border()); | ||
| 24 | main_box->AddSpacer(2); | ||
| 25 | main_box->Add(hybrid_areas_box_, wxSizerFlags().Border()); | ||
| 26 | main_box->AddSpacer(2); | ||
| 27 | main_box->Add(track_position_box_, wxSizerFlags().Border()); | ||
| 28 | |||
| 29 | const wxString visible_panels_choices[] = {"Only show locations", | ||
| 30 | "Show locations and hunt panels", | ||
| 31 | "Show all panels"}; | ||
| 32 | visible_panels_box_ = | ||
| 33 | new wxRadioBox(this, wxID_ANY, "Visible panels", wxDefaultPosition, | ||
| 34 | wxDefaultSize, 3, visible_panels_choices, 1); | ||
| 35 | visible_panels_box_->SetSelection( | ||
| 36 | static_cast<int>(GetTrackerConfig().visible_panels)); | ||
| 17 | 37 | ||
| 18 | wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL); | 38 | wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL); |
| 19 | 39 | form_sizer->Add(main_box, wxSizerFlags().Border().Expand()); | |
| 20 | form_sizer->Add(should_check_for_updates_box_, wxSizerFlags().HorzBorder()); | 40 | form_sizer->Add(visible_panels_box_, wxSizerFlags().Border().Expand()); |
| 21 | form_sizer->AddSpacer(2); | 41 | form_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), |
| 22 | 42 | wxSizerFlags().Center().Border()); | |
| 23 | form_sizer->Add(hybrid_areas_box_, wxSizerFlags().HorzBorder()); | ||
| 24 | form_sizer->AddSpacer(2); | ||
| 25 | |||
| 26 | form_sizer->Add(show_hunt_panels_box_, wxSizerFlags().HorzBorder()); | ||
| 27 | form_sizer->AddSpacer(2); | ||
| 28 | |||
| 29 | form_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Center()); | ||
| 30 | 43 | ||
| 31 | SetSizerAndFit(form_sizer); | 44 | SetSizerAndFit(form_sizer); |
| 32 | 45 | ||
| diff --git a/src/settings_dialog.h b/src/settings_dialog.h index d7c1ed3..c4dacfa 100644 --- a/src/settings_dialog.h +++ b/src/settings_dialog.h | |||
| @@ -7,6 +7,10 @@ | |||
| 7 | #include <wx/wx.h> | 7 | #include <wx/wx.h> |
| 8 | #endif | 8 | #endif |
| 9 | 9 | ||
| 10 | #include <wx/radiobox.h> | ||
| 11 | |||
| 12 | #include "tracker_config.h" | ||
| 13 | |||
| 10 | class SettingsDialog : public wxDialog { | 14 | class SettingsDialog : public wxDialog { |
| 11 | public: | 15 | public: |
| 12 | SettingsDialog(); | 16 | SettingsDialog(); |
| @@ -15,12 +19,17 @@ class SettingsDialog : public wxDialog { | |||
| 15 | return should_check_for_updates_box_->GetValue(); | 19 | return should_check_for_updates_box_->GetValue(); |
| 16 | } | 20 | } |
| 17 | bool GetHybridAreas() const { return hybrid_areas_box_->GetValue(); } | 21 | bool GetHybridAreas() const { return hybrid_areas_box_->GetValue(); } |
| 18 | bool GetShowHuntPanels() const { return show_hunt_panels_box_->GetValue(); } | 22 | TrackerConfig::VisiblePanels GetVisiblePanels() const { |
| 23 | return static_cast<TrackerConfig::VisiblePanels>( | ||
| 24 | visible_panels_box_->GetSelection()); | ||
| 25 | } | ||
| 26 | bool GetTrackPosition() const { return track_position_box_->GetValue(); } | ||
| 19 | 27 | ||
| 20 | private: | 28 | private: |
| 21 | wxCheckBox* should_check_for_updates_box_; | 29 | wxCheckBox* should_check_for_updates_box_; |
| 22 | wxCheckBox* hybrid_areas_box_; | 30 | wxCheckBox* hybrid_areas_box_; |
| 23 | wxCheckBox* show_hunt_panels_box_; | 31 | wxRadioBox* visible_panels_box_; |
| 32 | wxCheckBox* track_position_box_; | ||
| 24 | }; | 33 | }; |
| 25 | 34 | ||
| 26 | #endif /* end of include guard: SETTINGS_DIALOG_H_D8635719 */ | 35 | #endif /* end of include guard: SETTINGS_DIALOG_H_D8635719 */ |
| diff --git a/src/subway_map.cpp b/src/subway_map.cpp index 6070fd5..55ac411 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp | |||
| @@ -1,22 +1,36 @@ | |||
| 1 | #include "subway_map.h" | 1 | #include "subway_map.h" |
| 2 | 2 | ||
| 3 | #include <fmt/core.h> | ||
| 3 | #include <wx/dcbuffer.h> | 4 | #include <wx/dcbuffer.h> |
| 5 | #include <wx/dcgraph.h> | ||
| 4 | 6 | ||
| 5 | #include <sstream> | 7 | #include <sstream> |
| 6 | 8 | ||
| 7 | #include "ap_state.h" | 9 | #include "ap_state.h" |
| 8 | #include "game_data.h" | 10 | #include "game_data.h" |
| 9 | #include "global.h" | 11 | #include "global.h" |
| 12 | #include "report_popup.h" | ||
| 10 | #include "tracker_state.h" | 13 | #include "tracker_state.h" |
| 11 | 14 | ||
| 12 | constexpr int AREA_ACTUAL_SIZE = 21; | 15 | constexpr int AREA_ACTUAL_SIZE = 21; |
| 13 | constexpr int OWL_ACTUAL_SIZE = 32; | 16 | constexpr int OWL_ACTUAL_SIZE = 32; |
| 17 | constexpr int PAINTING_RADIUS = 9; // the actual circles on the map are radius 11 | ||
| 18 | constexpr int PAINTING_EXIT_RADIUS = 6; | ||
| 14 | 19 | ||
| 15 | enum class ItemDrawType { | 20 | enum class ItemDrawType { kNone, kBox, kOwl, kOwlExit }; |
| 16 | kNone, | 21 | |
| 17 | kBox, | 22 | namespace { |
| 18 | kOwl | 23 | |
| 19 | }; | 24 | wxPoint GetSubwayItemMapCenter(const SubwayItem &subway_item) { |
| 25 | if (subway_item.painting) { | ||
| 26 | return {subway_item.x, subway_item.y}; | ||
| 27 | } else { | ||
| 28 | return {subway_item.x + AREA_ACTUAL_SIZE / 2, | ||
| 29 | subway_item.y + AREA_ACTUAL_SIZE / 2}; | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | } // namespace | ||
| 20 | 34 | ||
| 21 | SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { | 35 | SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { |
| 22 | SetBackgroundStyle(wxBG_STYLE_PAINT); | 36 | SetBackgroundStyle(wxBG_STYLE_PAINT); |
| @@ -42,28 +56,109 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { | |||
| 42 | 56 | ||
| 43 | Redraw(); | 57 | Redraw(); |
| 44 | 58 | ||
| 59 | scroll_timer_ = new wxTimer(this); | ||
| 60 | |||
| 45 | Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this); | 61 | Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this); |
| 46 | Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this); | 62 | Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this); |
| 63 | Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this); | ||
| 64 | Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this); | ||
| 65 | Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this); | ||
| 66 | Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this); | ||
| 67 | |||
| 68 | zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, FromDIP(wxPoint{15, 15})); | ||
| 69 | zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this); | ||
| 70 | |||
| 71 | help_button_ = new wxButton(this, wxID_ANY, "Help"); | ||
| 72 | help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this); | ||
| 73 | SetUpHelpButton(); | ||
| 74 | |||
| 75 | report_popup_ = new ReportPopup(this); | ||
| 47 | } | 76 | } |
| 48 | 77 | ||
| 49 | void SubwayMap::OnConnect() { | 78 | void SubwayMap::OnConnect() { |
| 50 | networks_.Clear(); | 79 | networks_.Clear(); |
| 51 | 80 | ||
| 52 | std::map<std::string, std::vector<int>> tagged; | 81 | std::map<std::string, std::vector<int>> tagged; |
| 82 | std::map<std::string, std::vector<int>> entrances; | ||
| 83 | std::map<std::string, std::vector<int>> exits; | ||
| 53 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { | 84 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { |
| 54 | if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { | 85 | if (AP_HasEarlyColorHallways() && |
| 86 | subway_item.special == "early_color_hallways") { | ||
| 87 | entrances["early_ch"].push_back(subway_item.id); | ||
| 88 | } | ||
| 89 | |||
| 90 | if (AP_IsPaintingShuffle() && subway_item.painting) { | ||
| 55 | continue; | 91 | continue; |
| 56 | } | 92 | } |
| 57 | 93 | ||
| 58 | for (const std::string &tag : subway_item.tags) { | 94 | for (const std::string &tag : subway_item.tags) { |
| 59 | tagged[tag].push_back(subway_item.id); | 95 | tagged[tag].push_back(subway_item.id); |
| 60 | } | 96 | } |
| 97 | for (const std::string &tag : subway_item.entrances) { | ||
| 98 | entrances[tag].push_back(subway_item.id); | ||
| 99 | } | ||
| 100 | for (const std::string &tag : subway_item.exits) { | ||
| 101 | exits[tag].push_back(subway_item.id); | ||
| 102 | } | ||
| 61 | 103 | ||
| 62 | if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { | 104 | if (!AP_IsSunwarpShuffle() && subway_item.sunwarp) { |
| 63 | std::ostringstream tag; | 105 | std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots); |
| 64 | tag << "sunwarp" << subway_item.sunwarp->dots; | 106 | switch (subway_item.sunwarp->type) { |
| 107 | case SubwaySunwarpType::kEnter: | ||
| 108 | entrances[tag].push_back(subway_item.id); | ||
| 109 | break; | ||
| 110 | case SubwaySunwarpType::kExit: | ||
| 111 | exits[tag].push_back(subway_item.id); | ||
| 112 | break; | ||
| 113 | default: | ||
| 114 | break; | ||
| 115 | } | ||
| 116 | } | ||
| 65 | 117 | ||
| 66 | tagged[tag.str()].push_back(subway_item.id); | 118 | if (!AP_IsPilgrimageEnabled()) { |
| 119 | if (subway_item.special == "sun_painting") { | ||
| 120 | entrances["sun_painting"].push_back(subway_item.id); | ||
| 121 | } else if (subway_item.special == "sun_painting_exit") { | ||
| 122 | exits["sun_painting"].push_back(subway_item.id); | ||
| 123 | } | ||
| 124 | } | ||
| 125 | } | ||
| 126 | |||
| 127 | if (AP_IsSunwarpShuffle()) { | ||
| 128 | sunwarp_mapping_ = AP_GetSunwarpMapping(); | ||
| 129 | |||
| 130 | SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal}; | ||
| 131 | int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp); | ||
| 132 | |||
| 133 | for (const auto &[index, mapping] : sunwarp_mapping_) { | ||
| 134 | std::string tag = fmt::format("sunwarp{}", mapping.dots); | ||
| 135 | |||
| 136 | SubwaySunwarp fromWarp; | ||
| 137 | if (index < 6) { | ||
| 138 | fromWarp.dots = index + 1; | ||
| 139 | fromWarp.type = SubwaySunwarpType::kEnter; | ||
| 140 | } else { | ||
| 141 | fromWarp.dots = index - 5; | ||
| 142 | fromWarp.type = SubwaySunwarpType::kExit; | ||
| 143 | } | ||
| 144 | |||
| 145 | SubwaySunwarp toWarp; | ||
| 146 | if (mapping.exit_index < 6) { | ||
| 147 | toWarp.dots = mapping.exit_index + 1; | ||
| 148 | toWarp.type = SubwaySunwarpType::kEnter; | ||
| 149 | } else { | ||
| 150 | toWarp.dots = mapping.exit_index - 5; | ||
| 151 | toWarp.type = SubwaySunwarpType::kExit; | ||
| 152 | } | ||
| 153 | |||
| 154 | entrances[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp)); | ||
| 155 | exits[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp)); | ||
| 156 | |||
| 157 | networks_.AddLinkToNetwork( | ||
| 158 | final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp), | ||
| 159 | mapping.dots == 6 ? final_sunwarp_item | ||
| 160 | : GD_GetSubwayItemForSunwarp(toWarp), | ||
| 161 | false); | ||
| 67 | } | 162 | } |
| 68 | } | 163 | } |
| 69 | 164 | ||
| @@ -73,115 +168,243 @@ void SubwayMap::OnConnect() { | |||
| 73 | tag_it1++) { | 168 | tag_it1++) { |
| 74 | for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end(); | 169 | for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end(); |
| 75 | tag_it2++) { | 170 | tag_it2++) { |
| 76 | networks_.AddLink(*tag_it1, *tag_it2); | 171 | // two links because tags are bi-directional |
| 172 | networks_.AddLink(*tag_it1, *tag_it2, true); | ||
| 173 | } | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | for (const auto &[tag, items] : entrances) { | ||
| 178 | if (!exits.contains(tag)) continue; | ||
| 179 | for (auto exit : exits[tag]) { | ||
| 180 | for (auto entrance : items) { | ||
| 181 | networks_.AddLink(entrance, exit, false); | ||
| 77 | } | 182 | } |
| 78 | } | 183 | } |
| 79 | } | 184 | } |
| 80 | 185 | ||
| 81 | checked_paintings_.clear(); | 186 | checked_paintings_.clear(); |
| 187 | |||
| 188 | UpdateIndicators(); | ||
| 82 | } | 189 | } |
| 83 | 190 | ||
| 84 | void SubwayMap::UpdateIndicators() { | 191 | void SubwayMap::UpdateIndicators() { |
| 192 | if (AP_IsSunwarpShuffle()) { | ||
| 193 | sunwarp_mapping_ = AP_GetSunwarpMapping(); | ||
| 194 | } | ||
| 195 | |||
| 85 | if (AP_IsPaintingShuffle()) { | 196 | if (AP_IsPaintingShuffle()) { |
| 86 | for (const std::string &painting_id : AP_GetCheckedPaintings()) { | 197 | std::map<std::string, std::string> painting_mapping = |
| 198 | AP_GetPaintingMapping(); | ||
| 199 | std::set<std::string> remote_checked_paintings = AP_GetCheckedPaintings(); | ||
| 200 | |||
| 201 | for (const std::string &painting_id : remote_checked_paintings) { | ||
| 87 | if (!checked_paintings_.count(painting_id)) { | 202 | if (!checked_paintings_.count(painting_id)) { |
| 88 | checked_paintings_.insert(painting_id); | 203 | checked_paintings_.insert(painting_id); |
| 89 | 204 | ||
| 90 | if (AP_GetPaintingMapping().count(painting_id)) { | 205 | if (painting_mapping.count(painting_id)) { |
| 91 | networks_.AddLink(GD_GetSubwayItemForPainting(painting_id), | 206 | std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id); |
| 92 | GD_GetSubwayItemForPainting( | 207 | std::optional<int> to_id = GD_GetSubwayItemForPainting(painting_mapping.at(painting_id)); |
| 93 | AP_GetPaintingMapping().at(painting_id))); | 208 | |
| 209 | if (from_id && to_id) { | ||
| 210 | networks_.AddLink(*from_id, *to_id, false); | ||
| 211 | } | ||
| 94 | } | 212 | } |
| 95 | } | 213 | } |
| 96 | } | 214 | } |
| 97 | } | 215 | } |
| 98 | 216 | ||
| 217 | report_popup_->UpdateIndicators(); | ||
| 218 | |||
| 99 | Redraw(); | 219 | Redraw(); |
| 100 | } | 220 | } |
| 101 | 221 | ||
| 102 | void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, | 222 | void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, |
| 103 | SubwaySunwarp to_sunwarp) { | 223 | SubwaySunwarp to_sunwarp) { |
| 104 | networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), | 224 | networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), |
| 105 | GD_GetSubwayItemForSunwarp(to_sunwarp)); | 225 | GD_GetSubwayItemForSunwarp(to_sunwarp), false); |
| 226 | } | ||
| 227 | |||
| 228 | void SubwayMap::Zoom(bool in) { | ||
| 229 | wxPoint focus_point; | ||
| 230 | |||
| 231 | if (mouse_position_) { | ||
| 232 | focus_point = *mouse_position_; | ||
| 233 | } else { | ||
| 234 | focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2}; | ||
| 235 | } | ||
| 236 | |||
| 237 | if (in) { | ||
| 238 | if (zoom_ < 3.0) { | ||
| 239 | SetZoom(zoom_ + 0.25, focus_point); | ||
| 240 | } | ||
| 241 | } else { | ||
| 242 | if (zoom_ > 1.0) { | ||
| 243 | SetZoom(zoom_ - 0.25, focus_point); | ||
| 244 | } | ||
| 245 | } | ||
| 106 | } | 246 | } |
| 107 | 247 | ||
| 108 | void SubwayMap::OnPaint(wxPaintEvent &event) { | 248 | void SubwayMap::OnPaint(wxPaintEvent &event) { |
| 109 | if (GetSize() != rendered_.GetSize()) { | 249 | if (GetSize() != rendered_.GetSize()) { |
| 110 | Redraw(); | 250 | wxSize panel_size = GetSize(); |
| 111 | } | 251 | wxSize image_size = map_image_.GetSize(); |
| 252 | |||
| 253 | render_x_ = 0; | ||
| 254 | render_y_ = 0; | ||
| 255 | render_width_ = panel_size.GetWidth(); | ||
| 256 | render_height_ = panel_size.GetHeight(); | ||
| 257 | |||
| 258 | if (image_size.GetWidth() * panel_size.GetHeight() > | ||
| 259 | panel_size.GetWidth() * image_size.GetHeight()) { | ||
| 260 | render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) / | ||
| 261 | image_size.GetWidth(); | ||
| 262 | render_y_ = (panel_size.GetHeight() - render_height_) / 2; | ||
| 263 | } else { | ||
| 264 | render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) / | ||
| 265 | image_size.GetHeight(); | ||
| 266 | render_x_ = (panel_size.GetWidth() - render_width_) / 2; | ||
| 267 | } | ||
| 112 | 268 | ||
| 113 | wxBufferedPaintDC dc(this); | 269 | SetZoomPos({zoom_x_, zoom_y_}); |
| 114 | dc.DrawBitmap(rendered_, 0, 0); | ||
| 115 | 270 | ||
| 116 | if (hovered_item_ && networks_.IsItemInNetwork(*hovered_item_)) { | 271 | SetUpHelpButton(); |
| 117 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | ||
| 118 | 272 | ||
| 119 | for (const auto &[item_id1, item_id2] : | 273 | zoom_slider_->SetSize(FromDIP(15), FromDIP(15), wxDefaultCoord, |
| 120 | networks_.GetNetworkGraph(*hovered_item_)) { | 274 | wxDefaultCoord, wxSIZE_AUTO); |
| 121 | const SubwayItem &item1 = GD_GetSubwayItem(item_id1); | 275 | } |
| 122 | const SubwayItem &item2 = GD_GetSubwayItem(item_id2); | ||
| 123 | 276 | ||
| 124 | int item1_x = (item1.x + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_x_; | 277 | wxBufferedPaintDC dc(this); |
| 125 | int item1_y = (item1.y + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_y_; | 278 | dc.SetBackground(*wxWHITE_BRUSH); |
| 279 | dc.Clear(); | ||
| 280 | |||
| 281 | { | ||
| 282 | wxMemoryDC rendered_dc; | ||
| 283 | rendered_dc.SelectObject(rendered_); | ||
| 284 | |||
| 285 | int dst_x; | ||
| 286 | int dst_y; | ||
| 287 | int dst_w; | ||
| 288 | int dst_h; | ||
| 289 | int src_x; | ||
| 290 | int src_y; | ||
| 291 | int src_w; | ||
| 292 | int src_h; | ||
| 293 | |||
| 294 | int zoomed_width = render_width_ * zoom_; | ||
| 295 | int zoomed_height = render_height_ * zoom_; | ||
| 296 | |||
| 297 | if (zoomed_width <= GetSize().GetWidth()) { | ||
| 298 | dst_x = (GetSize().GetWidth() - zoomed_width) / 2; | ||
| 299 | dst_w = zoomed_width; | ||
| 300 | src_x = 0; | ||
| 301 | src_w = map_image_.GetWidth(); | ||
| 302 | } else { | ||
| 303 | dst_x = 0; | ||
| 304 | dst_w = GetSize().GetWidth(); | ||
| 305 | src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_; | ||
| 306 | src_w = | ||
| 307 | GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_; | ||
| 308 | } | ||
| 126 | 309 | ||
| 127 | int item2_x = (item2.x + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_x_; | 310 | if (zoomed_height <= GetSize().GetHeight()) { |
| 128 | int item2_y = (item2.y + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_y_; | 311 | dst_y = (GetSize().GetHeight() - zoomed_height) / 2; |
| 312 | dst_h = zoomed_height; | ||
| 313 | src_y = 0; | ||
| 314 | src_h = map_image_.GetHeight(); | ||
| 315 | } else { | ||
| 316 | dst_y = 0; | ||
| 317 | dst_h = GetSize().GetHeight(); | ||
| 318 | src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_; | ||
| 319 | src_h = | ||
| 320 | GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_; | ||
| 321 | } | ||
| 129 | 322 | ||
| 130 | int left = std::min(item1_x, item2_x); | 323 | wxGCDC gcdc(dc); |
| 131 | int top = std::min(item1_y, item2_y); | 324 | gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD); |
| 132 | int right = std::max(item1_x, item2_x); | 325 | gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y, |
| 133 | int bottom = std::max(item1_y, item2_y); | 326 | src_w, src_h); |
| 327 | } | ||
| 134 | 328 | ||
| 135 | int halfwidth = right - left; | 329 | if (hovered_item_) { |
| 136 | int halfheight = bottom - top; | 330 | if (networks_.IsItemInNetwork(*hovered_item_)) { |
| 331 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | ||
| 332 | |||
| 333 | for (const auto node : networks_.GetNetworkGraph(*hovered_item_)) { | ||
| 334 | const SubwayItem &item1 = GD_GetSubwayItem(node.entry); | ||
| 335 | const SubwayItem &item2 = GD_GetSubwayItem(node.exit); | ||
| 336 | |||
| 337 | wxPoint item1_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item1)); | ||
| 338 | wxPoint item2_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item2)); | ||
| 339 | |||
| 340 | int left = std::min(item1_pos.x, item2_pos.x); | ||
| 341 | int top = std::min(item1_pos.y, item2_pos.y); | ||
| 342 | int right = std::max(item1_pos.x, item2_pos.x); | ||
| 343 | int bottom = std::max(item1_pos.y, item2_pos.y); | ||
| 344 | |||
| 345 | int halfwidth = right - left; | ||
| 346 | int halfheight = bottom - top; | ||
| 347 | |||
| 348 | if (halfwidth < 4 || halfheight < 4) { | ||
| 349 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); | ||
| 350 | dc.DrawLine(item1_pos, item2_pos); | ||
| 351 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); | ||
| 352 | dc.DrawLine(item1_pos, item2_pos); | ||
| 353 | if (!node.two_way) { | ||
| 354 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2)); | ||
| 355 | dc.SetBrush(*wxCYAN_BRUSH); | ||
| 356 | dc.DrawCircle(item2_pos, 4); | ||
| 357 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | ||
| 358 | } | ||
| 359 | } else { | ||
| 360 | int ellipse_x; | ||
| 361 | int ellipse_y; | ||
| 362 | double start; | ||
| 363 | double end; | ||
| 137 | 364 | ||
| 138 | if (halfwidth < 4 || halfheight < 4) { | 365 | if (item1_pos.x > item2_pos.x) { |
| 139 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); | 366 | ellipse_y = top; |
| 140 | dc.DrawLine(item1_x, item1_y, item2_x, item2_y); | ||
| 141 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); | ||
| 142 | dc.DrawLine(item1_x, item1_y, item2_x, item2_y); | ||
| 143 | } else { | ||
| 144 | int ellipse_x; | ||
| 145 | int ellipse_y; | ||
| 146 | double start; | ||
| 147 | double end; | ||
| 148 | 367 | ||
| 149 | if (item1_x > item2_x) { | 368 | if (item1_pos.y > item2_pos.y) { |
| 150 | ellipse_y = top; | 369 | ellipse_x = left - halfwidth; |
| 151 | 370 | ||
| 152 | if (item1_y > item2_y) { | 371 | start = 0; |
| 153 | ellipse_x = left - halfwidth; | 372 | end = 90; |
| 373 | } else { | ||
| 374 | ellipse_x = left; | ||
| 154 | 375 | ||
| 155 | start = 0; | 376 | start = 90; |
| 156 | end = 90; | 377 | end = 180; |
| 378 | } | ||
| 157 | } else { | 379 | } else { |
| 158 | ellipse_x = left; | 380 | ellipse_y = top - halfheight; |
| 159 | 381 | ||
| 160 | start = 90; | 382 | if (item1_pos.y > item2_pos.y) { |
| 161 | end = 180; | 383 | ellipse_x = left - halfwidth; |
| 162 | } | ||
| 163 | } else { | ||
| 164 | ellipse_y = top - halfheight; | ||
| 165 | 384 | ||
| 166 | if (item1_y > item2_y) { | 385 | start = 270; |
| 167 | ellipse_x = left - halfwidth; | 386 | end = 360; |
| 387 | } else { | ||
| 388 | ellipse_x = left; | ||
| 168 | 389 | ||
| 169 | start = 270; | 390 | start = 180; |
| 170 | end = 360; | 391 | end = 270; |
| 171 | } else { | 392 | } |
| 172 | ellipse_x = left; | 393 | } |
| 173 | 394 | ||
| 174 | start = 180; | 395 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); |
| 175 | end = 270; | 396 | dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, |
| 397 | halfheight * 2, start, end); | ||
| 398 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); | ||
| 399 | dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, | ||
| 400 | halfheight * 2, start, end); | ||
| 401 | if (!node.two_way) { | ||
| 402 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2)); | ||
| 403 | dc.SetBrush(*wxCYAN_BRUSH); | ||
| 404 | dc.DrawCircle(item2_pos, 4); | ||
| 405 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | ||
| 176 | } | 406 | } |
| 177 | } | 407 | } |
| 178 | |||
| 179 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); | ||
| 180 | dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, halfheight * 2, | ||
| 181 | start, end); | ||
| 182 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); | ||
| 183 | dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, halfheight * 2, | ||
| 184 | start, end); | ||
| 185 | } | 408 | } |
| 186 | } | 409 | } |
| 187 | } | 410 | } |
| @@ -190,137 +413,426 @@ void SubwayMap::OnPaint(wxPaintEvent &event) { | |||
| 190 | } | 413 | } |
| 191 | 414 | ||
| 192 | void SubwayMap::OnMouseMove(wxMouseEvent &event) { | 415 | void SubwayMap::OnMouseMove(wxMouseEvent &event) { |
| 193 | int mouse_x = std::clamp( | 416 | wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition()); |
| 194 | (event.GetX() - render_x_) * map_image_.GetWidth() / render_width_, | ||
| 195 | 0, map_image_.GetWidth() - 1); | ||
| 196 | int mouse_y = std::clamp( | ||
| 197 | (event.GetY() - render_y_) * map_image_.GetWidth() / render_width_, | ||
| 198 | 0, map_image_.GetHeight() - 1); | ||
| 199 | 417 | ||
| 200 | std::vector<int> hovered = tree_->query( | 418 | std::vector<int> hovered = tree_->query( |
| 201 | {static_cast<float>(mouse_x), static_cast<float>(mouse_y), 2, 2}); | 419 | {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2}); |
| 202 | std::optional<int> new_hovered_item; | ||
| 203 | if (!hovered.empty()) { | 420 | if (!hovered.empty()) { |
| 204 | new_hovered_item = hovered[0]; | 421 | actual_hover_ = hovered[0]; |
| 422 | } else { | ||
| 423 | actual_hover_ = std::nullopt; | ||
| 205 | } | 424 | } |
| 206 | 425 | ||
| 207 | if (new_hovered_item != hovered_item_) { | 426 | if (!sticky_hover_ && actual_hover_ != hovered_item_) { |
| 208 | hovered_item_ = new_hovered_item; | 427 | EvaluateHover(); |
| 428 | } | ||
| 209 | 429 | ||
| 210 | Refresh(); | 430 | if (scroll_mode_) { |
| 431 | EvaluateScroll(event.GetPosition()); | ||
| 211 | } | 432 | } |
| 212 | 433 | ||
| 434 | mouse_position_ = event.GetPosition(); | ||
| 435 | |||
| 213 | event.Skip(); | 436 | event.Skip(); |
| 214 | } | 437 | } |
| 215 | 438 | ||
| 216 | void SubwayMap::Redraw() { | 439 | void SubwayMap::OnMouseScroll(wxMouseEvent &event) { |
| 217 | wxSize panel_size = GetSize(); | 440 | double new_zoom = zoom_; |
| 218 | wxSize image_size = map_image_.GetSize(); | 441 | if (event.GetWheelRotation() > 0) { |
| 219 | 442 | new_zoom = std::min(3.0, zoom_ + 0.25); | |
| 220 | render_x_ = 0; | ||
| 221 | render_y_ = 0; | ||
| 222 | render_width_ = panel_size.GetWidth(); | ||
| 223 | render_height_ = panel_size.GetHeight(); | ||
| 224 | |||
| 225 | if (image_size.GetWidth() * panel_size.GetHeight() > | ||
| 226 | panel_size.GetWidth() * image_size.GetHeight()) { | ||
| 227 | render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) / | ||
| 228 | image_size.GetWidth(); | ||
| 229 | render_y_ = (panel_size.GetHeight() - render_height_) / 2; | ||
| 230 | } else { | 443 | } else { |
| 231 | render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) / | 444 | new_zoom = std::max(1.0, zoom_ - 0.25); |
| 232 | image_size.GetHeight(); | 445 | } |
| 233 | render_x_ = (panel_size.GetWidth() - render_width_) / 2; | 446 | |
| 447 | if (zoom_ != new_zoom) { | ||
| 448 | SetZoom(new_zoom, event.GetPosition()); | ||
| 449 | } | ||
| 450 | |||
| 451 | event.Skip(); | ||
| 452 | } | ||
| 453 | |||
| 454 | void SubwayMap::OnMouseLeave(wxMouseEvent &event) { | ||
| 455 | SetScrollSpeed(0, 0); | ||
| 456 | mouse_position_ = std::nullopt; | ||
| 457 | } | ||
| 458 | |||
| 459 | void SubwayMap::OnMouseClick(wxMouseEvent &event) { | ||
| 460 | bool finished = false; | ||
| 461 | |||
| 462 | if (actual_hover_) { | ||
| 463 | const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_); | ||
| 464 | std::optional<int> subway_door = GetRealSubwayDoor(subway_item); | ||
| 465 | |||
| 466 | if ((subway_door && !GetDoorRequirements(*subway_door).empty()) || | ||
| 467 | networks_.IsItemInNetwork(*hovered_item_)) { | ||
| 468 | if (actual_hover_ != hovered_item_) { | ||
| 469 | EvaluateHover(); | ||
| 470 | |||
| 471 | if (!hovered_item_) { | ||
| 472 | sticky_hover_ = false; | ||
| 473 | } | ||
| 474 | } else { | ||
| 475 | sticky_hover_ = !sticky_hover_; | ||
| 476 | } | ||
| 477 | |||
| 478 | finished = true; | ||
| 479 | } | ||
| 480 | } | ||
| 481 | |||
| 482 | if (!finished) { | ||
| 483 | if (scroll_mode_) { | ||
| 484 | scroll_mode_ = false; | ||
| 485 | |||
| 486 | SetScrollSpeed(0, 0); | ||
| 487 | |||
| 488 | SetCursor(wxCURSOR_ARROW); | ||
| 489 | } else if (event.GetPosition().x < GetSize().GetWidth() / 6 || | ||
| 490 | event.GetPosition().x > 5 * GetSize().GetWidth() / 6 || | ||
| 491 | event.GetPosition().y < GetSize().GetHeight() / 6 || | ||
| 492 | event.GetPosition().y > 5 * GetSize().GetHeight() / 6) { | ||
| 493 | scroll_mode_ = true; | ||
| 494 | |||
| 495 | EvaluateScroll(event.GetPosition()); | ||
| 496 | |||
| 497 | SetCursor(wxCURSOR_CROSS); | ||
| 498 | } else { | ||
| 499 | sticky_hover_ = false; | ||
| 500 | } | ||
| 501 | } | ||
| 502 | } | ||
| 503 | |||
| 504 | void SubwayMap::OnTimer(wxTimerEvent &event) { | ||
| 505 | SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_}); | ||
| 506 | Refresh(); | ||
| 507 | } | ||
| 508 | |||
| 509 | void SubwayMap::OnZoomSlide(wxCommandEvent &event) { | ||
| 510 | double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue(); | ||
| 511 | |||
| 512 | if (new_zoom != zoom_) { | ||
| 513 | SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2}); | ||
| 234 | } | 514 | } |
| 515 | } | ||
| 235 | 516 | ||
| 236 | rendered_ = wxBitmap( | 517 | void SubwayMap::OnClickHelp(wxCommandEvent &event) { |
| 237 | map_image_ | 518 | wxMessageBox( |
| 238 | .Scale(render_width_, render_height_, wxIMAGE_QUALITY_BILINEAR) | 519 | "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the " |
| 239 | .Size(panel_size, {render_x_, render_y_}, 255, 255, 255)); | 520 | "corner.\nClick on a side of the screen to start panning. It will follow " |
| 521 | "your mouse. Click again to stop.\nHover over a door to see the " | ||
| 522 | "requirements to open it.\nHover over a warp or active painting to see " | ||
| 523 | "what it is connected to.\nFor one-way connections, there will be a " | ||
| 524 | "circle at the exit.\nCircles represent paintings.\nA red circle means " | ||
| 525 | "that the painting is locked by a door.\nA blue circle means painting " | ||
| 526 | "shuffle is enabled and the painting has not been checked yet.\nA black " | ||
| 527 | "circle means the painting is not a warp.\nA green circle means that the " | ||
| 528 | "painting is a warp.\nPainting exits will be indicated with an X.\nClick " | ||
| 529 | "on a door or warp to make the popup stick until you click again.", | ||
| 530 | "Subway Map Help"); | ||
| 531 | } | ||
| 532 | |||
| 533 | void SubwayMap::Redraw() { | ||
| 534 | rendered_ = wxBitmap(map_image_); | ||
| 240 | 535 | ||
| 241 | wxMemoryDC dc; | 536 | wxMemoryDC dc; |
| 242 | dc.SelectObject(rendered_); | 537 | dc.SelectObject(rendered_); |
| 243 | 538 | ||
| 539 | wxGCDC gcdc(dc); | ||
| 540 | |||
| 541 | std::map<std::string, std::string> painting_mapping = AP_GetPaintingMapping(); | ||
| 542 | |||
| 244 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { | 543 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { |
| 245 | ItemDrawType draw_type = ItemDrawType::kNone; | 544 | ItemDrawType draw_type = ItemDrawType::kNone; |
| 246 | const wxBrush *brush_color = wxGREY_BRUSH; | 545 | const wxBrush *brush_color = wxGREY_BRUSH; |
| 247 | std::optional<wxColour> shade_color; | 546 | std::optional<int> subway_door = GetRealSubwayDoor(subway_item); |
| 547 | |||
| 548 | if (AP_HasEarlyColorHallways() && | ||
| 549 | subway_item.special == "early_color_hallways") { | ||
| 550 | draw_type = ItemDrawType::kOwl; | ||
| 551 | brush_color = wxGREEN_BRUSH; | ||
| 552 | } else if (subway_item.special == "starting_room_overhead") { | ||
| 553 | // Do not draw. | ||
| 554 | } else if (AP_IsColorShuffle() && subway_item.special && | ||
| 555 | subway_item.special->starts_with("color_")) { | ||
| 556 | std::string color_name = subway_item.special->substr(6); | ||
| 557 | LingoColor lingo_color = GetLingoColorForString(color_name); | ||
| 558 | int color_item_id = GD_GetItemIdForColor(lingo_color); | ||
| 248 | 559 | ||
| 249 | if (subway_item.door) { | ||
| 250 | draw_type = ItemDrawType::kBox; | 560 | draw_type = ItemDrawType::kBox; |
| 251 | 561 | if (AP_HasItemSafe(color_item_id)) { | |
| 252 | if (IsDoorOpen(*subway_item.door)) { | 562 | brush_color = wxGREEN_BRUSH; |
| 253 | if (!subway_item.paintings.empty()) { | 563 | } else { |
| 254 | draw_type = ItemDrawType::kOwl; | 564 | brush_color = wxRED_BRUSH; |
| 255 | } else { | 565 | } |
| 566 | } else if (subway_item.special == "sun_painting") { | ||
| 567 | if (!AP_IsPilgrimageEnabled()) { | ||
| 568 | draw_type = ItemDrawType::kOwl; | ||
| 569 | if (IsDoorOpen(*subway_item.door)) { | ||
| 256 | brush_color = wxGREEN_BRUSH; | 570 | brush_color = wxGREEN_BRUSH; |
| 571 | } else { | ||
| 572 | brush_color = wxRED_BRUSH; | ||
| 257 | } | 573 | } |
| 574 | } | ||
| 575 | } else if (subway_item.sunwarp && | ||
| 576 | subway_item.sunwarp->type == SubwaySunwarpType::kFinal && | ||
| 577 | AP_IsPilgrimageEnabled()) { | ||
| 578 | draw_type = ItemDrawType::kBox; | ||
| 579 | |||
| 580 | if (IsPilgrimageDoable()) { | ||
| 581 | brush_color = wxGREEN_BRUSH; | ||
| 258 | } else { | 582 | } else { |
| 259 | brush_color = wxRED_BRUSH; | 583 | brush_color = wxRED_BRUSH; |
| 260 | } | 584 | } |
| 261 | } else if (!subway_item.paintings.empty()) { | 585 | } else if (subway_item.painting) { |
| 262 | if (AP_IsPaintingShuffle()) { | 586 | if (subway_door && !IsDoorOpen(*subway_door)) { |
| 263 | bool has_checked_painting = false; | 587 | draw_type = ItemDrawType::kOwl; |
| 264 | bool has_unchecked_painting = false; | 588 | brush_color = wxRED_BRUSH; |
| 265 | bool has_mapped_painting = false; | 589 | } else if (AP_IsPaintingShuffle()) { |
| 266 | 590 | if (!checked_paintings_.count(*subway_item.painting)) { | |
| 267 | for (const std::string &painting_id : subway_item.paintings) { | 591 | draw_type = ItemDrawType::kOwl; |
| 268 | if (checked_paintings_.count(painting_id)) { | 592 | brush_color = wxBLUE_BRUSH; |
| 269 | has_checked_painting = true; | 593 | } else if (painting_mapping.count(*subway_item.painting)) { |
| 270 | 594 | draw_type = ItemDrawType::kOwl; | |
| 271 | if (AP_GetPaintingMapping().count(painting_id)) { | 595 | brush_color = wxGREEN_BRUSH; |
| 272 | has_mapped_painting = true; | 596 | } else if (AP_IsPaintingMappedTo(*subway_item.painting)) { |
| 273 | } | 597 | draw_type = ItemDrawType::kOwlExit; |
| 274 | } else { | 598 | brush_color = wxGREEN_BRUSH; |
| 275 | has_unchecked_painting = true; | ||
| 276 | } | ||
| 277 | } | 599 | } |
| 278 | 600 | } else if (subway_item.HasWarps()) { | |
| 279 | if (has_unchecked_painting || has_mapped_painting) { | 601 | brush_color = wxGREEN_BRUSH; |
| 602 | if (!subway_item.exits.empty()) { | ||
| 603 | draw_type = ItemDrawType::kOwlExit; | ||
| 604 | } else { | ||
| 280 | draw_type = ItemDrawType::kOwl; | 605 | draw_type = ItemDrawType::kOwl; |
| 281 | |||
| 282 | if (has_unchecked_painting) { | ||
| 283 | if (has_checked_painting) { | ||
| 284 | shade_color = wxColour(255, 255, 0, 100); | ||
| 285 | } else { | ||
| 286 | shade_color = wxColour(100, 100, 100, 100); | ||
| 287 | } | ||
| 288 | } | ||
| 289 | } | 606 | } |
| 290 | } else if (!subway_item.tags.empty()) { | 607 | } |
| 291 | draw_type = ItemDrawType::kOwl; | 608 | } else if (subway_door) { |
| 609 | draw_type = ItemDrawType::kBox; | ||
| 610 | |||
| 611 | if (IsDoorOpen(*subway_door)) { | ||
| 612 | brush_color = wxGREEN_BRUSH; | ||
| 613 | } else { | ||
| 614 | brush_color = wxRED_BRUSH; | ||
| 292 | } | 615 | } |
| 293 | } | 616 | } |
| 294 | 617 | ||
| 295 | int real_area_x = | 618 | wxPoint real_area_pos = {subway_item.x, subway_item.y}; |
| 296 | render_x_ + subway_item.x * render_width_ / image_size.GetWidth(); | ||
| 297 | int real_area_y = | ||
| 298 | render_y_ + subway_item.y * render_width_ / image_size.GetWidth(); | ||
| 299 | 619 | ||
| 300 | int real_area_size = | 620 | int real_area_size = |
| 301 | render_width_ * | 621 | (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE); |
| 302 | (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE) / | ||
| 303 | image_size.GetWidth(); | ||
| 304 | if (real_area_size == 0) { | ||
| 305 | real_area_size = 1; | ||
| 306 | } | ||
| 307 | 622 | ||
| 308 | if (draw_type == ItemDrawType::kBox) { | 623 | if (draw_type == ItemDrawType::kBox) { |
| 309 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); | 624 | gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); |
| 310 | dc.SetBrush(*brush_color); | 625 | gcdc.SetBrush(*brush_color); |
| 311 | dc.DrawRectangle({real_area_x, real_area_y}, | 626 | |
| 312 | {real_area_size, real_area_size}); | 627 | if (subway_item.tilted) { |
| 313 | } else if (draw_type == ItemDrawType::kOwl) { | 628 | constexpr int AREA_TILTED_SIDE = |
| 314 | wxBitmap owl_bitmap = wxBitmap( | 629 | static_cast<int>(AREA_ACTUAL_SIZE / 1.41421356237); |
| 315 | owl_image_.Scale(real_area_size, real_area_size, | 630 | const wxPoint poly_points[] = {{AREA_TILTED_SIDE, 0}, |
| 316 | wxIMAGE_QUALITY_BILINEAR)); | 631 | {2 * AREA_TILTED_SIDE, AREA_TILTED_SIDE}, |
| 317 | dc.DrawBitmap(owl_bitmap, {real_area_x, real_area_y}); | 632 | {AREA_TILTED_SIDE, 2 * AREA_TILTED_SIDE}, |
| 633 | {0, AREA_TILTED_SIDE}}; | ||
| 634 | gcdc.DrawPolygon(4, poly_points, subway_item.x, subway_item.y); | ||
| 635 | } else { | ||
| 636 | gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); | ||
| 637 | } | ||
| 638 | } else if (draw_type == ItemDrawType::kOwl || draw_type == ItemDrawType::kOwlExit) { | ||
| 639 | gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); | ||
| 640 | gcdc.SetBrush(*brush_color); | ||
| 641 | gcdc.DrawCircle(real_area_pos, PAINTING_RADIUS); | ||
| 642 | |||
| 643 | if (draw_type == ItemDrawType::kOwlExit) { | ||
| 644 | gcdc.DrawLine(subway_item.x - PAINTING_EXIT_RADIUS, | ||
| 645 | subway_item.y - PAINTING_EXIT_RADIUS, | ||
| 646 | subway_item.x + PAINTING_EXIT_RADIUS, | ||
| 647 | subway_item.y + PAINTING_EXIT_RADIUS); | ||
| 648 | gcdc.DrawLine(subway_item.x + PAINTING_EXIT_RADIUS, | ||
| 649 | subway_item.y - PAINTING_EXIT_RADIUS, | ||
| 650 | subway_item.x - PAINTING_EXIT_RADIUS, | ||
| 651 | subway_item.y + PAINTING_EXIT_RADIUS); | ||
| 652 | } | ||
| 318 | } | 653 | } |
| 319 | } | 654 | } |
| 320 | } | 655 | } |
| 321 | 656 | ||
| 322 | quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int& id) const { | 657 | void SubwayMap::SetUpHelpButton() { |
| 658 | help_button_->SetSize(wxDefaultCoord, wxDefaultCoord, wxDefaultCoord, | ||
| 659 | wxDefaultCoord, wxSIZE_AUTO); | ||
| 660 | help_button_->SetPosition({ | ||
| 661 | GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15, | ||
| 662 | 15, | ||
| 663 | }); | ||
| 664 | } | ||
| 665 | |||
| 666 | void SubwayMap::EvaluateScroll(wxPoint pos) { | ||
| 667 | int scroll_x; | ||
| 668 | int scroll_y; | ||
| 669 | if (pos.x < GetSize().GetWidth() / 9) { | ||
| 670 | scroll_x = 20; | ||
| 671 | } else if (pos.x < GetSize().GetWidth() / 6) { | ||
| 672 | scroll_x = 5; | ||
| 673 | } else if (pos.x > 8 * GetSize().GetWidth() / 9) { | ||
| 674 | scroll_x = -20; | ||
| 675 | } else if (pos.x > 5 * GetSize().GetWidth() / 6) { | ||
| 676 | scroll_x = -5; | ||
| 677 | } else { | ||
| 678 | scroll_x = 0; | ||
| 679 | } | ||
| 680 | if (pos.y < GetSize().GetHeight() / 9) { | ||
| 681 | scroll_y = 20; | ||
| 682 | } else if (pos.y < GetSize().GetHeight() / 6) { | ||
| 683 | scroll_y = 5; | ||
| 684 | } else if (pos.y > 8 * GetSize().GetHeight() / 9) { | ||
| 685 | scroll_y = -20; | ||
| 686 | } else if (pos.y > 5 * GetSize().GetHeight() / 6) { | ||
| 687 | scroll_y = -5; | ||
| 688 | } else { | ||
| 689 | scroll_y = 0; | ||
| 690 | } | ||
| 691 | |||
| 692 | SetScrollSpeed(scroll_x, scroll_y); | ||
| 693 | } | ||
| 694 | |||
| 695 | void SubwayMap::EvaluateHover() { | ||
| 696 | hovered_item_ = actual_hover_; | ||
| 697 | |||
| 698 | if (hovered_item_) { | ||
| 699 | // Note that these requirements are duplicated on OnMouseClick so that it | ||
| 700 | // knows when an item has a hover effect. | ||
| 701 | const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_); | ||
| 702 | std::optional<int> subway_door = GetRealSubwayDoor(subway_item); | ||
| 703 | |||
| 704 | if (subway_door && !GetDoorRequirements(*subway_door).empty()) { | ||
| 705 | report_popup_->SetDoorId(*subway_door); | ||
| 706 | |||
| 707 | wxPoint popupPos = | ||
| 708 | MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2, | ||
| 709 | subway_item.y + AREA_ACTUAL_SIZE / 2}); | ||
| 710 | |||
| 711 | report_popup_->SetClientSize( | ||
| 712 | report_popup_->GetVirtualSize().GetWidth(), | ||
| 713 | std::min(GetSize().GetHeight(), | ||
| 714 | report_popup_->GetVirtualSize().GetHeight())); | ||
| 715 | |||
| 716 | if (popupPos.x + report_popup_->GetSize().GetWidth() > | ||
| 717 | GetSize().GetWidth()) { | ||
| 718 | popupPos.x = GetSize().GetWidth() - report_popup_->GetSize().GetWidth(); | ||
| 719 | } | ||
| 720 | if (popupPos.y + report_popup_->GetSize().GetHeight() > | ||
| 721 | GetSize().GetHeight()) { | ||
| 722 | popupPos.y = | ||
| 723 | GetSize().GetHeight() - report_popup_->GetSize().GetHeight(); | ||
| 724 | } | ||
| 725 | report_popup_->SetPosition(popupPos); | ||
| 726 | |||
| 727 | report_popup_->Show(); | ||
| 728 | } else { | ||
| 729 | report_popup_->Reset(); | ||
| 730 | report_popup_->Hide(); | ||
| 731 | } | ||
| 732 | } else { | ||
| 733 | report_popup_->Reset(); | ||
| 734 | report_popup_->Hide(); | ||
| 735 | } | ||
| 736 | |||
| 737 | Refresh(); | ||
| 738 | } | ||
| 739 | |||
| 740 | wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const { | ||
| 741 | return {static_cast<int>(pos.x * render_width_ * zoom_ / | ||
| 742 | map_image_.GetSize().GetWidth() + | ||
| 743 | zoom_x_), | ||
| 744 | static_cast<int>(pos.y * render_width_ * zoom_ / | ||
| 745 | map_image_.GetSize().GetWidth() + | ||
| 746 | zoom_y_)}; | ||
| 747 | } | ||
| 748 | |||
| 749 | wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const { | ||
| 750 | return {static_cast<int>(pos.x * render_width_ * zoom_ / | ||
| 751 | map_image_.GetSize().GetWidth()), | ||
| 752 | static_cast<int>(pos.y * render_width_ * zoom_ / | ||
| 753 | map_image_.GetSize().GetWidth())}; | ||
| 754 | } | ||
| 755 | |||
| 756 | wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const { | ||
| 757 | return { | ||
| 758 | std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() / | ||
| 759 | render_width_ / zoom_), | ||
| 760 | 0, map_image_.GetWidth() - 1), | ||
| 761 | std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() / | ||
| 762 | render_width_ / zoom_), | ||
| 763 | 0, map_image_.GetHeight() - 1)}; | ||
| 764 | } | ||
| 765 | |||
| 766 | void SubwayMap::SetZoomPos(wxPoint pos) { | ||
| 767 | if (render_width_ * zoom_ <= GetSize().GetWidth()) { | ||
| 768 | zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2; | ||
| 769 | } else { | ||
| 770 | zoom_x_ = std::clamp( | ||
| 771 | pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_), | ||
| 772 | 0); | ||
| 773 | } | ||
| 774 | if (render_height_ * zoom_ <= GetSize().GetHeight()) { | ||
| 775 | zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2; | ||
| 776 | } else { | ||
| 777 | zoom_y_ = std::clamp( | ||
| 778 | pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_), | ||
| 779 | 0); | ||
| 780 | } | ||
| 781 | } | ||
| 782 | |||
| 783 | void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) { | ||
| 784 | bool should_timer = (scroll_x != 0 || scroll_y != 0); | ||
| 785 | if (should_timer != scroll_timer_->IsRunning()) { | ||
| 786 | if (should_timer) { | ||
| 787 | scroll_timer_->Start(1000 / 60); | ||
| 788 | } else { | ||
| 789 | scroll_timer_->Stop(); | ||
| 790 | } | ||
| 791 | } | ||
| 792 | |||
| 793 | scroll_x_ = scroll_x; | ||
| 794 | scroll_y_ = scroll_y; | ||
| 795 | } | ||
| 796 | |||
| 797 | void SubwayMap::SetZoom(double zoom, wxPoint static_point) { | ||
| 798 | wxPoint map_pos = RenderPosToMapPos(static_point); | ||
| 799 | zoom_ = zoom; | ||
| 800 | |||
| 801 | wxPoint virtual_pos = MapPosToVirtualPos(map_pos); | ||
| 802 | SetZoomPos(-(virtual_pos - static_point)); | ||
| 803 | |||
| 804 | Refresh(); | ||
| 805 | |||
| 806 | zoom_slider_->SetValue((zoom - 1.0) / 0.25); | ||
| 807 | } | ||
| 808 | |||
| 809 | std::optional<int> SubwayMap::GetRealSubwayDoor(const SubwayItem subway_item) { | ||
| 810 | if (AP_IsSunwarpShuffle() && subway_item.sunwarp && | ||
| 811 | subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { | ||
| 812 | int sunwarp_index = subway_item.sunwarp->dots - 1; | ||
| 813 | if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) { | ||
| 814 | sunwarp_index += 6; | ||
| 815 | } | ||
| 816 | |||
| 817 | for (const auto &[start_index, mapping] : sunwarp_mapping_) { | ||
| 818 | if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) { | ||
| 819 | return GD_GetSunwarpDoors().at(mapping.dots - 1); | ||
| 820 | } | ||
| 821 | } | ||
| 822 | } | ||
| 823 | |||
| 824 | return subway_item.door; | ||
| 825 | } | ||
| 826 | |||
| 827 | quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const { | ||
| 323 | const SubwayItem &subway_item = GD_GetSubwayItem(id); | 828 | const SubwayItem &subway_item = GD_GetSubwayItem(id); |
| 324 | return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), | 829 | if (subway_item.painting) { |
| 325 | AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE}; | 830 | return {static_cast<float>(subway_item.x) - PAINTING_RADIUS, |
| 831 | static_cast<float>(subway_item.y) - PAINTING_RADIUS, | ||
| 832 | PAINTING_RADIUS * 2, PAINTING_RADIUS * 2}; | ||
| 833 | } else { | ||
| 834 | return {static_cast<float>(subway_item.x), | ||
| 835 | static_cast<float>(subway_item.y), AREA_ACTUAL_SIZE, | ||
| 836 | AREA_ACTUAL_SIZE}; | ||
| 837 | } | ||
| 326 | } | 838 | } |
| diff --git a/src/subway_map.h b/src/subway_map.h index e5f0bf6..b04c2fd 100644 --- a/src/subway_map.h +++ b/src/subway_map.h | |||
| @@ -15,9 +15,12 @@ | |||
| 15 | 15 | ||
| 16 | #include <quadtree/Quadtree.h> | 16 | #include <quadtree/Quadtree.h> |
| 17 | 17 | ||
| 18 | #include "ap_state.h" | ||
| 18 | #include "game_data.h" | 19 | #include "game_data.h" |
| 19 | #include "network_set.h" | 20 | #include "network_set.h" |
| 20 | 21 | ||
| 22 | class ReportPopup; | ||
| 23 | |||
| 21 | class SubwayMap : public wxPanel { | 24 | class SubwayMap : public wxPanel { |
| 22 | public: | 25 | public: |
| 23 | SubwayMap(wxWindow *parent); | 26 | SubwayMap(wxWindow *parent); |
| @@ -25,12 +28,33 @@ class SubwayMap : public wxPanel { | |||
| 25 | void OnConnect(); | 28 | void OnConnect(); |
| 26 | void UpdateIndicators(); | 29 | void UpdateIndicators(); |
| 27 | void UpdateSunwarp(SubwaySunwarp from_sunwarp, SubwaySunwarp to_sunwarp); | 30 | void UpdateSunwarp(SubwaySunwarp from_sunwarp, SubwaySunwarp to_sunwarp); |
| 31 | void Zoom(bool in); | ||
| 28 | 32 | ||
| 29 | private: | 33 | private: |
| 30 | void OnPaint(wxPaintEvent &event); | 34 | void OnPaint(wxPaintEvent &event); |
| 31 | void OnMouseMove(wxMouseEvent &event); | 35 | void OnMouseMove(wxMouseEvent &event); |
| 36 | void OnMouseScroll(wxMouseEvent &event); | ||
| 37 | void OnMouseLeave(wxMouseEvent &event); | ||
| 38 | void OnMouseClick(wxMouseEvent &event); | ||
| 39 | void OnTimer(wxTimerEvent &event); | ||
| 40 | void OnZoomSlide(wxCommandEvent &event); | ||
| 41 | void OnClickHelp(wxCommandEvent &event); | ||
| 32 | 42 | ||
| 33 | void Redraw(); | 43 | void Redraw(); |
| 44 | void SetUpHelpButton(); | ||
| 45 | |||
| 46 | wxPoint MapPosToRenderPos(wxPoint pos) const; | ||
| 47 | wxPoint MapPosToVirtualPos(wxPoint pos) const; | ||
| 48 | wxPoint RenderPosToMapPos(wxPoint pos) const; | ||
| 49 | |||
| 50 | void EvaluateScroll(wxPoint pos); | ||
| 51 | void EvaluateHover(); | ||
| 52 | |||
| 53 | void SetZoomPos(wxPoint pos); | ||
| 54 | void SetScrollSpeed(int scroll_x, int scroll_y); | ||
| 55 | void SetZoom(double zoom, wxPoint static_point); | ||
| 56 | |||
| 57 | std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item); | ||
| 34 | 58 | ||
| 35 | wxImage map_image_; | 59 | wxImage map_image_; |
| 36 | wxImage owl_image_; | 60 | wxImage owl_image_; |
| @@ -38,8 +62,23 @@ class SubwayMap : public wxPanel { | |||
| 38 | wxBitmap rendered_; | 62 | wxBitmap rendered_; |
| 39 | int render_x_ = 0; | 63 | int render_x_ = 0; |
| 40 | int render_y_ = 0; | 64 | int render_y_ = 0; |
| 41 | int render_width_ = 0; | 65 | int render_width_ = 1; |
| 42 | int render_height_ = 0; | 66 | int render_height_ = 1; |
| 67 | |||
| 68 | double zoom_ = 1.0; | ||
| 69 | int zoom_x_ = 0; // in render space | ||
| 70 | int zoom_y_ = 0; | ||
| 71 | |||
| 72 | bool scroll_mode_ = false; | ||
| 73 | wxTimer* scroll_timer_; | ||
| 74 | int scroll_x_ = 0; | ||
| 75 | int scroll_y_ = 0; | ||
| 76 | |||
| 77 | wxSlider *zoom_slider_; | ||
| 78 | |||
| 79 | wxButton *help_button_; | ||
| 80 | |||
| 81 | std::optional<wxPoint> mouse_position_; | ||
| 43 | 82 | ||
| 44 | struct GetItemBox { | 83 | struct GetItemBox { |
| 45 | quadtree::Box<float> operator()(const int &id) const; | 84 | quadtree::Box<float> operator()(const int &id) const; |
| @@ -47,9 +86,16 @@ class SubwayMap : public wxPanel { | |||
| 47 | 86 | ||
| 48 | std::unique_ptr<quadtree::Quadtree<int, GetItemBox>> tree_; | 87 | std::unique_ptr<quadtree::Quadtree<int, GetItemBox>> tree_; |
| 49 | std::optional<int> hovered_item_; | 88 | std::optional<int> hovered_item_; |
| 89 | std::optional<int> actual_hover_; | ||
| 90 | bool sticky_hover_ = false; | ||
| 91 | |||
| 92 | ReportPopup *report_popup_; | ||
| 50 | 93 | ||
| 51 | NetworkSet networks_; | 94 | NetworkSet networks_; |
| 52 | std::set<std::string> checked_paintings_; | 95 | std::set<std::string> checked_paintings_; |
| 96 | |||
| 97 | // Cached from APState. | ||
| 98 | std::map<int, SunwarpMapping> sunwarp_mapping_; | ||
| 53 | }; | 99 | }; |
| 54 | 100 | ||
| 55 | #endif /* end of include guard: SUBWAY_MAP_H_BD2D843E */ | 101 | #endif /* end of include guard: SUBWAY_MAP_H_BD2D843E */ |
| diff --git a/src/tracker_config.cpp b/src/tracker_config.cpp index 85164d5..da5d60a 100644 --- a/src/tracker_config.cpp +++ b/src/tracker_config.cpp | |||
| @@ -16,7 +16,9 @@ void TrackerConfig::Load() { | |||
| 16 | asked_to_check_for_updates = file["asked_to_check_for_updates"].as<bool>(); | 16 | asked_to_check_for_updates = file["asked_to_check_for_updates"].as<bool>(); |
| 17 | should_check_for_updates = file["should_check_for_updates"].as<bool>(); | 17 | should_check_for_updates = file["should_check_for_updates"].as<bool>(); |
| 18 | hybrid_areas = file["hybrid_areas"].as<bool>(); | 18 | hybrid_areas = file["hybrid_areas"].as<bool>(); |
| 19 | show_hunt_panels = file["show_hunt_panels"].as<bool>(); | 19 | if (file["show_hunt_panels"] && file["show_hunt_panels"].as<bool>()) { |
| 20 | visible_panels = kHUNT_PANELS; | ||
| 21 | } | ||
| 20 | 22 | ||
| 21 | if (file["connection_history"]) { | 23 | if (file["connection_history"]) { |
| 22 | for (const auto& connection : file["connection_history"]) { | 24 | for (const auto& connection : file["connection_history"]) { |
| @@ -27,6 +29,11 @@ void TrackerConfig::Load() { | |||
| 27 | }); | 29 | }); |
| 28 | } | 30 | } |
| 29 | } | 31 | } |
| 32 | |||
| 33 | ipc_address = file["ipc_address"].as<std::string>(); | ||
| 34 | track_position = file["track_position"].as<bool>(); | ||
| 35 | visible_panels = | ||
| 36 | static_cast<VisiblePanels>(file["visible_panels"].as<int>()); | ||
| 30 | } catch (const std::exception&) { | 37 | } catch (const std::exception&) { |
| 31 | // It's fine if the file can't be loaded. | 38 | // It's fine if the file can't be loaded. |
| 32 | } | 39 | } |
| @@ -40,7 +47,6 @@ void TrackerConfig::Save() { | |||
| 40 | output["asked_to_check_for_updates"] = asked_to_check_for_updates; | 47 | output["asked_to_check_for_updates"] = asked_to_check_for_updates; |
| 41 | output["should_check_for_updates"] = should_check_for_updates; | 48 | output["should_check_for_updates"] = should_check_for_updates; |
| 42 | output["hybrid_areas"] = hybrid_areas; | 49 | output["hybrid_areas"] = hybrid_areas; |
| 43 | output["show_hunt_panels"] = show_hunt_panels; | ||
| 44 | 50 | ||
| 45 | output.remove("connection_history"); | 51 | output.remove("connection_history"); |
| 46 | for (const ConnectionDetails& details : connection_history) { | 52 | for (const ConnectionDetails& details : connection_history) { |
| @@ -52,6 +58,10 @@ void TrackerConfig::Save() { | |||
| 52 | output["connection_history"].push_back(connection); | 58 | output["connection_history"].push_back(connection); |
| 53 | } | 59 | } |
| 54 | 60 | ||
| 61 | output["ipc_address"] = ipc_address; | ||
| 62 | output["track_position"] = track_position; | ||
| 63 | output["visible_panels"] = static_cast<int>(visible_panels); | ||
| 64 | |||
| 55 | std::ofstream filewriter(filename_); | 65 | std::ofstream filewriter(filename_); |
| 56 | filewriter << output; | 66 | filewriter << output; |
| 57 | } | 67 | } |
| diff --git a/src/tracker_config.h b/src/tracker_config.h index a1a6c1d..df4105d 100644 --- a/src/tracker_config.h +++ b/src/tracker_config.h | |||
| @@ -23,12 +23,20 @@ class TrackerConfig { | |||
| 23 | 23 | ||
| 24 | void Save(); | 24 | void Save(); |
| 25 | 25 | ||
| 26 | enum VisiblePanels { | ||
| 27 | kLOCATIONS_ONLY, | ||
| 28 | kHUNT_PANELS, | ||
| 29 | kALL_PANELS, | ||
| 30 | }; | ||
| 31 | |||
| 26 | ConnectionDetails connection_details; | 32 | ConnectionDetails connection_details; |
| 27 | bool asked_to_check_for_updates = false; | 33 | bool asked_to_check_for_updates = false; |
| 28 | bool should_check_for_updates = false; | 34 | bool should_check_for_updates = false; |
| 29 | bool hybrid_areas = false; | 35 | bool hybrid_areas = false; |
| 30 | bool show_hunt_panels = false; | ||
| 31 | std::deque<ConnectionDetails> connection_history; | 36 | std::deque<ConnectionDetails> connection_history; |
| 37 | std::string ipc_address; | ||
| 38 | bool track_position = true; | ||
| 39 | VisiblePanels visible_panels = kLOCATIONS_ONLY; | ||
| 32 | 40 | ||
| 33 | private: | 41 | private: |
| 34 | std::string filename_; | 42 | std::string filename_; |
| diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index e944704..e8d7ef6 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp | |||
| @@ -1,30 +1,64 @@ | |||
| 1 | #include "tracker_frame.h" | 1 | #include "tracker_frame.h" |
| 2 | 2 | ||
| 3 | #include <fmt/core.h> | ||
| 3 | #include <wx/aboutdlg.h> | 4 | #include <wx/aboutdlg.h> |
| 4 | #include <wx/choicebk.h> | 5 | #include <wx/choicebk.h> |
| 6 | #include <wx/filedlg.h> | ||
| 7 | #include <wx/notebook.h> | ||
| 8 | #include <wx/splitter.h> | ||
| 9 | #include <wx/stdpaths.h> | ||
| 5 | #include <wx/webrequest.h> | 10 | #include <wx/webrequest.h> |
| 6 | 11 | ||
| 12 | #include <algorithm> | ||
| 7 | #include <nlohmann/json.hpp> | 13 | #include <nlohmann/json.hpp> |
| 8 | #include <sstream> | 14 | #include <sstream> |
| 9 | 15 | ||
| 10 | #include "achievements_pane.h" | 16 | #include "achievements_pane.h" |
| 11 | #include "ap_state.h" | 17 | #include "ap_state.h" |
| 12 | #include "connection_dialog.h" | 18 | #include "connection_dialog.h" |
| 19 | #include "ipc_dialog.h" | ||
| 20 | #include "ipc_state.h" | ||
| 21 | #include "items_pane.h" | ||
| 22 | #include "log_dialog.h" | ||
| 23 | #include "logger.h" | ||
| 24 | #include "options_pane.h" | ||
| 25 | #include "paintings_pane.h" | ||
| 13 | #include "settings_dialog.h" | 26 | #include "settings_dialog.h" |
| 14 | #include "subway_map.h" | 27 | #include "subway_map.h" |
| 15 | #include "tracker_config.h" | 28 | #include "tracker_config.h" |
| 16 | #include "tracker_panel.h" | 29 | #include "tracker_panel.h" |
| 17 | #include "version.h" | 30 | #include "version.h" |
| 18 | 31 | ||
| 32 | namespace { | ||
| 33 | |||
| 34 | std::string GetStatusMessage() { | ||
| 35 | std::string msg = AP_GetStatusMessage(); | ||
| 36 | |||
| 37 | std::optional<std::string> ipc_msg = IPC_GetStatusMessage(); | ||
| 38 | if (ipc_msg) { | ||
| 39 | msg += " "; | ||
| 40 | msg += *ipc_msg; | ||
| 41 | } | ||
| 42 | |||
| 43 | return msg; | ||
| 44 | } | ||
| 45 | |||
| 46 | } // namespace | ||
| 47 | |||
| 19 | enum TrackerFrameIds { | 48 | enum TrackerFrameIds { |
| 20 | ID_CONNECT = 1, | 49 | ID_AP_CONNECT = 1, |
| 21 | ID_CHECK_FOR_UPDATES = 2, | 50 | ID_CHECK_FOR_UPDATES = 2, |
| 22 | ID_SETTINGS = 3 | 51 | ID_SETTINGS = 3, |
| 52 | ID_ZOOM_IN = 4, | ||
| 53 | ID_ZOOM_OUT = 5, | ||
| 54 | ID_IPC_CONNECT = 7, | ||
| 55 | ID_LOG_DIALOG = 8, | ||
| 23 | }; | 56 | }; |
| 24 | 57 | ||
| 25 | wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); | 58 | wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); |
| 26 | wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); | 59 | wxDEFINE_EVENT(STATE_CHANGED, StateChangedEvent); |
| 27 | wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); | 60 | wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); |
| 61 | wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent); | ||
| 28 | 62 | ||
| 29 | TrackerFrame::TrackerFrame() | 63 | TrackerFrame::TrackerFrame() |
| 30 | : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition, | 64 | : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition, |
| @@ -32,52 +66,87 @@ TrackerFrame::TrackerFrame() | |||
| 32 | ::wxInitAllImageHandlers(); | 66 | ::wxInitAllImageHandlers(); |
| 33 | 67 | ||
| 34 | AP_SetTrackerFrame(this); | 68 | AP_SetTrackerFrame(this); |
| 69 | IPC_SetTrackerFrame(this); | ||
| 70 | |||
| 71 | SetTheIconCache(&icons_); | ||
| 72 | |||
| 73 | updater_ = std::make_unique<Updater>(this); | ||
| 74 | updater_->Cleanup(); | ||
| 35 | 75 | ||
| 36 | wxMenu *menuFile = new wxMenu(); | 76 | wxMenu *menuFile = new wxMenu(); |
| 37 | menuFile->Append(ID_CONNECT, "&Connect"); | 77 | menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago"); |
| 78 | menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo"); | ||
| 38 | menuFile->Append(ID_SETTINGS, "&Settings"); | 79 | menuFile->Append(ID_SETTINGS, "&Settings"); |
| 39 | menuFile->Append(wxID_EXIT); | 80 | menuFile->Append(wxID_EXIT); |
| 40 | 81 | ||
| 82 | wxMenu *menuView = new wxMenu(); | ||
| 83 | zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+"); | ||
| 84 | zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--"); | ||
| 85 | menuView->AppendSeparator(); | ||
| 86 | menuView->Append(ID_LOG_DIALOG, "Show Log Window\tCtrl-L"); | ||
| 87 | |||
| 88 | zoom_in_menu_item_->Enable(false); | ||
| 89 | zoom_out_menu_item_->Enable(false); | ||
| 90 | |||
| 41 | wxMenu *menuHelp = new wxMenu(); | 91 | wxMenu *menuHelp = new wxMenu(); |
| 42 | menuHelp->Append(wxID_ABOUT); | 92 | menuHelp->Append(wxID_ABOUT); |
| 43 | menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates"); | 93 | menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates"); |
| 44 | 94 | ||
| 45 | wxMenuBar *menuBar = new wxMenuBar(); | 95 | wxMenuBar *menuBar = new wxMenuBar(); |
| 46 | menuBar->Append(menuFile, "&File"); | 96 | menuBar->Append(menuFile, "&File"); |
| 97 | menuBar->Append(menuView, "&View"); | ||
| 47 | menuBar->Append(menuHelp, "&Help"); | 98 | menuBar->Append(menuHelp, "&Help"); |
| 48 | 99 | ||
| 49 | SetMenuBar(menuBar); | 100 | SetMenuBar(menuBar); |
| 50 | 101 | ||
| 51 | CreateStatusBar(); | 102 | CreateStatusBar(); |
| 52 | SetStatusText("Not connected to Archipelago."); | ||
| 53 | 103 | ||
| 54 | Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT); | 104 | Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT); |
| 55 | Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT); | 105 | Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT); |
| 56 | Bind(wxEVT_MENU, &TrackerFrame::OnConnect, this, ID_CONNECT); | 106 | Bind(wxEVT_MENU, &TrackerFrame::OnApConnect, this, ID_AP_CONNECT); |
| 107 | Bind(wxEVT_MENU, &TrackerFrame::OnIpcConnect, this, ID_IPC_CONNECT); | ||
| 57 | Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); | 108 | Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); |
| 58 | Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, | 109 | Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, |
| 59 | ID_CHECK_FOR_UPDATES); | 110 | ID_CHECK_FOR_UPDATES); |
| 111 | Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN); | ||
| 112 | Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT); | ||
| 113 | Bind(wxEVT_MENU, &TrackerFrame::OnOpenLogWindow, this, ID_LOG_DIALOG); | ||
| 114 | Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this); | ||
| 115 | Bind(wxEVT_SPLITTER_SASH_POS_CHANGED, &TrackerFrame::OnSashPositionChanged, | ||
| 116 | this); | ||
| 60 | Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); | 117 | Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); |
| 61 | Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); | 118 | Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); |
| 62 | Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); | 119 | Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); |
| 120 | Bind(CONNECT_TO_AP, &TrackerFrame::OnConnectToAp, this); | ||
| 121 | |||
| 122 | wxSize logicalSize = FromDIP(wxSize(1280, 728)); | ||
| 63 | 123 | ||
| 64 | achievements_pane_ = new AchievementsPane(this); | 124 | splitter_window_ = new wxSplitterWindow(this, wxID_ANY); |
| 125 | splitter_window_->SetMinimumPaneSize(logicalSize.x / 5); | ||
| 65 | 126 | ||
| 66 | wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY); | 127 | wxChoicebook *choicebook = new wxChoicebook(splitter_window_, wxID_ANY); |
| 128 | |||
| 129 | achievements_pane_ = new AchievementsPane(choicebook); | ||
| 67 | choicebook->AddPage(achievements_pane_, "Achievements"); | 130 | choicebook->AddPage(achievements_pane_, "Achievements"); |
| 68 | 131 | ||
| 69 | wxNotebook *rightpane = new wxNotebook(this, wxID_ANY); | 132 | items_pane_ = new ItemsPane(choicebook); |
| 70 | tracker_panel_ = new TrackerPanel(rightpane); | 133 | choicebook->AddPage(items_pane_, "Items"); |
| 71 | subway_map_ = new SubwayMap(rightpane); | 134 | |
| 72 | rightpane->AddPage(tracker_panel_, "Map"); | 135 | options_pane_ = new OptionsPane(choicebook); |
| 73 | rightpane->AddPage(subway_map_, "Subway"); | 136 | choicebook->AddPage(options_pane_, "Options"); |
| 137 | |||
| 138 | paintings_pane_ = new PaintingsPane(choicebook); | ||
| 139 | choicebook->AddPage(paintings_pane_, "Paintings"); | ||
| 74 | 140 | ||
| 75 | wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL); | 141 | notebook_ = new wxNotebook(splitter_window_, wxID_ANY); |
| 76 | top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1)); | 142 | tracker_panel_ = new TrackerPanel(notebook_); |
| 77 | top_sizer->Add(rightpane, wxSizerFlags().Expand().Proportion(3)); | 143 | subway_map_ = new SubwayMap(notebook_); |
| 144 | notebook_->AddPage(tracker_panel_, "Map"); | ||
| 145 | notebook_->AddPage(subway_map_, "Subway"); | ||
| 78 | 146 | ||
| 79 | SetSizerAndFit(top_sizer); | 147 | splitter_window_->SplitVertically(choicebook, notebook_, logicalSize.x / 4); |
| 80 | SetSize(1280, 728); | 148 | |
| 149 | SetSize(logicalSize); | ||
| 81 | 150 | ||
| 82 | if (!GetTrackerConfig().asked_to_check_for_updates) { | 151 | if (!GetTrackerConfig().asked_to_check_for_updates) { |
| 83 | GetTrackerConfig().asked_to_check_for_updates = true; | 152 | GetTrackerConfig().asked_to_check_for_updates = true; |
| @@ -94,23 +163,28 @@ TrackerFrame::TrackerFrame() | |||
| 94 | } | 163 | } |
| 95 | 164 | ||
| 96 | if (GetTrackerConfig().should_check_for_updates) { | 165 | if (GetTrackerConfig().should_check_for_updates) { |
| 97 | CheckForUpdates(/*manual=*/false); | 166 | updater_->CheckForUpdates(/*invisible=*/true); |
| 98 | } | 167 | } |
| 168 | |||
| 169 | SetStatusText(GetStatusMessage()); | ||
| 99 | } | 170 | } |
| 100 | 171 | ||
| 101 | void TrackerFrame::SetStatusMessage(std::string message) { | 172 | void TrackerFrame::ConnectToAp(std::string server, std::string user, |
| 102 | wxCommandEvent *event = new wxCommandEvent(STATUS_CHANGED); | 173 | std::string pass) { |
| 103 | event->SetString(message.c_str()); | 174 | QueueEvent(new ApConnectEvent(CONNECT_TO_AP, GetId(), std::move(server), |
| 175 | std::move(user), std::move(pass))); | ||
| 176 | } | ||
| 104 | 177 | ||
| 105 | QueueEvent(event); | 178 | void TrackerFrame::UpdateStatusMessage() { |
| 179 | QueueEvent(new wxCommandEvent(STATUS_CHANGED)); | ||
| 106 | } | 180 | } |
| 107 | 181 | ||
| 108 | void TrackerFrame::ResetIndicators() { | 182 | void TrackerFrame::ResetIndicators() { |
| 109 | QueueEvent(new wxCommandEvent(STATE_RESET)); | 183 | QueueEvent(new wxCommandEvent(STATE_RESET)); |
| 110 | } | 184 | } |
| 111 | 185 | ||
| 112 | void TrackerFrame::UpdateIndicators() { | 186 | void TrackerFrame::UpdateIndicators(StateUpdate state) { |
| 113 | QueueEvent(new wxCommandEvent(STATE_CHANGED)); | 187 | QueueEvent(new StateChangedEvent(STATE_CHANGED, GetId(), std::move(state))); |
| 114 | } | 188 | } |
| 115 | 189 | ||
| 116 | void TrackerFrame::OnAbout(wxCommandEvent &event) { | 190 | void TrackerFrame::OnAbout(wxCommandEvent &event) { |
| @@ -118,6 +192,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) { | |||
| 118 | about_info.SetName("Lingo Archipelago Tracker"); | 192 | about_info.SetName("Lingo Archipelago Tracker"); |
| 119 | about_info.SetVersion(kTrackerVersion.ToString()); | 193 | about_info.SetVersion(kTrackerVersion.ToString()); |
| 120 | about_info.AddDeveloper("hatkirby"); | 194 | about_info.AddDeveloper("hatkirby"); |
| 195 | about_info.AddDeveloper("art0007i"); | ||
| 121 | about_info.AddArtist("Brenton Wildes"); | 196 | about_info.AddArtist("Brenton Wildes"); |
| 122 | about_info.AddArtist("kinrah"); | 197 | about_info.AddArtist("kinrah"); |
| 123 | 198 | ||
| @@ -126,7 +201,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) { | |||
| 126 | 201 | ||
| 127 | void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } | 202 | void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } |
| 128 | 203 | ||
| 129 | void TrackerFrame::OnConnect(wxCommandEvent &event) { | 204 | void TrackerFrame::OnApConnect(wxCommandEvent &event) { |
| 130 | ConnectionDialog dlg; | 205 | ConnectionDialog dlg; |
| 131 | 206 | ||
| 132 | if (dlg.ShowModal() == wxID_OK) { | 207 | if (dlg.ShowModal() == wxID_OK) { |
| @@ -144,7 +219,7 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) { | |||
| 144 | } | 219 | } |
| 145 | } | 220 | } |
| 146 | 221 | ||
| 147 | while (new_history.size() > 5) { | 222 | while (new_history.size() > 10) { |
| 148 | new_history.pop_back(); | 223 | new_history.pop_back(); |
| 149 | } | 224 | } |
| 150 | 225 | ||
| @@ -156,6 +231,17 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) { | |||
| 156 | } | 231 | } |
| 157 | } | 232 | } |
| 158 | 233 | ||
| 234 | void TrackerFrame::OnIpcConnect(wxCommandEvent &event) { | ||
| 235 | IpcDialog dlg; | ||
| 236 | |||
| 237 | if (dlg.ShowModal() == wxID_OK) { | ||
| 238 | GetTrackerConfig().ipc_address = dlg.GetIpcAddress(); | ||
| 239 | GetTrackerConfig().Save(); | ||
| 240 | |||
| 241 | IPC_Connect(dlg.GetIpcAddress()); | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 159 | void TrackerFrame::OnSettings(wxCommandEvent &event) { | 245 | void TrackerFrame::OnSettings(wxCommandEvent &event) { |
| 160 | SettingsDialog dlg; | 246 | SettingsDialog dlg; |
| 161 | 247 | ||
| @@ -163,83 +249,119 @@ void TrackerFrame::OnSettings(wxCommandEvent &event) { | |||
| 163 | GetTrackerConfig().should_check_for_updates = | 249 | GetTrackerConfig().should_check_for_updates = |
| 164 | dlg.GetShouldCheckForUpdates(); | 250 | dlg.GetShouldCheckForUpdates(); |
| 165 | GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas(); | 251 | GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas(); |
| 166 | GetTrackerConfig().show_hunt_panels = dlg.GetShowHuntPanels(); | 252 | GetTrackerConfig().visible_panels = dlg.GetVisiblePanels(); |
| 253 | GetTrackerConfig().track_position = dlg.GetTrackPosition(); | ||
| 167 | GetTrackerConfig().Save(); | 254 | GetTrackerConfig().Save(); |
| 168 | 255 | ||
| 169 | UpdateIndicators(); | 256 | UpdateIndicators(StateUpdate{.cleared_locations = true, |
| 257 | .player_position = true, | ||
| 258 | .changed_settings = true}); | ||
| 170 | } | 259 | } |
| 171 | } | 260 | } |
| 172 | 261 | ||
| 173 | void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { | 262 | void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { |
| 174 | CheckForUpdates(/*manual=*/true); | 263 | updater_->CheckForUpdates(/*invisible=*/false); |
| 175 | } | 264 | } |
| 176 | 265 | ||
| 177 | void TrackerFrame::OnStateReset(wxCommandEvent& event) { | 266 | void TrackerFrame::OnZoomIn(wxCommandEvent &event) { |
| 178 | tracker_panel_->UpdateIndicators(); | 267 | if (notebook_->GetSelection() == 1) { |
| 179 | achievements_pane_->UpdateIndicators(); | 268 | subway_map_->Zoom(true); |
| 180 | subway_map_->OnConnect(); | 269 | } |
| 181 | Refresh(); | ||
| 182 | } | 270 | } |
| 183 | 271 | ||
| 184 | void TrackerFrame::OnStateChanged(wxCommandEvent &event) { | 272 | void TrackerFrame::OnZoomOut(wxCommandEvent &event) { |
| 185 | tracker_panel_->UpdateIndicators(); | 273 | if (notebook_->GetSelection() == 1) { |
| 186 | achievements_pane_->UpdateIndicators(); | 274 | subway_map_->Zoom(false); |
| 187 | subway_map_->UpdateIndicators(); | 275 | } |
| 188 | Refresh(); | ||
| 189 | } | 276 | } |
| 190 | 277 | ||
| 191 | void TrackerFrame::OnStatusChanged(wxCommandEvent &event) { | 278 | void TrackerFrame::OnOpenLogWindow(wxCommandEvent &event) { |
| 192 | SetStatusText(event.GetString()); | 279 | if (log_dialog_ == nullptr) { |
| 280 | log_dialog_ = new LogDialog(this); | ||
| 281 | log_dialog_->Show(); | ||
| 282 | TrackerSetLogDialog(log_dialog_); | ||
| 283 | |||
| 284 | log_dialog_->Bind(wxEVT_CLOSE_WINDOW, &TrackerFrame::OnCloseLogWindow, | ||
| 285 | this); | ||
| 286 | } else { | ||
| 287 | log_dialog_->SetFocus(); | ||
| 288 | } | ||
| 193 | } | 289 | } |
| 194 | 290 | ||
| 195 | void TrackerFrame::CheckForUpdates(bool manual) { | 291 | void TrackerFrame::OnCloseLogWindow(wxCloseEvent& event) { |
| 196 | wxWebRequest request = wxWebSession::GetDefault().CreateRequest( | 292 | TrackerSetLogDialog(nullptr); |
| 197 | this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION"); | 293 | log_dialog_ = nullptr; |
| 198 | 294 | ||
| 199 | if (!request.IsOk()) { | 295 | event.Skip(); |
| 200 | if (manual) { | 296 | } |
| 201 | wxMessageBox("Could not check for updates.", "Error", | 297 | |
| 202 | wxOK | wxICON_ERROR); | 298 | void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) { |
| 203 | } else { | 299 | zoom_in_menu_item_->Enable(event.GetSelection() == 1); |
| 204 | SetStatusText("Could not check for updates."); | 300 | zoom_out_menu_item_->Enable(event.GetSelection() == 1); |
| 301 | } | ||
| 302 | |||
| 303 | void TrackerFrame::OnSashPositionChanged(wxSplitterEvent& event) { | ||
| 304 | notebook_->Refresh(); | ||
| 305 | } | ||
| 306 | |||
| 307 | void TrackerFrame::OnStateReset(wxCommandEvent &event) { | ||
| 308 | tracker_panel_->UpdateIndicators(/*reset=*/true); | ||
| 309 | achievements_pane_->UpdateIndicators(); | ||
| 310 | items_pane_->ResetIndicators(); | ||
| 311 | options_pane_->OnConnect(); | ||
| 312 | paintings_pane_->ResetIndicators(); | ||
| 313 | subway_map_->OnConnect(); | ||
| 314 | Refresh(); | ||
| 315 | } | ||
| 316 | |||
| 317 | void TrackerFrame::OnStateChanged(StateChangedEvent &event) { | ||
| 318 | const StateUpdate &state = event.GetState(); | ||
| 319 | |||
| 320 | bool hunt_panels = false; | ||
| 321 | if (GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) { | ||
| 322 | hunt_panels = std::any_of( | ||
| 323 | state.panels.begin(), state.panels.end(), [](int solve_index) { | ||
| 324 | return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index)).hunt; | ||
| 325 | }); | ||
| 326 | } else if (GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) { | ||
| 327 | hunt_panels = true; | ||
| 328 | } | ||
| 329 | |||
| 330 | if (!state.items.empty() || !state.paintings.empty() || | ||
| 331 | state.cleared_locations || hunt_panels) { | ||
| 332 | // TODO: The only real reason to reset tracker_panel during an active | ||
| 333 | // connection is if the hunt panels setting changes. If we remove hunt | ||
| 334 | // panels later, we can get rid of this. | ||
| 335 | tracker_panel_->UpdateIndicators(/*reset=*/state.changed_settings); | ||
| 336 | subway_map_->UpdateIndicators(); | ||
| 337 | Refresh(); | ||
| 338 | } else if (state.player_position && GetTrackerConfig().track_position) { | ||
| 339 | if (notebook_->GetSelection() == 0) { | ||
| 340 | tracker_panel_->Refresh(); | ||
| 205 | } | 341 | } |
| 342 | } | ||
| 206 | 343 | ||
| 207 | return; | 344 | if (std::any_of(state.panels.begin(), state.panels.end(), |
| 345 | [](int solve_index) { | ||
| 346 | return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index)) | ||
| 347 | .achievement; | ||
| 348 | })) { | ||
| 349 | achievements_pane_->UpdateIndicators(); | ||
| 208 | } | 350 | } |
| 209 | 351 | ||
| 210 | Bind(wxEVT_WEBREQUEST_STATE, [this, manual](wxWebRequestEvent &evt) { | 352 | if (!state.items.empty()) { |
| 211 | if (evt.GetState() == wxWebRequest::State_Completed) { | 353 | items_pane_->UpdateIndicators(state.items); |
| 212 | std::string response = evt.GetResponse().AsString().ToStdString(); | 354 | } |
| 213 | 355 | ||
| 214 | Version latest_version(response); | 356 | if (!state.paintings.empty()) { |
| 215 | if (kTrackerVersion < latest_version) { | 357 | paintings_pane_->UpdateIndicators(state.paintings); |
| 216 | std::ostringstream message_text; | 358 | } |
| 217 | message_text << "There is a newer version of Lingo AP Tracker " | 359 | } |
| 218 | "available. You have " | 360 | |
| 219 | << kTrackerVersion.ToString() | 361 | void TrackerFrame::OnStatusChanged(wxCommandEvent &event) { |
| 220 | << ", and the latest version is " | 362 | SetStatusText(wxString::FromUTF8(GetStatusMessage())); |
| 221 | << latest_version.ToString() | 363 | } |
| 222 | << ". Would you like to update?"; | ||
| 223 | |||
| 224 | if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) == | ||
| 225 | wxYES) { | ||
| 226 | wxLaunchDefaultBrowser( | ||
| 227 | "https://code.fourisland.com/lingo-ap-tracker/about/" | ||
| 228 | "CHANGELOG.md"); | ||
| 229 | } | ||
| 230 | } else if (manual) { | ||
| 231 | wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", | ||
| 232 | wxOK); | ||
| 233 | } | ||
| 234 | } else if (evt.GetState() == wxWebRequest::State_Failed) { | ||
| 235 | if (manual) { | ||
| 236 | wxMessageBox("Could not check for updates.", "Error", | ||
| 237 | wxOK | wxICON_ERROR); | ||
| 238 | } else { | ||
| 239 | SetStatusText("Could not check for updates."); | ||
| 240 | } | ||
| 241 | } | ||
| 242 | }); | ||
| 243 | 364 | ||
| 244 | request.Start(); | 365 | void TrackerFrame::OnConnectToAp(ApConnectEvent &event) { |
| 366 | AP_Connect(event.GetServer(), event.GetUser(), event.GetPass()); | ||
| 245 | } | 367 | } |
| diff --git a/src/tracker_frame.h b/src/tracker_frame.h index f1d7171..00bbe70 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h | |||
| @@ -7,39 +7,121 @@ | |||
| 7 | #include <wx/wx.h> | 7 | #include <wx/wx.h> |
| 8 | #endif | 8 | #endif |
| 9 | 9 | ||
| 10 | #include <memory> | ||
| 11 | #include <set> | ||
| 12 | |||
| 13 | #include "ap_state.h" | ||
| 14 | #include "icons.h" | ||
| 15 | #include "updater.h" | ||
| 16 | |||
| 10 | class AchievementsPane; | 17 | class AchievementsPane; |
| 18 | class ItemsPane; | ||
| 19 | class LogDialog; | ||
| 20 | class OptionsPane; | ||
| 21 | class PaintingsPane; | ||
| 11 | class SubwayMap; | 22 | class SubwayMap; |
| 12 | class TrackerPanel; | 23 | class TrackerPanel; |
| 24 | class wxBookCtrlEvent; | ||
| 25 | class wxNotebook; | ||
| 26 | class wxSplitterEvent; | ||
| 27 | class wxSplitterWindow; | ||
| 28 | |||
| 29 | class ApConnectEvent : public wxEvent { | ||
| 30 | public: | ||
| 31 | ApConnectEvent(wxEventType eventType, int winid, std::string server, | ||
| 32 | std::string user, std::string pass) | ||
| 33 | : wxEvent(winid, eventType), | ||
| 34 | ap_server_(std::move(server)), | ||
| 35 | ap_user_(std::move(user)), | ||
| 36 | ap_pass_(std::move(pass)) {} | ||
| 37 | |||
| 38 | const std::string &GetServer() const { return ap_server_; } | ||
| 39 | |||
| 40 | const std::string &GetUser() const { return ap_user_; } | ||
| 41 | |||
| 42 | const std::string &GetPass() const { return ap_pass_; } | ||
| 43 | |||
| 44 | virtual wxEvent *Clone() const { return new ApConnectEvent(*this); } | ||
| 45 | |||
| 46 | private: | ||
| 47 | std::string ap_server_; | ||
| 48 | std::string ap_user_; | ||
| 49 | std::string ap_pass_; | ||
| 50 | }; | ||
| 51 | |||
| 52 | struct StateUpdate { | ||
| 53 | std::vector<ItemState> items; | ||
| 54 | bool progression_items = false; | ||
| 55 | std::vector<std::string> paintings; | ||
| 56 | bool cleared_locations = false; | ||
| 57 | std::set<int> panels; | ||
| 58 | bool player_position = false; | ||
| 59 | bool changed_settings = false; | ||
| 60 | }; | ||
| 61 | |||
| 62 | class StateChangedEvent : public wxEvent { | ||
| 63 | public: | ||
| 64 | StateChangedEvent(wxEventType eventType, int winid, StateUpdate state) | ||
| 65 | : wxEvent(winid, eventType), state_(std::move(state)) {} | ||
| 66 | |||
| 67 | const StateUpdate &GetState() const { return state_; } | ||
| 68 | |||
| 69 | virtual wxEvent *Clone() const { return new StateChangedEvent(*this); } | ||
| 70 | |||
| 71 | private: | ||
| 72 | StateUpdate state_; | ||
| 73 | }; | ||
| 13 | 74 | ||
| 14 | wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); | 75 | wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); |
| 15 | wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); | 76 | wxDECLARE_EVENT(STATE_CHANGED, StateChangedEvent); |
| 16 | wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); | 77 | wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); |
| 78 | wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent); | ||
| 17 | 79 | ||
| 18 | class TrackerFrame : public wxFrame { | 80 | class TrackerFrame : public wxFrame { |
| 19 | public: | 81 | public: |
| 20 | TrackerFrame(); | 82 | TrackerFrame(); |
| 21 | 83 | ||
| 22 | void SetStatusMessage(std::string message); | 84 | void ConnectToAp(std::string server, std::string user, std::string pass); |
| 85 | void UpdateStatusMessage(); | ||
| 23 | 86 | ||
| 24 | void ResetIndicators(); | 87 | void ResetIndicators(); |
| 25 | void UpdateIndicators(); | 88 | void UpdateIndicators(StateUpdate state); |
| 26 | 89 | ||
| 27 | private: | 90 | private: |
| 28 | void OnExit(wxCommandEvent &event); | 91 | void OnExit(wxCommandEvent &event); |
| 29 | void OnAbout(wxCommandEvent &event); | 92 | void OnAbout(wxCommandEvent &event); |
| 30 | void OnConnect(wxCommandEvent &event); | 93 | void OnApConnect(wxCommandEvent &event); |
| 94 | void OnIpcConnect(wxCommandEvent &event); | ||
| 31 | void OnSettings(wxCommandEvent &event); | 95 | void OnSettings(wxCommandEvent &event); |
| 32 | void OnCheckForUpdates(wxCommandEvent &event); | 96 | void OnCheckForUpdates(wxCommandEvent &event); |
| 97 | void OnZoomIn(wxCommandEvent &event); | ||
| 98 | void OnZoomOut(wxCommandEvent &event); | ||
| 99 | void OnOpenLogWindow(wxCommandEvent &event); | ||
| 100 | void OnCloseLogWindow(wxCloseEvent &event); | ||
| 101 | void OnChangePage(wxBookCtrlEvent &event); | ||
| 102 | void OnSashPositionChanged(wxSplitterEvent &event); | ||
| 33 | 103 | ||
| 34 | void OnStateReset(wxCommandEvent &event); | 104 | void OnStateReset(wxCommandEvent &event); |
| 35 | void OnStateChanged(wxCommandEvent &event); | 105 | void OnStateChanged(StateChangedEvent &event); |
| 36 | void OnStatusChanged(wxCommandEvent &event); | 106 | void OnStatusChanged(wxCommandEvent &event); |
| 107 | void OnConnectToAp(ApConnectEvent &event); | ||
| 108 | |||
| 109 | std::unique_ptr<Updater> updater_; | ||
| 37 | 110 | ||
| 38 | void CheckForUpdates(bool manual); | 111 | wxSplitterWindow *splitter_window_; |
| 39 | 112 | wxNotebook *notebook_; | |
| 40 | TrackerPanel *tracker_panel_; | 113 | TrackerPanel *tracker_panel_; |
| 41 | AchievementsPane *achievements_pane_; | 114 | AchievementsPane *achievements_pane_; |
| 115 | ItemsPane *items_pane_; | ||
| 116 | OptionsPane *options_pane_; | ||
| 117 | PaintingsPane *paintings_pane_; | ||
| 42 | SubwayMap *subway_map_; | 118 | SubwayMap *subway_map_; |
| 119 | LogDialog *log_dialog_ = nullptr; | ||
| 120 | |||
| 121 | wxMenuItem *zoom_in_menu_item_; | ||
| 122 | wxMenuItem *zoom_out_menu_item_; | ||
| 123 | |||
| 124 | IconCache icons_; | ||
| 43 | }; | 125 | }; |
| 44 | 126 | ||
| 45 | #endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */ | 127 | #endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */ |
| diff --git a/src/tracker_panel.cpp b/src/tracker_panel.cpp index 66bce81..ddb4df9 100644 --- a/src/tracker_panel.cpp +++ b/src/tracker_panel.cpp | |||
| @@ -1,11 +1,15 @@ | |||
| 1 | #include "tracker_panel.h" | 1 | #include "tracker_panel.h" |
| 2 | 2 | ||
| 3 | #include <fmt/core.h> | ||
| 3 | #include <wx/dcbuffer.h> | 4 | #include <wx/dcbuffer.h> |
| 4 | 5 | ||
| 6 | #include <algorithm> | ||
| 7 | |||
| 5 | #include "ap_state.h" | 8 | #include "ap_state.h" |
| 6 | #include "area_popup.h" | 9 | #include "area_popup.h" |
| 7 | #include "game_data.h" | 10 | #include "game_data.h" |
| 8 | #include "global.h" | 11 | #include "global.h" |
| 12 | #include "ipc_state.h" | ||
| 9 | #include "tracker_config.h" | 13 | #include "tracker_config.h" |
| 10 | #include "tracker_state.h" | 14 | #include "tracker_state.h" |
| 11 | 15 | ||
| @@ -39,15 +43,38 @@ TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) { | |||
| 39 | areas_.push_back(area); | 43 | areas_.push_back(area); |
| 40 | } | 44 | } |
| 41 | 45 | ||
| 46 | Resize(); | ||
| 42 | Redraw(); | 47 | Redraw(); |
| 43 | 48 | ||
| 44 | Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this); | 49 | Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this); |
| 45 | Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this); | 50 | Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this); |
| 46 | } | 51 | } |
| 47 | 52 | ||
| 48 | void TrackerPanel::UpdateIndicators() { | 53 | void TrackerPanel::UpdateIndicators(bool reset) { |
| 49 | for (AreaIndicator &area : areas_) { | 54 | if (reset) { |
| 50 | area.popup->UpdateIndicators(); | 55 | for (AreaIndicator &area : areas_) { |
| 56 | const MapArea &map_area = GD_GetMapArea(area.area_id); | ||
| 57 | |||
| 58 | if ((!AP_IsLocationVisible(map_area.classification) || | ||
| 59 | IsAreaPostgame(area.area_id)) && | ||
| 60 | !(map_area.hunt && | ||
| 61 | GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) && | ||
| 62 | !(map_area.has_single_panel && | ||
| 63 | GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) && | ||
| 64 | !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) { | ||
| 65 | area.active = false; | ||
| 66 | } else { | ||
| 67 | area.active = true; | ||
| 68 | } | ||
| 69 | |||
| 70 | area.popup->ResetIndicators(); | ||
| 71 | } | ||
| 72 | |||
| 73 | Resize(); | ||
| 74 | } else { | ||
| 75 | for (AreaIndicator &area : areas_) { | ||
| 76 | area.popup->UpdateIndicators(); | ||
| 77 | } | ||
| 51 | } | 78 | } |
| 52 | 79 | ||
| 53 | Redraw(); | 80 | Redraw(); |
| @@ -55,22 +82,35 @@ void TrackerPanel::UpdateIndicators() { | |||
| 55 | 82 | ||
| 56 | void TrackerPanel::OnPaint(wxPaintEvent &event) { | 83 | void TrackerPanel::OnPaint(wxPaintEvent &event) { |
| 57 | if (GetSize() != rendered_.GetSize()) { | 84 | if (GetSize() != rendered_.GetSize()) { |
| 85 | Resize(); | ||
| 58 | Redraw(); | 86 | Redraw(); |
| 59 | } | 87 | } |
| 60 | 88 | ||
| 61 | wxBufferedPaintDC dc(this); | 89 | wxBufferedPaintDC dc(this); |
| 62 | dc.DrawBitmap(rendered_, 0, 0); | 90 | dc.DrawBitmap(rendered_, 0, 0); |
| 63 | 91 | ||
| 64 | if (AP_GetPlayerPosition().has_value()) { | 92 | std::optional<std::tuple<int, int>> player_position; |
| 93 | if (GetTrackerConfig().track_position) | ||
| 94 | { | ||
| 95 | if (IPC_IsConnected()) { | ||
| 96 | player_position = IPC_GetPlayerPosition(); | ||
| 97 | } else { | ||
| 98 | player_position = AP_GetPlayerPosition(); | ||
| 99 | } | ||
| 100 | } | ||
| 101 | |||
| 102 | if (player_position.has_value()) { | ||
| 65 | // 1588, 1194 | 103 | // 1588, 1194 |
| 66 | // 14x14 -> 154x154 | 104 | // 14x14 -> 154x154 |
| 67 | double intended_x = | 105 | double intended_x = |
| 68 | 1588.0 + (std::get<0>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); | 106 | 1588.0 + (std::get<0>(*player_position) * (154.0 / 14.0)); |
| 69 | double intended_y = | 107 | double intended_y = |
| 70 | 1194.0 + (std::get<1>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); | 108 | 1194.0 + (std::get<1>(*player_position) * (154.0 / 14.0)); |
| 71 | 109 | ||
| 72 | int real_x = offset_x_ + scale_x_ * intended_x - scaled_player_.GetWidth() / 2; | 110 | int real_x = |
| 73 | int real_y = offset_y_ + scale_y_ * intended_y - scaled_player_.GetHeight() / 2; | 111 | offset_x_ + scale_x_ * intended_x - scaled_player_.GetWidth() / 2; |
| 112 | int real_y = | ||
| 113 | offset_y_ + scale_y_ * intended_y - scaled_player_.GetHeight() / 2; | ||
| 74 | 114 | ||
| 75 | dc.DrawBitmap(scaled_player_, real_x, real_y); | 115 | dc.DrawBitmap(scaled_player_, real_x, real_y); |
| 76 | } | 116 | } |
| @@ -92,8 +132,8 @@ void TrackerPanel::OnMouseMove(wxMouseEvent &event) { | |||
| 92 | event.Skip(); | 132 | event.Skip(); |
| 93 | } | 133 | } |
| 94 | 134 | ||
| 95 | void TrackerPanel::Redraw() { | 135 | void TrackerPanel::Resize() { |
| 96 | wxSize panel_size = GetSize(); | 136 | wxSize panel_size = GetClientSize(); |
| 97 | wxSize image_size = map_image_.GetSize(); | 137 | wxSize image_size = map_image_.GetSize(); |
| 98 | 138 | ||
| 99 | int final_x = 0; | 139 | int final_x = 0; |
| @@ -112,7 +152,7 @@ void TrackerPanel::Redraw() { | |||
| 112 | final_x = (panel_size.GetWidth() - final_width) / 2; | 152 | final_x = (panel_size.GetWidth() - final_width) / 2; |
| 113 | } | 153 | } |
| 114 | 154 | ||
| 115 | rendered_ = wxBitmap( | 155 | scaled_map_ = wxBitmap( |
| 116 | map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL) | 156 | map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL) |
| 117 | .Size(panel_size, {final_x, final_y}, 0, 0, 0)); | 157 | .Size(panel_size, {final_x, final_y}, 0, 0, 0)); |
| 118 | 158 | ||
| @@ -127,27 +167,64 @@ void TrackerPanel::Redraw() { | |||
| 127 | wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1, | 167 | wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1, |
| 128 | player_height > 0 ? player_height : 1)); | 168 | player_height > 0 ? player_height : 1)); |
| 129 | 169 | ||
| 170 | real_area_size_ = final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth(); | ||
| 171 | |||
| 172 | for (AreaIndicator &area : areas_) { | ||
| 173 | const MapArea &map_area = GD_GetMapArea(area.area_id); | ||
| 174 | |||
| 175 | int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * | ||
| 176 | final_width / image_size.GetWidth(); | ||
| 177 | int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) * | ||
| 178 | final_width / image_size.GetWidth(); | ||
| 179 | |||
| 180 | area.real_x1 = real_area_x; | ||
| 181 | area.real_x2 = real_area_x + real_area_size_; | ||
| 182 | area.real_y1 = real_area_y; | ||
| 183 | area.real_y2 = real_area_y + real_area_size_; | ||
| 184 | |||
| 185 | int popup_x = | ||
| 186 | final_x + map_area.map_x * final_width / image_size.GetWidth(); | ||
| 187 | int popup_y = | ||
| 188 | final_y + map_area.map_y * final_width / image_size.GetWidth(); | ||
| 189 | |||
| 190 | area.popup->SetClientSize( | ||
| 191 | area.popup->GetFullWidth(), | ||
| 192 | std::min(panel_size.GetHeight(), area.popup->GetFullHeight())); | ||
| 193 | |||
| 194 | if (area.popup->GetSize().GetHeight() > panel_size.GetHeight()) { | ||
| 195 | area.popup->SetSize(area.popup->GetSize().GetWidth(), | ||
| 196 | panel_size.GetHeight()); | ||
| 197 | } | ||
| 198 | |||
| 199 | if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) { | ||
| 200 | popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth(); | ||
| 201 | } | ||
| 202 | if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) { | ||
| 203 | popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight(); | ||
| 204 | } | ||
| 205 | area.popup->SetPosition({popup_x, popup_y}); | ||
| 206 | } | ||
| 207 | } | ||
| 208 | |||
| 209 | void TrackerPanel::Redraw() { | ||
| 210 | rendered_ = scaled_map_; | ||
| 211 | |||
| 130 | wxMemoryDC dc; | 212 | wxMemoryDC dc; |
| 131 | dc.SelectObject(rendered_); | 213 | dc.SelectObject(rendered_); |
| 132 | 214 | ||
| 133 | int real_area_size = | ||
| 134 | final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth(); | ||
| 135 | int actual_border_size = | 215 | int actual_border_size = |
| 136 | real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE; | 216 | real_area_size_ * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE; |
| 137 | const wxPoint upper_left_triangle[] = { | 217 | const wxPoint upper_left_triangle[] = { |
| 138 | {0, 0}, {0, real_area_size}, {real_area_size, 0}}; | 218 | {0, 0}, {0, real_area_size_}, {real_area_size_, 0}}; |
| 139 | const wxPoint lower_right_triangle[] = {{0, real_area_size - 1}, | 219 | const wxPoint lower_right_triangle[] = {{0, real_area_size_ - 1}, |
| 140 | {real_area_size - 1, 0}, | 220 | {real_area_size_ - 1, 0}, |
| 141 | {real_area_size, real_area_size}}; | 221 | {real_area_size_, real_area_size_}}; |
| 142 | 222 | ||
| 143 | for (AreaIndicator &area : areas_) { | 223 | for (AreaIndicator &area : areas_) { |
| 144 | const MapArea &map_area = GD_GetMapArea(area.area_id); | 224 | const MapArea &map_area = GD_GetMapArea(area.area_id); |
| 145 | if (!AP_IsLocationVisible(map_area.classification) && | 225 | |
| 146 | !(map_area.hunt && GetTrackerConfig().show_hunt_panels)) { | 226 | if (!area.active) { |
| 147 | area.active = false; | ||
| 148 | continue; | 227 | continue; |
| 149 | } else { | ||
| 150 | area.active = true; | ||
| 151 | } | 228 | } |
| 152 | 229 | ||
| 153 | bool has_reachable_unchecked = false; | 230 | bool has_reachable_unchecked = false; |
| @@ -156,10 +233,15 @@ void TrackerPanel::Redraw() { | |||
| 156 | bool has_unchecked = false; | 233 | bool has_unchecked = false; |
| 157 | if (IsLocationWinCondition(section)) { | 234 | if (IsLocationWinCondition(section)) { |
| 158 | has_unchecked = !AP_HasReachedGoal(); | 235 | has_unchecked = !AP_HasReachedGoal(); |
| 159 | } else if (AP_IsLocationVisible(section.classification)) { | 236 | } else if (AP_IsLocationVisible(section.classification) && |
| 237 | !IsLocationPostgame(section.ap_location_id)) { | ||
| 160 | has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); | 238 | has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); |
| 161 | } else if (section.hunt && GetTrackerConfig().show_hunt_panels) { | 239 | } else if ((section.hunt && GetTrackerConfig().visible_panels == |
| 162 | has_unchecked = !AP_HasCheckedHuntPanel(section.ap_location_id); | 240 | TrackerConfig::kHUNT_PANELS) || |
| 241 | (section.single_panel && GetTrackerConfig().visible_panels == | ||
| 242 | TrackerConfig::kALL_PANELS)) { | ||
| 243 | has_unchecked = | ||
| 244 | !AP_IsPanelSolved(GD_GetPanel(*section.single_panel).solve_index); | ||
| 163 | } | 245 | } |
| 164 | 246 | ||
| 165 | if (has_unchecked) { | 247 | if (has_unchecked) { |
| @@ -171,10 +253,26 @@ void TrackerPanel::Redraw() { | |||
| 171 | } | 253 | } |
| 172 | } | 254 | } |
| 173 | 255 | ||
| 174 | int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * | 256 | if (AP_IsPaintingShuffle()) { |
| 175 | final_width / image_size.GetWidth(); | 257 | for (int painting_id : map_area.paintings) { |
| 176 | int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) * | 258 | if (IsPaintingPostgame(painting_id)) { |
| 177 | final_width / image_size.GetWidth(); | 259 | continue; |
| 260 | } | ||
| 261 | |||
| 262 | const PaintingExit &painting = GD_GetPaintingExit(painting_id); | ||
| 263 | bool reachable = IsPaintingReachable(painting_id); | ||
| 264 | if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) { | ||
| 265 | if (reachable) { | ||
| 266 | has_reachable_unchecked = true; | ||
| 267 | } else { | ||
| 268 | has_unreachable_unchecked = true; | ||
| 269 | } | ||
| 270 | } | ||
| 271 | } | ||
| 272 | } | ||
| 273 | |||
| 274 | int real_area_x = area.real_x1; | ||
| 275 | int real_area_y = area.real_y1; | ||
| 178 | 276 | ||
| 179 | if (has_reachable_unchecked && has_unreachable_unchecked && | 277 | if (has_reachable_unchecked && has_unreachable_unchecked && |
| 180 | GetTrackerConfig().hybrid_areas) { | 278 | GetTrackerConfig().hybrid_areas) { |
| @@ -188,7 +286,7 @@ void TrackerPanel::Redraw() { | |||
| 188 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); | 286 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); |
| 189 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | 287 | dc.SetBrush(*wxTRANSPARENT_BRUSH); |
| 190 | dc.DrawRectangle({real_area_x, real_area_y}, | 288 | dc.DrawRectangle({real_area_x, real_area_y}, |
| 191 | {real_area_size, real_area_size}); | 289 | {real_area_size_, real_area_size_}); |
| 192 | 290 | ||
| 193 | } else { | 291 | } else { |
| 194 | const wxBrush *brush_color = wxGREY_BRUSH; | 292 | const wxBrush *brush_color = wxGREY_BRUSH; |
| @@ -203,30 +301,7 @@ void TrackerPanel::Redraw() { | |||
| 203 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); | 301 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); |
| 204 | dc.SetBrush(*brush_color); | 302 | dc.SetBrush(*brush_color); |
| 205 | dc.DrawRectangle({real_area_x, real_area_y}, | 303 | dc.DrawRectangle({real_area_x, real_area_y}, |
| 206 | {real_area_size, real_area_size}); | 304 | {real_area_size_, real_area_size_}); |
| 207 | } | 305 | } |
| 208 | |||
| 209 | area.real_x1 = real_area_x; | ||
| 210 | area.real_x2 = real_area_x + real_area_size; | ||
| 211 | area.real_y1 = real_area_y; | ||
| 212 | area.real_y2 = real_area_y + real_area_size; | ||
| 213 | |||
| 214 | int popup_x = | ||
| 215 | final_x + map_area.map_x * final_width / image_size.GetWidth(); | ||
| 216 | int popup_y = | ||
| 217 | final_y + map_area.map_y * final_width / image_size.GetWidth(); | ||
| 218 | |||
| 219 | area.popup->SetClientSize( | ||
| 220 | area.popup->GetVirtualSize().GetWidth(), | ||
| 221 | std::min(panel_size.GetHeight(), | ||
| 222 | area.popup->GetVirtualSize().GetHeight())); | ||
| 223 | |||
| 224 | if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) { | ||
| 225 | popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth(); | ||
| 226 | } | ||
| 227 | if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) { | ||
| 228 | popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight(); | ||
| 229 | } | ||
| 230 | area.popup->SetPosition({popup_x, popup_y}); | ||
| 231 | } | 306 | } |
| 232 | } | 307 | } |
| diff --git a/src/tracker_panel.h b/src/tracker_panel.h index 06ec7a0..6825843 100644 --- a/src/tracker_panel.h +++ b/src/tracker_panel.h | |||
| @@ -7,13 +7,17 @@ | |||
| 7 | #include <wx/wx.h> | 7 | #include <wx/wx.h> |
| 8 | #endif | 8 | #endif |
| 9 | 9 | ||
| 10 | #include <optional> | ||
| 11 | #include <set> | ||
| 12 | #include <string> | ||
| 13 | |||
| 10 | class AreaPopup; | 14 | class AreaPopup; |
| 11 | 15 | ||
| 12 | class TrackerPanel : public wxPanel { | 16 | class TrackerPanel : public wxPanel { |
| 13 | public: | 17 | public: |
| 14 | TrackerPanel(wxWindow *parent); | 18 | TrackerPanel(wxWindow *parent); |
| 15 | 19 | ||
| 16 | void UpdateIndicators(); | 20 | void UpdateIndicators(bool reset); |
| 17 | 21 | ||
| 18 | private: | 22 | private: |
| 19 | struct AreaIndicator { | 23 | struct AreaIndicator { |
| @@ -29,10 +33,12 @@ class TrackerPanel : public wxPanel { | |||
| 29 | void OnPaint(wxPaintEvent &event); | 33 | void OnPaint(wxPaintEvent &event); |
| 30 | void OnMouseMove(wxMouseEvent &event); | 34 | void OnMouseMove(wxMouseEvent &event); |
| 31 | 35 | ||
| 36 | void Resize(); | ||
| 32 | void Redraw(); | 37 | void Redraw(); |
| 33 | 38 | ||
| 34 | wxImage map_image_; | 39 | wxImage map_image_; |
| 35 | wxImage player_image_; | 40 | wxImage player_image_; |
| 41 | wxBitmap scaled_map_; | ||
| 36 | wxBitmap rendered_; | 42 | wxBitmap rendered_; |
| 37 | wxBitmap scaled_player_; | 43 | wxBitmap scaled_player_; |
| 38 | 44 | ||
| @@ -40,6 +46,7 @@ class TrackerPanel : public wxPanel { | |||
| 40 | int offset_y_ = 0; | 46 | int offset_y_ = 0; |
| 41 | double scale_x_ = 0; | 47 | double scale_x_ = 0; |
| 42 | double scale_y_ = 0; | 48 | double scale_y_ = 0; |
| 49 | int real_area_size_ = 0; | ||
| 43 | 50 | ||
| 44 | std::vector<AreaIndicator> areas_; | 51 | std::vector<AreaIndicator> areas_; |
| 45 | }; | 52 | }; |
| diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index 5588c7f..674f68a 100644 --- a/src/tracker_state.cpp +++ b/src/tracker_state.cpp | |||
| @@ -1,5 +1,8 @@ | |||
| 1 | #include "tracker_state.h" | 1 | #include "tracker_state.h" |
| 2 | 2 | ||
| 3 | #include <fmt/core.h> | ||
| 4 | #include <hkutil/string.h> | ||
| 5 | |||
| 3 | #include <list> | 6 | #include <list> |
| 4 | #include <map> | 7 | #include <map> |
| 5 | #include <mutex> | 8 | #include <mutex> |
| @@ -9,13 +12,172 @@ | |||
| 9 | 12 | ||
| 10 | #include "ap_state.h" | 13 | #include "ap_state.h" |
| 11 | #include "game_data.h" | 14 | #include "game_data.h" |
| 15 | #include "global.h" | ||
| 16 | #include "logger.h" | ||
| 12 | 17 | ||
| 13 | namespace { | 18 | namespace { |
| 14 | 19 | ||
| 20 | struct Requirements { | ||
| 21 | bool disabled = false; | ||
| 22 | |||
| 23 | std::set<int> doors; // non-grouped, handles progressive | ||
| 24 | std::set<int> panel_doors; // non-grouped, handles progressive | ||
| 25 | std::set<int> items; // all other items | ||
| 26 | std::set<int> rooms; // maybe | ||
| 27 | bool mastery = false; // maybe | ||
| 28 | bool panel_hunt = false; // maybe | ||
| 29 | bool postgame = false; | ||
| 30 | |||
| 31 | void Merge(const Requirements& rhs) { | ||
| 32 | if (rhs.disabled) { | ||
| 33 | return; | ||
| 34 | } | ||
| 35 | |||
| 36 | for (int id : rhs.doors) { | ||
| 37 | doors.insert(id); | ||
| 38 | } | ||
| 39 | for (int id : rhs.panel_doors) { | ||
| 40 | panel_doors.insert(id); | ||
| 41 | } | ||
| 42 | for (int id : rhs.items) { | ||
| 43 | items.insert(id); | ||
| 44 | } | ||
| 45 | for (int id : rhs.rooms) { | ||
| 46 | rooms.insert(id); | ||
| 47 | } | ||
| 48 | mastery = mastery || rhs.mastery; | ||
| 49 | panel_hunt = panel_hunt || rhs.panel_hunt; | ||
| 50 | postgame = postgame || rhs.postgame; | ||
| 51 | } | ||
| 52 | }; | ||
| 53 | |||
| 54 | class RequirementCalculator { | ||
| 55 | public: | ||
| 56 | void Reset() { | ||
| 57 | doors_.clear(); | ||
| 58 | panels_.clear(); | ||
| 59 | } | ||
| 60 | |||
| 61 | const Requirements& GetDoor(int door_id) { | ||
| 62 | if (!doors_.count(door_id)) { | ||
| 63 | Requirements requirements; | ||
| 64 | const Door& door_obj = GD_GetDoor(door_id); | ||
| 65 | |||
| 66 | if (door_obj.type == DoorType::kSunPainting) { | ||
| 67 | if (!AP_IsPilgrimageEnabled()) { | ||
| 68 | requirements.items.insert(door_obj.ap_item_id); | ||
| 69 | } else { | ||
| 70 | requirements.disabled = true; | ||
| 71 | } | ||
| 72 | } else if (door_obj.type == DoorType::kSunwarp) { | ||
| 73 | switch (AP_GetSunwarpAccess()) { | ||
| 74 | case kSUNWARP_ACCESS_NORMAL: | ||
| 75 | // Do nothing. | ||
| 76 | break; | ||
| 77 | case kSUNWARP_ACCESS_DISABLED: | ||
| 78 | requirements.disabled = true; | ||
| 79 | break; | ||
| 80 | case kSUNWARP_ACCESS_UNLOCK: | ||
| 81 | requirements.items.insert(door_obj.group_ap_item_id); | ||
| 82 | break; | ||
| 83 | case kSUNWARP_ACCESS_INDIVIDUAL: | ||
| 84 | case kSUNWARP_ACCESS_PROGRESSIVE: | ||
| 85 | requirements.doors.insert(door_obj.id); | ||
| 86 | break; | ||
| 87 | } | ||
| 88 | } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) { | ||
| 89 | for (int panel_id : door_obj.panels) { | ||
| 90 | const Requirements& panel_reqs = GetPanel(panel_id); | ||
| 91 | requirements.Merge(panel_reqs); | ||
| 92 | } | ||
| 93 | } else if (AP_AreDoorsGrouped() && !door_obj.group_name.empty()) { | ||
| 94 | requirements.items.insert(door_obj.group_ap_item_id); | ||
| 95 | } else { | ||
| 96 | requirements.doors.insert(door_obj.id); | ||
| 97 | } | ||
| 98 | |||
| 99 | doors_[door_id] = requirements; | ||
| 100 | } | ||
| 101 | |||
| 102 | return doors_[door_id]; | ||
| 103 | } | ||
| 104 | |||
| 105 | const Requirements& GetPanel(int panel_id) { | ||
| 106 | if (!panels_.count(panel_id)) { | ||
| 107 | Requirements requirements; | ||
| 108 | const Panel& panel_obj = GD_GetPanel(panel_id); | ||
| 109 | |||
| 110 | requirements.rooms.insert(panel_obj.room); | ||
| 111 | |||
| 112 | if (panel_obj.name == "THE MASTER") { | ||
| 113 | requirements.mastery = true; | ||
| 114 | } | ||
| 115 | |||
| 116 | if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") && | ||
| 117 | AP_GetLevel2Requirement() > 1) { | ||
| 118 | requirements.panel_hunt = true; | ||
| 119 | } | ||
| 120 | |||
| 121 | for (int room_id : panel_obj.required_rooms) { | ||
| 122 | requirements.rooms.insert(room_id); | ||
| 123 | } | ||
| 124 | |||
| 125 | for (int door_id : panel_obj.required_doors) { | ||
| 126 | const Requirements& door_reqs = GetDoor(door_id); | ||
| 127 | requirements.Merge(door_reqs); | ||
| 128 | } | ||
| 129 | |||
| 130 | for (int panel_id : panel_obj.required_panels) { | ||
| 131 | const Requirements& panel_reqs = GetPanel(panel_id); | ||
| 132 | requirements.Merge(panel_reqs); | ||
| 133 | } | ||
| 134 | |||
| 135 | if (AP_IsColorShuffle()) { | ||
| 136 | for (LingoColor color : panel_obj.colors) { | ||
| 137 | requirements.items.insert(GD_GetItemIdForColor(color)); | ||
| 138 | } | ||
| 139 | } | ||
| 140 | |||
| 141 | if (panel_obj.panel_door != -1 && | ||
| 142 | AP_GetDoorShuffleMode() == kPANELS_MODE) { | ||
| 143 | const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_obj.panel_door); | ||
| 144 | |||
| 145 | if (panel_door_obj.group_ap_item_id != -1 && AP_AreDoorsGrouped()) { | ||
| 146 | requirements.items.insert(panel_door_obj.group_ap_item_id); | ||
| 147 | } else { | ||
| 148 | requirements.panel_doors.insert(panel_obj.panel_door); | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | if (panel_obj.location_name == GetWinCondition()) { | ||
| 153 | requirements.postgame = true; | ||
| 154 | } | ||
| 155 | |||
| 156 | panels_[panel_id] = requirements; | ||
| 157 | } | ||
| 158 | |||
| 159 | return panels_[panel_id]; | ||
| 160 | } | ||
| 161 | |||
| 162 | private: | ||
| 163 | std::map<int, Requirements> doors_; | ||
| 164 | std::map<int, Requirements> panels_; | ||
| 165 | }; | ||
| 166 | |||
| 15 | struct TrackerState { | 167 | struct TrackerState { |
| 16 | std::map<int, bool> reachability; | 168 | std::map<int, bool> reachability; |
| 17 | std::set<int> reachable_doors; | 169 | std::set<int> reachable_doors; |
| 170 | std::set<int> solveable_panels; | ||
| 171 | std::set<int> reachable_paintings; | ||
| 18 | std::mutex reachability_mutex; | 172 | std::mutex reachability_mutex; |
| 173 | RequirementCalculator requirements; | ||
| 174 | std::map<int, std::map<std::string, bool>> door_reports; | ||
| 175 | bool pilgrimage_doable = false; | ||
| 176 | |||
| 177 | // If these are empty, it actually means everything is non-postgame. | ||
| 178 | std::set<int> non_postgame_areas; | ||
| 179 | std::set<int> non_postgame_locations; | ||
| 180 | std::set<int> non_postgame_paintings; | ||
| 19 | }; | 181 | }; |
| 20 | 182 | ||
| 21 | enum Decision { kYes, kNo, kMaybe }; | 183 | enum Decision { kYes, kNo, kMaybe }; |
| @@ -30,6 +192,11 @@ class StateCalculator; | |||
| 30 | struct StateCalculatorOptions { | 192 | struct StateCalculatorOptions { |
| 31 | int start; | 193 | int start; |
| 32 | bool pilgrimage = false; | 194 | bool pilgrimage = false; |
| 195 | |||
| 196 | // Treats all items as collected and all paintings as checked, but postgame | ||
| 197 | // areas cannot be reached. | ||
| 198 | bool postgame_detection = false; | ||
| 199 | |||
| 33 | StateCalculator* parent = nullptr; | 200 | StateCalculator* parent = nullptr; |
| 34 | }; | 201 | }; |
| 35 | 202 | ||
| @@ -40,15 +207,33 @@ class StateCalculator { | |||
| 40 | explicit StateCalculator(StateCalculatorOptions options) | 207 | explicit StateCalculator(StateCalculatorOptions options) |
| 41 | : options_(options) {} | 208 | : options_(options) {} |
| 42 | 209 | ||
| 210 | void PreloadPanels(const std::set<int>& panels) { | ||
| 211 | solveable_panels_ = panels; | ||
| 212 | } | ||
| 213 | |||
| 214 | void PreloadDoors(const std::set<int>& doors) { | ||
| 215 | for (int door_id : doors) { | ||
| 216 | door_decisions_[door_id] = kYes; | ||
| 217 | } | ||
| 218 | } | ||
| 219 | |||
| 43 | void Calculate() { | 220 | void Calculate() { |
| 221 | painting_mapping_ = AP_GetPaintingMapping(); | ||
| 222 | checked_paintings_ = AP_GetCheckedPaintings(); | ||
| 223 | sunwarp_mapping_ = AP_GetSunwarpMapping(); | ||
| 224 | |||
| 44 | std::list<int> panel_boundary; | 225 | std::list<int> panel_boundary; |
| 226 | std::list<int> painting_boundary; | ||
| 45 | std::list<Exit> flood_boundary; | 227 | std::list<Exit> flood_boundary; |
| 46 | flood_boundary.push_back({.destination_room = options_.start}); | 228 | flood_boundary.push_back( |
| 229 | {.source_room = -1, .destination_room = options_.start}); | ||
| 47 | 230 | ||
| 48 | bool reachable_changed = true; | 231 | bool reachable_changed = true; |
| 49 | while (reachable_changed) { | 232 | while (reachable_changed) { |
| 50 | reachable_changed = false; | 233 | reachable_changed = false; |
| 51 | 234 | ||
| 235 | std::list<Exit> new_boundary; | ||
| 236 | |||
| 52 | std::list<int> new_panel_boundary; | 237 | std::list<int> new_panel_boundary; |
| 53 | for (int panel_id : panel_boundary) { | 238 | for (int panel_id : panel_boundary) { |
| 54 | if (solveable_panels_.count(panel_id)) { | 239 | if (solveable_panels_.count(panel_id)) { |
| @@ -64,7 +249,36 @@ class StateCalculator { | |||
| 64 | } | 249 | } |
| 65 | } | 250 | } |
| 66 | 251 | ||
| 67 | std::list<Exit> new_boundary; | 252 | std::list<int> new_painting_boundary; |
| 253 | for (int painting_id : painting_boundary) { | ||
| 254 | if (reachable_paintings_.count(painting_id)) { | ||
| 255 | continue; | ||
| 256 | } | ||
| 257 | |||
| 258 | Decision painting_reachable = IsPaintingReachable(painting_id); | ||
| 259 | if (painting_reachable == kYes) { | ||
| 260 | reachable_paintings_.insert(painting_id); | ||
| 261 | reachable_changed = true; | ||
| 262 | |||
| 263 | PaintingExit cur_painting = GD_GetPaintingExit(painting_id); | ||
| 264 | if (painting_mapping_.count(cur_painting.internal_id) && | ||
| 265 | (checked_paintings_.count(cur_painting.internal_id) || | ||
| 266 | options_.postgame_detection)) { | ||
| 267 | Exit painting_exit; | ||
| 268 | PaintingExit target_painting = | ||
| 269 | GD_GetPaintingExit(GD_GetPaintingByName( | ||
| 270 | painting_mapping_.at(cur_painting.internal_id))); | ||
| 271 | painting_exit.source_room = cur_painting.room; | ||
| 272 | painting_exit.destination_room = target_painting.room; | ||
| 273 | painting_exit.type = EntranceType::kPainting; | ||
| 274 | |||
| 275 | new_boundary.push_back(painting_exit); | ||
| 276 | } | ||
| 277 | } else if (painting_reachable == kMaybe) { | ||
| 278 | new_painting_boundary.push_back(painting_id); | ||
| 279 | } | ||
| 280 | } | ||
| 281 | |||
| 68 | for (const Exit& room_exit : flood_boundary) { | 282 | for (const Exit& room_exit : flood_boundary) { |
| 69 | if (reachable_rooms_.count(room_exit.destination_room)) { | 283 | if (reachable_rooms_.count(room_exit.destination_room)) { |
| 70 | continue; | 284 | continue; |
| @@ -83,6 +297,12 @@ class StateCalculator { | |||
| 83 | reachable_rooms_.insert(room_exit.destination_room); | 297 | reachable_rooms_.insert(room_exit.destination_room); |
| 84 | reachable_changed = true; | 298 | reachable_changed = true; |
| 85 | 299 | ||
| 300 | #ifndef NDEBUG | ||
| 301 | std::list<int> room_path = paths_[room_exit.source_room]; | ||
| 302 | room_path.push_back(room_exit.destination_room); | ||
| 303 | paths_[room_exit.destination_room] = room_path; | ||
| 304 | #endif | ||
| 305 | |||
| 86 | const Room& room_obj = GD_GetRoom(room_exit.destination_room); | 306 | const Room& room_obj = GD_GetRoom(room_exit.destination_room); |
| 87 | for (const Exit& out_edge : room_obj.exits) { | 307 | for (const Exit& out_edge : room_obj.exits) { |
| 88 | if (out_edge.type == EntranceType::kPainting && | 308 | if (out_edge.type == EntranceType::kPainting && |
| @@ -99,52 +319,56 @@ class StateCalculator { | |||
| 99 | } | 319 | } |
| 100 | 320 | ||
| 101 | if (AP_IsPaintingShuffle()) { | 321 | if (AP_IsPaintingShuffle()) { |
| 102 | for (const PaintingExit& out_edge : room_obj.paintings) { | 322 | for (int out_edge : room_obj.paintings) { |
| 103 | if (AP_GetPaintingMapping().count(out_edge.id)) { | 323 | new_painting_boundary.push_back(out_edge); |
| 104 | Exit painting_exit; | ||
| 105 | painting_exit.destination_room = GD_GetRoomForPainting( | ||
| 106 | AP_GetPaintingMapping().at(out_edge.id)); | ||
| 107 | painting_exit.door = out_edge.door; | ||
| 108 | |||
| 109 | new_boundary.push_back(painting_exit); | ||
| 110 | } | ||
| 111 | } | 324 | } |
| 112 | } | 325 | } |
| 113 | 326 | ||
| 114 | if (AP_IsSunwarpShuffle()) { | 327 | if (AP_IsSunwarpShuffle()) { |
| 115 | for (int index : room_obj.sunwarps) { | 328 | for (int index : room_obj.sunwarps) { |
| 116 | if (AP_GetSunwarpMapping().count(index)) { | 329 | if (sunwarp_mapping_.count(index)) { |
| 117 | const SunwarpMapping& sm = AP_GetSunwarpMapping().at(index); | 330 | const SunwarpMapping& sm = sunwarp_mapping_.at(index); |
| 118 | 331 | ||
| 119 | Exit sunwarp_exit; | 332 | new_boundary.push_back( |
| 120 | sunwarp_exit.destination_room = | 333 | {.source_room = room_exit.destination_room, |
| 121 | GD_GetRoomForSunwarp(sm.exit_index); | 334 | .destination_room = GD_GetRoomForSunwarp(sm.exit_index), |
| 122 | sunwarp_exit.door = GD_GetSunwarpDoors().at(sm.dots - 1); | 335 | .door = GD_GetSunwarpDoors().at(sm.dots - 1), |
| 123 | 336 | .type = EntranceType::kSunwarp}); | |
| 124 | new_boundary.push_back(sunwarp_exit); | ||
| 125 | } | 337 | } |
| 126 | } | 338 | } |
| 127 | } | 339 | } |
| 128 | 340 | ||
| 129 | if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") { | 341 | if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") { |
| 130 | new_boundary.push_back( | 342 | new_boundary.push_back( |
| 131 | {.destination_room = GD_GetRoomByName("Outside The Undeterred"), | 343 | {.source_room = room_exit.destination_room, |
| 132 | .type = EntranceType::kPainting}); | 344 | .destination_room = GD_GetRoomByName("Color Hallways"), |
| 345 | .type = EntranceType::kStaticPainting}); | ||
| 133 | } | 346 | } |
| 134 | 347 | ||
| 135 | if (AP_IsPilgrimageEnabled()) { | 348 | if (AP_IsPilgrimageEnabled()) { |
| 136 | if (room_obj.name == "Hub Room") { | 349 | int pilgrimage_start_id = GD_GetRoomByName("Hub Room"); |
| 350 | if (AP_IsSunwarpShuffle()) { | ||
| 351 | for (const auto& [start_index, mapping] : sunwarp_mapping_) { | ||
| 352 | if (mapping.dots == 1) { | ||
| 353 | pilgrimage_start_id = GD_GetRoomForSunwarp(start_index); | ||
| 354 | } | ||
| 355 | } | ||
| 356 | } | ||
| 357 | |||
| 358 | if (room_exit.destination_room == pilgrimage_start_id) { | ||
| 137 | new_boundary.push_back( | 359 | new_boundary.push_back( |
| 138 | {.destination_room = GD_GetRoomByName("Pilgrim Antechamber"), | 360 | {.source_room = room_exit.destination_room, |
| 361 | .destination_room = GD_GetRoomByName("Pilgrim Antechamber"), | ||
| 139 | .type = EntranceType::kPilgrimage}); | 362 | .type = EntranceType::kPilgrimage}); |
| 140 | } | 363 | } |
| 141 | } else { | 364 | } else { |
| 142 | if (room_obj.name == "Starting Room") { | 365 | if (room_obj.name == "Starting Room") { |
| 143 | new_boundary.push_back( | 366 | new_boundary.push_back( |
| 144 | {.destination_room = GD_GetRoomByName("Pilgrim Antechamber"), | 367 | {.source_room = room_exit.destination_room, |
| 368 | .destination_room = GD_GetRoomByName("Pilgrim Antechamber"), | ||
| 145 | .door = | 369 | .door = |
| 146 | GD_GetDoorByName("Pilgrim Antechamber - Sun Painting"), | 370 | GD_GetDoorByName("Pilgrim Antechamber - Sun Painting"), |
| 147 | .type = EntranceType::kPainting}); | 371 | .type = EntranceType::kStaticPainting}); |
| 148 | } | 372 | } |
| 149 | } | 373 | } |
| 150 | 374 | ||
| @@ -156,11 +380,17 @@ class StateCalculator { | |||
| 156 | 380 | ||
| 157 | flood_boundary = new_boundary; | 381 | flood_boundary = new_boundary; |
| 158 | panel_boundary = new_panel_boundary; | 382 | panel_boundary = new_panel_boundary; |
| 383 | painting_boundary = new_painting_boundary; | ||
| 159 | } | 384 | } |
| 160 | 385 | ||
| 161 | // Now that we know the full reachable area, let's make sure all doors are evaluated. | 386 | // Now that we know the full reachable area, let's make sure all doors are |
| 387 | // evaluated. | ||
| 162 | for (const Door& door : GD_GetDoors()) { | 388 | for (const Door& door : GD_GetDoors()) { |
| 163 | int discard = IsDoorReachable(door.id); | 389 | int discard = IsDoorReachable(door.id); |
| 390 | |||
| 391 | door_report_[door.id] = {}; | ||
| 392 | discard = AreRequirementsSatisfied( | ||
| 393 | GetState().requirements.GetDoor(door.id), &door_report_[door.id]); | ||
| 164 | } | 394 | } |
| 165 | } | 395 | } |
| 166 | 396 | ||
| @@ -172,8 +402,32 @@ class StateCalculator { | |||
| 172 | 402 | ||
| 173 | const std::set<int>& GetSolveablePanels() const { return solveable_panels_; } | 403 | const std::set<int>& GetSolveablePanels() const { return solveable_panels_; } |
| 174 | 404 | ||
| 405 | const std::set<int>& GetReachablePaintings() const { | ||
| 406 | return reachable_paintings_; | ||
| 407 | } | ||
| 408 | |||
| 409 | const std::map<int, std::map<std::string, bool>>& GetDoorReports() const { | ||
| 410 | return door_report_; | ||
| 411 | } | ||
| 412 | |||
| 413 | bool IsPilgrimageDoable() const { return pilgrimage_doable_; } | ||
| 414 | |||
| 415 | std::string GetPathToRoom(int room_id) const { | ||
| 416 | if (!paths_.count(room_id)) { | ||
| 417 | return ""; | ||
| 418 | } | ||
| 419 | |||
| 420 | const std::list<int>& path = paths_.at(room_id); | ||
| 421 | std::vector<std::string> room_names; | ||
| 422 | for (int room_id : path) { | ||
| 423 | room_names.push_back(GD_GetRoom(room_id).name); | ||
| 424 | } | ||
| 425 | return hatkirby::implode(room_names, " -> "); | ||
| 426 | } | ||
| 427 | |||
| 175 | private: | 428 | private: |
| 176 | Decision IsNonGroupedDoorReachable(const Door& door_obj) { | 429 | template <typename T> |
| 430 | Decision IsNonGroupedDoorReachable(const T& door_obj) { | ||
| 177 | bool has_item = AP_HasItem(door_obj.ap_item_id); | 431 | bool has_item = AP_HasItem(door_obj.ap_item_id); |
| 178 | 432 | ||
| 179 | if (!has_item) { | 433 | if (!has_item) { |
| @@ -188,68 +442,71 @@ class StateCalculator { | |||
| 188 | return has_item ? kYes : kNo; | 442 | return has_item ? kYes : kNo; |
| 189 | } | 443 | } |
| 190 | 444 | ||
| 191 | Decision IsDoorReachable_Helper(int door_id) { | 445 | Decision AreRequirementsSatisfied( |
| 192 | const Door& door_obj = GD_GetDoor(door_id); | 446 | const Requirements& reqs, std::map<std::string, bool>* report = nullptr) { |
| 193 | 447 | if (reqs.disabled) { | |
| 194 | if (!AP_IsPilgrimageEnabled() && door_obj.type == DoorType::kSunPainting) { | 448 | return kNo; |
| 195 | return AP_HasItem(door_obj.ap_item_id) ? kYes : kNo; | 449 | } |
| 196 | } else if (door_obj.type == DoorType::kSunwarp) { | 450 | |
| 197 | switch (AP_GetSunwarpAccess()) { | 451 | if (reqs.postgame && options_.postgame_detection) { |
| 198 | case kSUNWARP_ACCESS_NORMAL: | 452 | return kNo; |
| 199 | return kYes; | 453 | } |
| 200 | case kSUNWARP_ACCESS_DISABLED: | 454 | |
| 201 | return kNo; | 455 | Decision final_decision = kYes; |
| 202 | case kSUNWARP_ACCESS_UNLOCK: | 456 | |
| 203 | return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo; | 457 | if (!options_.postgame_detection) { |
| 204 | case kSUNWARP_ACCESS_INDIVIDUAL: | 458 | for (int door_id : reqs.doors) { |
| 205 | case kSUNWARP_ACCESS_PROGRESSIVE: | 459 | const Door& door_obj = GD_GetDoor(door_id); |
| 206 | return IsNonGroupedDoorReachable(door_obj); | 460 | Decision decision = IsNonGroupedDoorReachable(door_obj); |
| 207 | } | 461 | |
| 208 | } else if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) { | 462 | if (report) { |
| 209 | if (!reachable_rooms_.count(door_obj.room)) { | 463 | (*report)[door_obj.item_name] = (decision == kYes); |
| 210 | return kMaybe; | 464 | } |
| 211 | } | 465 | |
| 212 | 466 | if (decision != kYes) { | |
| 213 | for (int panel_id : door_obj.panels) { | 467 | final_decision = decision; |
| 214 | if (!solveable_panels_.count(panel_id)) { | ||
| 215 | return kMaybe; | ||
| 216 | } | 468 | } |
| 217 | } | 469 | } |
| 218 | 470 | ||
| 219 | return kYes; | 471 | for (int panel_door_id : reqs.panel_doors) { |
| 220 | } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS && | 472 | const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_door_id); |
| 221 | !door_obj.group_name.empty()) { | 473 | Decision decision = IsNonGroupedDoorReachable(panel_door_obj); |
| 222 | return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo; | ||
| 223 | } else { | ||
| 224 | return IsNonGroupedDoorReachable(door_obj); | ||
| 225 | } | ||
| 226 | } | ||
| 227 | 474 | ||
| 228 | Decision IsDoorReachable(int door_id) { | 475 | if (report) { |
| 229 | if (options_.parent) { | 476 | (*report)[panel_door_obj.item_name] = (decision == kYes); |
| 230 | return options_.parent->IsDoorReachable(door_id); | 477 | } |
| 231 | } | ||
| 232 | 478 | ||
| 233 | if (door_decisions_.count(door_id)) { | 479 | if (decision != kYes) { |
| 234 | return door_decisions_.at(door_id); | 480 | final_decision = decision; |
| 235 | } | 481 | } |
| 482 | } | ||
| 236 | 483 | ||
| 237 | Decision result = IsDoorReachable_Helper(door_id); | 484 | for (int item_id : reqs.items) { |
| 238 | if (result != kMaybe) { | 485 | bool has_item = AP_HasItem(item_id); |
| 239 | door_decisions_[door_id] = result; | 486 | if (report) { |
| 487 | (*report)[GD_GetItemName(item_id)] = has_item; | ||
| 488 | } | ||
| 489 | |||
| 490 | if (!has_item) { | ||
| 491 | final_decision = kNo; | ||
| 492 | } | ||
| 493 | } | ||
| 240 | } | 494 | } |
| 241 | 495 | ||
| 242 | return result; | 496 | for (int room_id : reqs.rooms) { |
| 243 | } | 497 | bool reachable = reachable_rooms_.count(room_id); |
| 244 | 498 | ||
| 245 | Decision IsPanelReachable(int panel_id) { | 499 | if (report) { |
| 246 | const Panel& panel_obj = GD_GetPanel(panel_id); | 500 | std::string report_name = "Reach \"" + GD_GetRoom(room_id).name + "\""; |
| 501 | (*report)[report_name] = reachable; | ||
| 502 | } | ||
| 247 | 503 | ||
| 248 | if (!reachable_rooms_.count(panel_obj.room)) { | 504 | if (!reachable && final_decision != kNo) { |
| 249 | return kMaybe; | 505 | final_decision = kMaybe; |
| 506 | } | ||
| 250 | } | 507 | } |
| 251 | 508 | ||
| 252 | if (panel_obj.name == "THE MASTER") { | 509 | if (reqs.mastery) { |
| 253 | int achievements_accessible = 0; | 510 | int achievements_accessible = 0; |
| 254 | 511 | ||
| 255 | for (int achieve_id : GD_GetAchievementPanels()) { | 512 | for (int achieve_id : GD_GetAchievementPanels()) { |
| @@ -262,12 +519,18 @@ class StateCalculator { | |||
| 262 | } | 519 | } |
| 263 | } | 520 | } |
| 264 | 521 | ||
| 265 | return (achievements_accessible >= AP_GetMasteryRequirement()) ? kYes | 522 | bool can_mastery = |
| 266 | : kMaybe; | 523 | (achievements_accessible >= AP_GetMasteryRequirement()); |
| 524 | if (report) { | ||
| 525 | (*report)["Mastery"] = can_mastery; | ||
| 526 | } | ||
| 527 | |||
| 528 | if (!can_mastery && final_decision != kNo) { | ||
| 529 | final_decision = kMaybe; | ||
| 530 | } | ||
| 267 | } | 531 | } |
| 268 | 532 | ||
| 269 | if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") && | 533 | if (reqs.panel_hunt) { |
| 270 | AP_GetLevel2Requirement() > 1) { | ||
| 271 | int counting_panels_accessible = 0; | 534 | int counting_panels_accessible = 0; |
| 272 | 535 | ||
| 273 | for (int solved_panel_id : solveable_panels_) { | 536 | for (int solved_panel_id : solveable_panels_) { |
| @@ -278,41 +541,51 @@ class StateCalculator { | |||
| 278 | } | 541 | } |
| 279 | } | 542 | } |
| 280 | 543 | ||
| 281 | return (counting_panels_accessible >= AP_GetLevel2Requirement() - 1) | 544 | bool can_level2 = |
| 282 | ? kYes | 545 | (counting_panels_accessible >= AP_GetLevel2Requirement() - 1); |
| 283 | : kMaybe; | 546 | if (report) { |
| 284 | } | 547 | std::string report_name = |
| 548 | std::to_string(AP_GetLevel2Requirement()) + " Panels"; | ||
| 549 | (*report)[report_name] = can_level2; | ||
| 550 | } | ||
| 285 | 551 | ||
| 286 | for (int room_id : panel_obj.required_rooms) { | 552 | if (!can_level2 && final_decision != kNo) { |
| 287 | if (!reachable_rooms_.count(room_id)) { | 553 | final_decision = kMaybe; |
| 288 | return kMaybe; | ||
| 289 | } | 554 | } |
| 290 | } | 555 | } |
| 291 | 556 | ||
| 292 | for (int door_id : panel_obj.required_doors) { | 557 | return final_decision; |
| 293 | Decision door_reachable = IsDoorReachable(door_id); | 558 | } |
| 294 | if (door_reachable == kNo) { | 559 | |
| 295 | const Door& door_obj = GD_GetDoor(door_id); | 560 | Decision IsDoorReachable_Helper(int door_id) { |
| 296 | return (door_obj.is_event || AP_GetDoorShuffleMode() == kNO_DOORS) | 561 | return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id)); |
| 297 | ? kMaybe | 562 | } |
| 298 | : kNo; | 563 | |
| 299 | } else if (door_reachable == kMaybe) { | 564 | Decision IsDoorReachable(int door_id) { |
| 300 | return kMaybe; | 565 | if (options_.parent) { |
| 301 | } | 566 | return options_.parent->IsDoorReachable(door_id); |
| 302 | } | 567 | } |
| 303 | 568 | ||
| 304 | for (int panel_id : panel_obj.required_panels) { | 569 | if (door_decisions_.count(door_id)) { |
| 305 | if (!solveable_panels_.count(panel_id)) { | 570 | return door_decisions_.at(door_id); |
| 306 | return kMaybe; | ||
| 307 | } | ||
| 308 | } | 571 | } |
| 309 | 572 | ||
| 310 | if (AP_IsColorShuffle()) { | 573 | Decision result = IsDoorReachable_Helper(door_id); |
| 311 | for (LingoColor color : panel_obj.colors) { | 574 | if (result != kMaybe) { |
| 312 | if (!AP_HasItem(GD_GetItemIdForColor(color))) { | 575 | door_decisions_[door_id] = result; |
| 313 | return kNo; | 576 | } |
| 314 | } | 577 | |
| 315 | } | 578 | return result; |
| 579 | } | ||
| 580 | |||
| 581 | Decision IsPanelReachable(int panel_id) { | ||
| 582 | return AreRequirementsSatisfied(GetState().requirements.GetPanel(panel_id)); | ||
| 583 | } | ||
| 584 | |||
| 585 | Decision IsPaintingReachable(int painting_id) { | ||
| 586 | const PaintingExit& painting = GD_GetPaintingExit(painting_id); | ||
| 587 | if (painting.door) { | ||
| 588 | return IsDoorReachable(*painting.door); | ||
| 316 | } | 589 | } |
| 317 | 590 | ||
| 318 | return kYes; | 591 | return kYes; |
| @@ -337,7 +610,7 @@ class StateCalculator { | |||
| 337 | if (AP_IsSunwarpShuffle()) { | 610 | if (AP_IsSunwarpShuffle()) { |
| 338 | pilgrimage_pairs = std::vector<std::tuple<int, int>>(5); | 611 | pilgrimage_pairs = std::vector<std::tuple<int, int>>(5); |
| 339 | 612 | ||
| 340 | for (const auto& [start_index, mapping] : AP_GetSunwarpMapping()) { | 613 | for (const auto& [start_index, mapping] : sunwarp_mapping_) { |
| 341 | if (mapping.dots > 1) { | 614 | if (mapping.dots > 1) { |
| 342 | std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index; | 615 | std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index; |
| 343 | } | 616 | } |
| @@ -363,6 +636,8 @@ class StateCalculator { | |||
| 363 | } | 636 | } |
| 364 | } | 637 | } |
| 365 | 638 | ||
| 639 | pilgrimage_doable_ = true; | ||
| 640 | |||
| 366 | return kYes; | 641 | return kYes; |
| 367 | } | 642 | } |
| 368 | 643 | ||
| @@ -375,7 +650,8 @@ class StateCalculator { | |||
| 375 | !AP_DoesPilgrimageAllowRoofAccess()) { | 650 | !AP_DoesPilgrimageAllowRoofAccess()) { |
| 376 | return kNo; | 651 | return kNo; |
| 377 | } | 652 | } |
| 378 | if (room_exit.type == EntranceType::kPainting && | 653 | if ((room_exit.type == EntranceType::kPainting || |
| 654 | room_exit.type == EntranceType::kStaticPainting) && | ||
| 379 | !AP_DoesPilgrimageAllowPaintings()) { | 655 | !AP_DoesPilgrimageAllowPaintings()) { |
| 380 | return kNo; | 656 | return kNo; |
| 381 | } | 657 | } |
| @@ -401,16 +677,99 @@ class StateCalculator { | |||
| 401 | std::set<int> reachable_rooms_; | 677 | std::set<int> reachable_rooms_; |
| 402 | std::map<int, Decision> door_decisions_; | 678 | std::map<int, Decision> door_decisions_; |
| 403 | std::set<int> solveable_panels_; | 679 | std::set<int> solveable_panels_; |
| 680 | std::set<int> reachable_paintings_; | ||
| 681 | std::map<int, std::map<std::string, bool>> door_report_; | ||
| 682 | bool pilgrimage_doable_ = false; | ||
| 683 | |||
| 684 | std::map<int, std::list<int>> paths_; | ||
| 685 | |||
| 686 | std::map<std::string, std::string> painting_mapping_; | ||
| 687 | std::set<std::string> checked_paintings_; | ||
| 688 | std::map<int, SunwarpMapping> sunwarp_mapping_; | ||
| 404 | }; | 689 | }; |
| 405 | 690 | ||
| 406 | } // namespace | 691 | } // namespace |
| 407 | 692 | ||
| 693 | void ResetReachabilityRequirements() { | ||
| 694 | TrackerLog("Resetting tracker state..."); | ||
| 695 | |||
| 696 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
| 697 | GetState().requirements.Reset(); | ||
| 698 | GetState().reachable_doors.clear(); | ||
| 699 | GetState().solveable_panels.clear(); | ||
| 700 | |||
| 701 | if (AP_IsPostgameShuffle()) { | ||
| 702 | GetState().non_postgame_areas.clear(); | ||
| 703 | GetState().non_postgame_locations.clear(); | ||
| 704 | GetState().non_postgame_paintings.clear(); | ||
| 705 | } else { | ||
| 706 | StateCalculator postgame_calculator( | ||
| 707 | {.start = GD_GetRoomByName("Menu"), .postgame_detection = true}); | ||
| 708 | postgame_calculator.Calculate(); | ||
| 709 | |||
| 710 | std::set<int>& non_postgame_areas = GetState().non_postgame_areas; | ||
| 711 | non_postgame_areas.clear(); | ||
| 712 | |||
| 713 | std::set<int>& non_postgame_locations = GetState().non_postgame_locations; | ||
| 714 | non_postgame_locations.clear(); | ||
| 715 | |||
| 716 | const std::set<int>& reachable_rooms = | ||
| 717 | postgame_calculator.GetReachableRooms(); | ||
| 718 | const std::set<int>& solveable_panels = | ||
| 719 | postgame_calculator.GetSolveablePanels(); | ||
| 720 | |||
| 721 | for (const MapArea& map_area : GD_GetMapAreas()) { | ||
| 722 | bool area_reachable = false; | ||
| 723 | |||
| 724 | for (const Location& location_section : map_area.locations) { | ||
| 725 | bool reachable = reachable_rooms.count(location_section.room); | ||
| 726 | if (reachable) { | ||
| 727 | for (int panel_id : location_section.panels) { | ||
| 728 | reachable &= (solveable_panels.count(panel_id) == 1); | ||
| 729 | } | ||
| 730 | } | ||
| 731 | |||
| 732 | if (!reachable && IsLocationWinCondition(location_section)) { | ||
| 733 | reachable = true; | ||
| 734 | } | ||
| 735 | |||
| 736 | if (reachable) { | ||
| 737 | non_postgame_locations.insert(location_section.ap_location_id); | ||
| 738 | area_reachable = true; | ||
| 739 | } | ||
| 740 | } | ||
| 741 | |||
| 742 | for (int painting_id : map_area.paintings) { | ||
| 743 | if (postgame_calculator.GetReachablePaintings().count(painting_id)) { | ||
| 744 | area_reachable = true; | ||
| 745 | } | ||
| 746 | } | ||
| 747 | |||
| 748 | if (area_reachable) { | ||
| 749 | non_postgame_areas.insert(map_area.id); | ||
| 750 | } | ||
| 751 | } | ||
| 752 | |||
| 753 | GetState().non_postgame_paintings = | ||
| 754 | postgame_calculator.GetReachablePaintings(); | ||
| 755 | } | ||
| 756 | } | ||
| 757 | |||
| 408 | void RecalculateReachability() { | 758 | void RecalculateReachability() { |
| 759 | TrackerLog("Calculating reachability..."); | ||
| 760 | |||
| 761 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
| 762 | |||
| 763 | // Receiving items and checking paintings should never remove access to doors | ||
| 764 | // or panels, so we can preload any doors and panels we already know are | ||
| 765 | // accessible from previous runs, in order to reduce the work. | ||
| 409 | StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); | 766 | StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); |
| 767 | state_calculator.PreloadDoors(GetState().reachable_doors); | ||
| 768 | state_calculator.PreloadPanels(GetState().solveable_panels); | ||
| 410 | state_calculator.Calculate(); | 769 | state_calculator.Calculate(); |
| 411 | 770 | ||
| 412 | const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); | 771 | const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); |
| 413 | const std::set<int>& solveable_panels = state_calculator.GetSolveablePanels(); | 772 | std::set<int> solveable_panels = state_calculator.GetSolveablePanels(); |
| 414 | 773 | ||
| 415 | std::map<int, bool> new_reachability; | 774 | std::map<int, bool> new_reachability; |
| 416 | for (const MapArea& map_area : GD_GetMapAreas()) { | 775 | for (const MapArea& map_area : GD_GetMapAreas()) { |
| @@ -435,11 +794,16 @@ void RecalculateReachability() { | |||
| 435 | } | 794 | } |
| 436 | } | 795 | } |
| 437 | 796 | ||
| 438 | { | 797 | std::set<int> reachable_paintings = state_calculator.GetReachablePaintings(); |
| 439 | std::lock_guard reachability_guard(GetState().reachability_mutex); | 798 | std::map<int, std::map<std::string, bool>> door_reports = |
| 440 | std::swap(GetState().reachability, new_reachability); | 799 | state_calculator.GetDoorReports(); |
| 441 | std::swap(GetState().reachable_doors, new_reachable_doors); | 800 | |
| 442 | } | 801 | std::swap(GetState().reachability, new_reachability); |
| 802 | std::swap(GetState().reachable_doors, new_reachable_doors); | ||
| 803 | std::swap(GetState().solveable_panels, solveable_panels); | ||
| 804 | std::swap(GetState().reachable_paintings, reachable_paintings); | ||
| 805 | std::swap(GetState().door_reports, door_reports); | ||
| 806 | GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable(); | ||
| 443 | } | 807 | } |
| 444 | 808 | ||
| 445 | bool IsLocationReachable(int location_id) { | 809 | bool IsLocationReachable(int location_id) { |
| @@ -457,3 +821,51 @@ bool IsDoorOpen(int door_id) { | |||
| 457 | 821 | ||
| 458 | return GetState().reachable_doors.count(door_id); | 822 | return GetState().reachable_doors.count(door_id); |
| 459 | } | 823 | } |
| 824 | |||
| 825 | bool IsPaintingReachable(int painting_id) { | ||
| 826 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
| 827 | |||
| 828 | return GetState().reachable_paintings.count(painting_id); | ||
| 829 | } | ||
| 830 | |||
| 831 | const std::map<std::string, bool>& GetDoorRequirements(int door_id) { | ||
| 832 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
| 833 | |||
| 834 | return GetState().door_reports[door_id]; | ||
| 835 | } | ||
| 836 | |||
| 837 | bool IsPilgrimageDoable() { | ||
| 838 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
| 839 | |||
| 840 | return GetState().pilgrimage_doable; | ||
| 841 | } | ||
| 842 | |||
| 843 | bool IsAreaPostgame(int area_id) { | ||
| 844 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
| 845 | |||
| 846 | if (GetState().non_postgame_areas.empty()) { | ||
| 847 | return false; | ||
| 848 | } else { | ||
| 849 | return !GetState().non_postgame_areas.count(area_id); | ||
| 850 | } | ||
| 851 | } | ||
| 852 | |||
| 853 | bool IsLocationPostgame(int location_id) { | ||
| 854 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
| 855 | |||
| 856 | if (GetState().non_postgame_locations.empty()) { | ||
| 857 | return false; | ||
| 858 | } else { | ||
| 859 | return !GetState().non_postgame_locations.count(location_id); | ||
| 860 | } | ||
| 861 | } | ||
| 862 | |||
| 863 | bool IsPaintingPostgame(int painting_id) { | ||
| 864 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
| 865 | |||
| 866 | if (GetState().non_postgame_paintings.empty()) { | ||
| 867 | return false; | ||
| 868 | } else { | ||
| 869 | return !GetState().non_postgame_paintings.count(painting_id); | ||
| 870 | } | ||
| 871 | } | ||
| diff --git a/src/tracker_state.h b/src/tracker_state.h index 119b3b5..8f1002f 100644 --- a/src/tracker_state.h +++ b/src/tracker_state.h | |||
| @@ -1,10 +1,27 @@ | |||
| 1 | #ifndef TRACKER_STATE_H_8639BC90 | 1 | #ifndef TRACKER_STATE_H_8639BC90 |
| 2 | #define TRACKER_STATE_H_8639BC90 | 2 | #define TRACKER_STATE_H_8639BC90 |
| 3 | 3 | ||
| 4 | #include <map> | ||
| 5 | #include <string> | ||
| 6 | |||
| 7 | void ResetReachabilityRequirements(); | ||
| 8 | |||
| 4 | void RecalculateReachability(); | 9 | void RecalculateReachability(); |
| 5 | 10 | ||
| 6 | bool IsLocationReachable(int location_id); | 11 | bool IsLocationReachable(int location_id); |
| 7 | 12 | ||
| 8 | bool IsDoorOpen(int door_id); | 13 | bool IsDoorOpen(int door_id); |
| 9 | 14 | ||
| 15 | bool IsPaintingReachable(int painting_id); | ||
| 16 | |||
| 17 | const std::map<std::string, bool>& GetDoorRequirements(int door_id); | ||
| 18 | |||
| 19 | bool IsPilgrimageDoable(); | ||
| 20 | |||
| 21 | bool IsAreaPostgame(int area_id); | ||
| 22 | |||
| 23 | bool IsLocationPostgame(int location_id); | ||
| 24 | |||
| 25 | bool IsPaintingPostgame(int painting_id); | ||
| 26 | |||
| 10 | #endif /* end of include guard: TRACKER_STATE_H_8639BC90 */ | 27 | #endif /* end of include guard: TRACKER_STATE_H_8639BC90 */ |
| diff --git a/src/updater.cpp b/src/updater.cpp new file mode 100644 index 0000000..2b05daf --- /dev/null +++ b/src/updater.cpp | |||
| @@ -0,0 +1,309 @@ | |||
| 1 | #include "updater.h" | ||
| 2 | |||
| 3 | #include <fmt/core.h> | ||
| 4 | #include <openssl/evp.h> | ||
| 5 | #include <openssl/sha.h> | ||
| 6 | #include <wx/evtloop.h> | ||
| 7 | #include <wx/progdlg.h> | ||
| 8 | #include <wx/webrequest.h> | ||
| 9 | #include <wx/wfstream.h> | ||
| 10 | #include <wx/zipstrm.h> | ||
| 11 | #include <yaml-cpp/yaml.h> | ||
| 12 | |||
| 13 | #include <cstdio> | ||
| 14 | #include <deque> | ||
| 15 | #include <filesystem> | ||
| 16 | #include <fstream> | ||
| 17 | |||
| 18 | #include "global.h" | ||
| 19 | #include "logger.h" | ||
| 20 | #include "version.h" | ||
| 21 | |||
| 22 | constexpr const char* kVersionFileUrl = | ||
| 23 | "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml"; | ||
| 24 | constexpr const char* kChangelogUrl = | ||
| 25 | "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md"; | ||
| 26 | |||
| 27 | namespace { | ||
| 28 | |||
| 29 | std::string CalculateStringSha256(const wxString& data) { | ||
| 30 | unsigned char hash[SHA256_DIGEST_LENGTH]; | ||
| 31 | EVP_MD_CTX* sha256 = EVP_MD_CTX_new(); | ||
| 32 | EVP_DigestInit(sha256, EVP_sha256()); | ||
| 33 | EVP_DigestUpdate(sha256, data.c_str(), data.length()); | ||
| 34 | EVP_DigestFinal_ex(sha256, hash, nullptr); | ||
| 35 | EVP_MD_CTX_free(sha256); | ||
| 36 | |||
| 37 | char output[65] = {0}; | ||
| 38 | for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { | ||
| 39 | snprintf(output + (i * 2), 3, "%02x", hash[i]); | ||
| 40 | } | ||
| 41 | |||
| 42 | return std::string(output); | ||
| 43 | } | ||
| 44 | |||
| 45 | } // namespace | ||
| 46 | |||
| 47 | Updater::Updater(wxFrame* parent) : parent_(parent) { | ||
| 48 | Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this); | ||
| 49 | } | ||
| 50 | |||
| 51 | void Updater::Cleanup() { | ||
| 52 | std::filesystem::path oldDir = GetExecutableDirectory() / "old"; | ||
| 53 | if (std::filesystem::is_directory(oldDir)) { | ||
| 54 | std::filesystem::remove_all(oldDir); | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | void Updater::CheckForUpdates(bool invisible) { | ||
| 59 | wxWebRequest versionRequest = | ||
| 60 | wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl); | ||
| 61 | |||
| 62 | if (invisible) { | ||
| 63 | update_state_ = UpdateState::GetVersionInvisible; | ||
| 64 | |||
| 65 | versionRequest.Start(); | ||
| 66 | } else { | ||
| 67 | update_state_ = UpdateState::GetVersionManual; | ||
| 68 | |||
| 69 | if (DownloadWithProgress(versionRequest)) { | ||
| 70 | if (versionRequest.GetState() == wxWebRequest::State_Failed) { | ||
| 71 | wxMessageBox("Could not check for updates.", "Error", | ||
| 72 | wxOK | wxICON_ERROR); | ||
| 73 | } else if (versionRequest.GetState() == wxWebRequest::State_Completed) { | ||
| 74 | ProcessVersionFile( | ||
| 75 | versionRequest.GetResponse().AsString().utf8_string()); | ||
| 76 | } | ||
| 77 | } | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | void Updater::OnWebRequestState(wxWebRequestEvent& evt) { | ||
| 82 | if (update_state_ == UpdateState::GetVersionInvisible) { | ||
| 83 | if (evt.GetState() == wxWebRequest::State_Completed) { | ||
| 84 | ProcessVersionFile(evt.GetResponse().AsString().utf8_string()); | ||
| 85 | } else if (evt.GetState() == wxWebRequest::State_Failed) { | ||
| 86 | parent_->SetStatusText("Could not check for updates."); | ||
| 87 | } | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | void Updater::ProcessVersionFile(std::string data) { | ||
| 92 | try { | ||
| 93 | YAML::Node versionInfo = YAML::Load(data); | ||
| 94 | Version latestVersion(versionInfo["version"].as<std::string>()); | ||
| 95 | |||
| 96 | if (kTrackerVersion < latestVersion) { | ||
| 97 | if (versionInfo["packages"]) { | ||
| 98 | std::string platformIdentifier; | ||
| 99 | |||
| 100 | if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) { | ||
| 101 | platformIdentifier = "win64"; | ||
| 102 | } | ||
| 103 | |||
| 104 | if (!platformIdentifier.empty() && | ||
| 105 | versionInfo["packages"][platformIdentifier]) { | ||
| 106 | wxMessageDialog dialog( | ||
| 107 | nullptr, | ||
| 108 | fmt::format("There is a newer version of Lingo AP Tracker " | ||
| 109 | "available. You have {}, and the latest version is " | ||
| 110 | "{}. Would you like to update?", | ||
| 111 | kTrackerVersion.ToString(), latestVersion.ToString()), | ||
| 112 | "Update available", wxYES_NO | wxCANCEL); | ||
| 113 | dialog.SetYesNoLabels("Install update", "Open changelog"); | ||
| 114 | |||
| 115 | int dlgResult = dialog.ShowModal(); | ||
| 116 | if (dlgResult == wxID_YES) { | ||
| 117 | const YAML::Node& packageInfo = | ||
| 118 | versionInfo["packages"][platformIdentifier]; | ||
| 119 | std::string packageUrl = packageInfo["url"].as<std::string>(); | ||
| 120 | std::string packageChecksum = | ||
| 121 | packageInfo["checksum"].as<std::string>(); | ||
| 122 | |||
| 123 | std::vector<std::filesystem::path> packageFiles; | ||
| 124 | if (packageInfo["files"]) { | ||
| 125 | for (const YAML::Node& filename : packageInfo["files"]) { | ||
| 126 | packageFiles.push_back(filename.as<std::string>()); | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | std::vector<std::filesystem::path> deletedFiles; | ||
| 131 | if (packageInfo["deleted_files"]) { | ||
| 132 | for (const YAML::Node& filename : packageInfo["deleted_files"]) { | ||
| 133 | deletedFiles.push_back(filename.as<std::string>()); | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 | InstallUpdate(packageUrl, packageChecksum, packageFiles, | ||
| 138 | deletedFiles); | ||
| 139 | } else if (dlgResult == wxID_NO) { | ||
| 140 | wxLaunchDefaultBrowser(kChangelogUrl); | ||
| 141 | } | ||
| 142 | |||
| 143 | return; | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | if (wxMessageBox( | ||
| 148 | fmt::format("There is a newer version of Lingo AP Tracker " | ||
| 149 | "available. You have {}, and the latest version is " | ||
| 150 | "{}. Would you like to update?", | ||
| 151 | kTrackerVersion.ToString(), latestVersion.ToString()), | ||
| 152 | "Update available", wxYES_NO) == wxYES) { | ||
| 153 | wxLaunchDefaultBrowser(kChangelogUrl); | ||
| 154 | } | ||
| 155 | } else if (update_state_ == UpdateState::GetVersionManual) { | ||
| 156 | wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK); | ||
| 157 | } | ||
| 158 | } catch (const std::exception& ex) { | ||
| 159 | wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR); | ||
| 160 | } | ||
| 161 | } | ||
| 162 | |||
| 163 | void Updater::InstallUpdate(std::string url, std::string checksum, | ||
| 164 | std::vector<std::filesystem::path> files, | ||
| 165 | std::vector<std::filesystem::path> deletedFiles) { | ||
| 166 | update_state_ = UpdateState::GetPackage; | ||
| 167 | |||
| 168 | wxWebRequest packageRequest = | ||
| 169 | wxWebSession::GetDefault().CreateRequest(this, url); | ||
| 170 | |||
| 171 | if (!DownloadWithProgress(packageRequest)) { | ||
| 172 | return; | ||
| 173 | } | ||
| 174 | |||
| 175 | bool download_issue = false; | ||
| 176 | |||
| 177 | wxFileName package_path; | ||
| 178 | package_path.AssignTempFileName(""); | ||
| 179 | |||
| 180 | if (!package_path.IsOk()) { | ||
| 181 | download_issue = true; | ||
| 182 | } else { | ||
| 183 | wxFileOutputStream writeOut(package_path.GetFullPath()); | ||
| 184 | wxString fileData = packageRequest.GetResponse().AsString(); | ||
| 185 | writeOut.WriteAll(fileData.c_str(), fileData.length()); | ||
| 186 | |||
| 187 | std::string downloadedChecksum = CalculateStringSha256(fileData); | ||
| 188 | if (downloadedChecksum != checksum) { | ||
| 189 | download_issue = true; | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | if (download_issue) { | ||
| 194 | if (wxMessageBox("There was an issue downloading the update. Would you " | ||
| 195 | "like to manually download it instead?", | ||
| 196 | "Error", wxYES_NO | wxICON_ERROR) == wxID_YES) { | ||
| 197 | wxLaunchDefaultBrowser(kChangelogUrl); | ||
| 198 | } | ||
| 199 | return; | ||
| 200 | } | ||
| 201 | |||
| 202 | std::filesystem::path newArea = GetExecutableDirectory(); | ||
| 203 | std::filesystem::path oldArea = newArea / "old"; | ||
| 204 | std::set<std::filesystem::path> folders; | ||
| 205 | std::set<std::filesystem::path> filesToMove; | ||
| 206 | for (const std::filesystem::path& existingFile : files) { | ||
| 207 | std::filesystem::path movedPath = oldArea / existingFile; | ||
| 208 | std::filesystem::path movedDir = movedPath; | ||
| 209 | movedDir.remove_filename(); | ||
| 210 | folders.insert(movedDir); | ||
| 211 | filesToMove.insert(existingFile); | ||
| 212 | } | ||
| 213 | for (const std::filesystem::path& existingFile : deletedFiles) { | ||
| 214 | std::filesystem::path movedPath = oldArea / existingFile; | ||
| 215 | std::filesystem::path movedDir = movedPath; | ||
| 216 | movedDir.remove_filename(); | ||
| 217 | folders.insert(movedDir); | ||
| 218 | } | ||
| 219 | |||
| 220 | for (const std::filesystem::path& newFolder : folders) { | ||
| 221 | TrackerLog(fmt::format("Creating directory {}", newFolder.string())); | ||
| 222 | |||
| 223 | std::filesystem::create_directories(newFolder); | ||
| 224 | } | ||
| 225 | |||
| 226 | for (const std::filesystem::path& existingFile : files) { | ||
| 227 | std::filesystem::path existingPath = newArea / existingFile; | ||
| 228 | |||
| 229 | if (std::filesystem::is_regular_file(existingPath)) { | ||
| 230 | std::filesystem::path movedPath = oldArea / existingFile; | ||
| 231 | |||
| 232 | TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(), | ||
| 233 | movedPath.string())); | ||
| 234 | |||
| 235 | std::filesystem::rename(existingPath, movedPath); | ||
| 236 | } | ||
| 237 | } | ||
| 238 | for (const std::filesystem::path& existingFile : deletedFiles) { | ||
| 239 | std::filesystem::path existingPath = newArea / existingFile; | ||
| 240 | |||
| 241 | if (std::filesystem::is_regular_file(existingPath)) { | ||
| 242 | std::filesystem::path movedPath = oldArea / existingFile; | ||
| 243 | |||
| 244 | TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(), | ||
| 245 | movedPath.string())); | ||
| 246 | |||
| 247 | std::filesystem::rename(existingPath, movedPath); | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | wxFileInputStream fileInputStream(package_path.GetFullPath()); | ||
| 252 | wxZipInputStream zipStream(fileInputStream); | ||
| 253 | std::unique_ptr<wxZipEntry> zipEntry; | ||
| 254 | while ((zipEntry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) != | ||
| 255 | nullptr) { | ||
| 256 | if (zipEntry->IsDir()) { | ||
| 257 | continue; | ||
| 258 | } | ||
| 259 | |||
| 260 | std::filesystem::path archivePath = zipEntry->GetName().utf8_string(); | ||
| 261 | |||
| 262 | TrackerLog(fmt::format("Found {} in archive", archivePath.string())); | ||
| 263 | |||
| 264 | // Cut off the root folder name | ||
| 265 | std::filesystem::path subPath; | ||
| 266 | for (auto it = std::next(archivePath.begin()); it != archivePath.end(); | ||
| 267 | it++) { | ||
| 268 | subPath /= *it; | ||
| 269 | } | ||
| 270 | |||
| 271 | std::filesystem::path pastePath = newArea / subPath; | ||
| 272 | |||
| 273 | wxFileOutputStream fileOutput(pastePath.string()); | ||
| 274 | zipStream.Read(fileOutput); | ||
| 275 | } | ||
| 276 | |||
| 277 | if (wxMessageBox( | ||
| 278 | "Update installed! The tracker must be restarted for the changes to take " | ||
| 279 | "effect. Do you want to close the tracker?", | ||
| 280 | "Update installed", wxYES_NO) == wxYES) { | ||
| 281 | wxExit(); | ||
| 282 | } | ||
| 283 | } | ||
| 284 | |||
| 285 | bool Updater::DownloadWithProgress(wxWebRequest& request) { | ||
| 286 | request.Start(); | ||
| 287 | |||
| 288 | wxProgressDialog dialog("Checking for updates...", "Checking for updates...", | ||
| 289 | 100, nullptr, | ||
| 290 | wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT | | ||
| 291 | wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME); | ||
| 292 | while (request.GetState() != wxWebRequest::State_Completed && | ||
| 293 | request.GetState() != wxWebRequest::State_Failed) { | ||
| 294 | if (request.GetBytesExpectedToReceive() == -1) { | ||
| 295 | if (!dialog.Pulse()) { | ||
| 296 | request.Cancel(); | ||
| 297 | return false; | ||
| 298 | } | ||
| 299 | } else { | ||
| 300 | dialog.SetRange(request.GetBytesExpectedToReceive()); | ||
| 301 | if (!dialog.Update(request.GetBytesReceived())) { | ||
| 302 | request.Cancel(); | ||
| 303 | return false; | ||
| 304 | } | ||
| 305 | } | ||
| 306 | } | ||
| 307 | |||
| 308 | return true; | ||
| 309 | } | ||
| diff --git a/src/updater.h b/src/updater.h new file mode 100644 index 0000000..c604a49 --- /dev/null +++ b/src/updater.h | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | #ifndef UPDATER_H_809E7381 | ||
| 2 | #define UPDATER_H_809E7381 | ||
| 3 | |||
| 4 | #include <filesystem> | ||
| 5 | #include <set> | ||
| 6 | #include <string> | ||
| 7 | |||
| 8 | #include <wx/wxprec.h> | ||
| 9 | |||
| 10 | #ifndef WX_PRECOMP | ||
| 11 | #include <wx/wx.h> | ||
| 12 | #endif | ||
| 13 | |||
| 14 | class wxWebRequest; | ||
| 15 | class wxWebRequestEvent; | ||
| 16 | |||
| 17 | class Updater : public wxEvtHandler { | ||
| 18 | public: | ||
| 19 | explicit Updater(wxFrame* parent); | ||
| 20 | |||
| 21 | void Cleanup(); | ||
| 22 | |||
| 23 | void CheckForUpdates(bool invisible); | ||
| 24 | |||
| 25 | private: | ||
| 26 | enum class UpdateState { | ||
| 27 | GetVersionInvisible, | ||
| 28 | GetVersionManual, | ||
| 29 | GetPackage, | ||
| 30 | }; | ||
| 31 | |||
| 32 | void OnWebRequestState(wxWebRequestEvent& event); | ||
| 33 | |||
| 34 | void ProcessVersionFile(std::string data); | ||
| 35 | |||
| 36 | void InstallUpdate(std::string url, std::string checksum, | ||
| 37 | std::vector<std::filesystem::path> files, | ||
| 38 | std::vector<std::filesystem::path> deletedFiles); | ||
| 39 | |||
| 40 | bool DownloadWithProgress(wxWebRequest& request); | ||
| 41 | |||
| 42 | wxFrame* parent_; | ||
| 43 | UpdateState update_state_ = UpdateState::GetVersionInvisible; | ||
| 44 | }; | ||
| 45 | |||
| 46 | #endif /* end of include guard: UPDATER_H_809E7381 */ | ||
| diff --git a/src/version.cpp b/src/version.cpp new file mode 100644 index 0000000..3b4d5f3 --- /dev/null +++ b/src/version.cpp | |||
| @@ -0,0 +1,5 @@ | |||
| 1 | #include "version.h" | ||
| 2 | |||
| 3 | std::ostream& operator<<(std::ostream& out, const Version& ver) { | ||
| 4 | return out << "v" << ver.major << "." << ver.minor << "." << ver.revision; | ||
| 5 | } | ||
| diff --git a/src/version.h b/src/version.h index 36bd8c1..3439fda 100644 --- a/src/version.h +++ b/src/version.h | |||
| @@ -1,9 +1,10 @@ | |||
| 1 | #ifndef VERSION_H_C757E53C | 1 | #ifndef VERSION_H_C757E53C |
| 2 | #define VERSION_H_C757E53C | 2 | #define VERSION_H_C757E53C |
| 3 | 3 | ||
| 4 | #include <sstream> | ||
| 5 | #include <regex> | 4 | #include <regex> |
| 6 | 5 | ||
| 6 | #include <fmt/core.h> | ||
| 7 | |||
| 7 | struct Version { | 8 | struct Version { |
| 8 | int major = 0; | 9 | int major = 0; |
| 9 | int minor = 0; | 10 | int minor = 0; |
| @@ -24,9 +25,7 @@ struct Version { | |||
| 24 | } | 25 | } |
| 25 | 26 | ||
| 26 | std::string ToString() const { | 27 | std::string ToString() const { |
| 27 | std::ostringstream output; | 28 | return fmt::format("v{}.{}.{}", major, minor, revision); |
| 28 | output << "v" << major << "." << minor << "." << revision; | ||
| 29 | return output.str(); | ||
| 30 | } | 29 | } |
| 31 | 30 | ||
| 32 | bool operator<(const Version& rhs) const { | 31 | bool operator<(const Version& rhs) const { |
| @@ -37,6 +36,6 @@ struct Version { | |||
| 37 | } | 36 | } |
| 38 | }; | 37 | }; |
| 39 | 38 | ||
| 40 | constexpr const Version kTrackerVersion = Version(0, 9, 0); | 39 | constexpr const Version kTrackerVersion = Version(2, 0, 2); |
| 41 | 40 | ||
| 42 | #endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file | 41 | #endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file |
| diff --git a/src/windows.rc b/src/windows.rc new file mode 100644 index 0000000..8ba30ed --- /dev/null +++ b/src/windows.rc | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | #define wxUSE_RC_MANIFEST 1 | ||
| 2 | #define wxUSE_DPI_AWARE_MANIFEST 2 | ||
| 3 | #include "wx/msw/wx.rc" | ||
