about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ap_state.cpp102
-rw-r--r--src/game_data.cpp80
-rw-r--r--src/game_data.h4
-rw-r--r--src/logger.cpp32
-rw-r--r--src/logger.h8
-rw-r--r--src/main.cpp12
-rw-r--r--src/subway_map.cpp77
-rw-r--r--src/tracker_frame.cpp2
-rw-r--r--src/tracker_state.cpp48
-rw-r--r--src/version.h9
10 files changed, 241 insertions, 133 deletions
diff --git a/src/ap_state.cpp b/src/ap_state.cpp index a7565cf..d501e81 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp
@@ -4,6 +4,7 @@
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>
@@ -21,6 +22,7 @@
21#include <tuple> 22#include <tuple>
22 23
23#include "game_data.h" 24#include "game_data.h"
25#include "logger.h"
24#include "tracker_frame.h" 26#include "tracker_frame.h"
25#include "tracker_state.h" 27#include "tracker_state.h"
26 28
@@ -76,7 +78,7 @@ struct APState {
76 78
77 void Connect(std::string server, std::string player, std::string password) { 79 void Connect(std::string server, std::string player, std::string password) {
78 if (!initialized) { 80 if (!initialized) {
79 wxLogVerbose("Initializing APState..."); 81 TrackerLog("Initializing APState...");
80 82
81 std::thread([this]() { 83 std::thread([this]() {
82 for (;;) { 84 for (;;) {
@@ -92,14 +94,14 @@ struct APState {
92 }).detach(); 94 }).detach();
93 95
94 for (int panel_id : GD_GetAchievementPanels()) { 96 for (int panel_id : GD_GetAchievementPanels()) {
95 tracked_data_storage_keys.push_back( 97 tracked_data_storage_keys.push_back(fmt::format(
96 "Achievement|" + GD_GetPanel(panel_id).achievement_name); 98 "Achievement|{}", GD_GetPanel(panel_id).achievement_name));
97 } 99 }
98 100
99 for (const MapArea& map_area : GD_GetMapAreas()) { 101 for (const MapArea& map_area : GD_GetMapAreas()) {
100 for (const Location& location : map_area.locations) { 102 for (const Location& location : map_area.locations) {
101 tracked_data_storage_keys.push_back( 103 tracked_data_storage_keys.push_back(
102 "Hunt|" + std::to_string(location.ap_location_id)); 104 fmt::format("Hunt|{}", location.ap_location_id));
103 } 105 }
104 } 106 }
105 107
@@ -110,10 +112,10 @@ struct APState {
110 } 112 }
111 113
112 tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); 114 tracker_frame->SetStatusMessage("Connecting to Archipelago server....");
113 wxLogStatus("Connecting to Archipelago server (%s)...", server); 115 TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server));
114 116
115 { 117 {
116 wxLogVerbose("Destroying old AP client..."); 118 TrackerLog("Destroying old AP client...");
117 119
118 std::lock_guard client_guard(client_mutex); 120 std::lock_guard client_guard(client_mutex);
119 121
@@ -160,10 +162,10 @@ struct APState {
160 apclient->set_room_info_handler([this, player, password]() { 162 apclient->set_room_info_handler([this, player, password]() {
161 inventory.clear(); 163 inventory.clear();
162 164
163 wxLogStatus("Connected to Archipelago server. Authenticating as %s %s", 165 TrackerLog(fmt::format(
164 player, 166 "Connected to Archipelago server. Authenticating as {} {}", player,
165 (password.empty() ? "without password" 167 (password.empty() ? "without password"
166 : "with password " + password)); 168 : "with password " + password)));
167 tracker_frame->SetStatusMessage( 169 tracker_frame->SetStatusMessage(
168 "Connected to Archipelago server. Authenticating..."); 170 "Connected to Archipelago server. Authenticating...");
169 171
@@ -175,7 +177,7 @@ struct APState {
175 [this](const std::list<int64_t>& locations) { 177 [this](const std::list<int64_t>& locations) {
176 for (const int64_t location_id : locations) { 178 for (const int64_t location_id : locations) {
177 checked_locations.insert(location_id); 179 checked_locations.insert(location_id);
178 wxLogVerbose("Location: %lld", location_id); 180 TrackerLog(fmt::format("Location: {}", location_id));
179 } 181 }
180 182
181 RefreshTracker(false); 183 RefreshTracker(false);
@@ -184,14 +186,14 @@ struct APState {
184 apclient->set_slot_disconnected_handler([this]() { 186 apclient->set_slot_disconnected_handler([this]() {
185 tracker_frame->SetStatusMessage( 187 tracker_frame->SetStatusMessage(
186 "Disconnected from Archipelago. Attempting to reconnect..."); 188 "Disconnected from Archipelago. Attempting to reconnect...");
187 wxLogStatus( 189 TrackerLog(
188 "Slot disconnected from Archipelago. Attempting to reconnect..."); 190 "Slot disconnected from Archipelago. Attempting to reconnect...");
189 }); 191 });
190 192
191 apclient->set_socket_disconnected_handler([this]() { 193 apclient->set_socket_disconnected_handler([this]() {
192 tracker_frame->SetStatusMessage( 194 tracker_frame->SetStatusMessage(
193 "Disconnected from Archipelago. Attempting to reconnect..."); 195 "Disconnected from Archipelago. Attempting to reconnect...");
194 wxLogStatus( 196 TrackerLog(
195 "Socket disconnected from Archipelago. Attempting to reconnect..."); 197 "Socket disconnected from Archipelago. Attempting to reconnect...");
196 }); 198 });
197 199
@@ -199,7 +201,7 @@ struct APState {
199 [this](const std::list<APClient::NetworkItem>& items) { 201 [this](const std::list<APClient::NetworkItem>& items) {
200 for (const APClient::NetworkItem& item : items) { 202 for (const APClient::NetworkItem& item : items) {
201 inventory[item.item]++; 203 inventory[item.item]++;
202 wxLogVerbose("Item: %lld", item.item); 204 TrackerLog(fmt::format("Item: {}", item.item));
203 } 205 }
204 206
205 RefreshTracker(false); 207 RefreshTracker(false);
@@ -221,13 +223,15 @@ struct APState {
221 RefreshTracker(false); 223 RefreshTracker(false);
222 }); 224 });
223 225
224 apclient->set_slot_connected_handler([this, &connection_mutex]( 226 apclient->set_slot_connected_handler([this, player, server,
227 &connection_mutex](
225 const nlohmann::json& slot_data) { 228 const nlohmann::json& slot_data) {
226 tracker_frame->SetStatusMessage("Connected to Archipelago!"); 229 tracker_frame->SetStatusMessage(
227 wxLogStatus("Connected to Archipelago!"); 230 fmt::format("Connected to Archipelago! ({}@{})", player, server));
231 TrackerLog("Connected to Archipelago!");
228 232
229 data_storage_prefix = 233 data_storage_prefix =
230 "Lingo_" + std::to_string(apclient->get_player_number()) + "_"; 234 fmt::format("Lingo_{}_", apclient->get_player_number());
231 door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>(); 235 door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
232 if (slot_data.contains("group_doors")) { 236 if (slot_data.contains("group_doors")) {
233 group_doors = slot_data.contains("group_doors") && 237 group_doors = slot_data.contains("group_doors") &&
@@ -290,18 +294,18 @@ struct APState {
290 corrected_keys.push_back(data_storage_prefix + key); 294 corrected_keys.push_back(data_storage_prefix + key);
291 } 295 }
292 296
293 { 297 victory_data_storage_key =
294 std::ostringstream vdsks; 298 fmt::format("_read_client_status_{}_{}", apclient->get_team_number(),
295 vdsks << "_read_client_status_" << apclient->get_team_number() << "_" 299 apclient->get_player_number());
296 << apclient->get_player_number();
297 victory_data_storage_key = vdsks.str();
298 }
299 300
300 corrected_keys.push_back(victory_data_storage_key); 301 corrected_keys.push_back(victory_data_storage_key);
301 302
302 apclient->Get(corrected_keys); 303 apclient->Get(corrected_keys);
303 apclient->SetNotify(corrected_keys); 304 apclient->SetNotify(corrected_keys);
304 305
306 ResetReachabilityRequirements();
307 RefreshTracker(true);
308
305 { 309 {
306 std::lock_guard connection_lock(connection_mutex); 310 std::lock_guard connection_lock(connection_mutex);
307 if (!has_connection_result) { 311 if (!has_connection_result) {
@@ -346,7 +350,7 @@ struct APState {
346 } 350 }
347 351
348 std::string full_message = hatkirby::implode(error_messages, " "); 352 std::string full_message = hatkirby::implode(error_messages, " ");
349 wxLogError(wxString(full_message)); 353 TrackerLog(full_message);
350 354
351 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR); 355 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
352 }); 356 });
@@ -368,7 +372,7 @@ struct APState {
368 DestroyClient(); 372 DestroyClient();
369 373
370 tracker_frame->SetStatusMessage("Disconnected from Archipelago."); 374 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
371 wxLogStatus("Timeout while connecting to Archipelago server."); 375 TrackerLog("Timeout while connecting to Archipelago server.");
372 wxMessageBox("Timeout while connecting to Archipelago server.", 376 wxMessageBox("Timeout while connecting to Archipelago server.",
373 "Connection failed", wxOK | wxICON_ERROR); 377 "Connection failed", wxOK | wxICON_ERROR);
374 378
@@ -387,9 +391,6 @@ struct APState {
387 } 391 }
388 392
389 if (connected) { 393 if (connected) {
390 ResetReachabilityRequirements();
391 RefreshTracker(true);
392 } else {
393 client_active = false; 394 client_active = false;
394 } 395 }
395 } 396 }
@@ -397,11 +398,12 @@ struct APState {
397 void HandleDataStorage(const std::string& key, const nlohmann::json& value) { 398 void HandleDataStorage(const std::string& key, const nlohmann::json& value) {
398 if (value.is_boolean()) { 399 if (value.is_boolean()) {
399 data_storage[key] = value.get<bool>(); 400 data_storage[key] = value.get<bool>();
400 wxLogVerbose("Data storage %s retrieved as %s", key, 401 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
401 (value.get<bool>() ? "true" : "false")); 402 (value.get<bool>() ? "true" : "false")));
402 } else if (value.is_number()) { 403 } else if (value.is_number()) {
403 data_storage[key] = value.get<int>(); 404 data_storage[key] = value.get<int>();
404 wxLogVerbose("Data storage %s retrieved as %d", key, value.get<int>()); 405 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
406 value.get<int>()));
405 } else if (value.is_object()) { 407 } else if (value.is_object()) {
406 if (key.ends_with("PlayerPos")) { 408 if (key.ends_with("PlayerPos")) {
407 auto map_value = value.get<std::map<std::string, int>>(); 409 auto map_value = value.get<std::map<std::string, int>>();
@@ -410,7 +412,7 @@ struct APState {
410 data_storage[key] = value.get<std::map<std::string, int>>(); 412 data_storage[key] = value.get<std::map<std::string, int>>();
411 } 413 }
412 414
413 wxLogVerbose("Data storage %s retrieved as dictionary", key); 415 TrackerLog(fmt::format("Data storage {} retrieved as dictionary", key));
414 } else if (value.is_null()) { 416 } else if (value.is_null()) {
415 if (key.ends_with("PlayerPos")) { 417 if (key.ends_with("PlayerPos")) {
416 player_pos = std::nullopt; 418 player_pos = std::nullopt;
@@ -418,7 +420,7 @@ struct APState {
418 data_storage.erase(key); 420 data_storage.erase(key);
419 } 421 }
420 422
421 wxLogVerbose("Data storage %s retrieved as null", key); 423 TrackerLog(fmt::format("Data storage {} retrieved as null", key));
422 } else if (value.is_array()) { 424 } else if (value.is_array()) {
423 auto list_value = value.get<std::vector<std::string>>(); 425 auto list_value = value.get<std::vector<std::string>>();
424 426
@@ -429,8 +431,8 @@ struct APState {
429 data_storage[key] = list_value; 431 data_storage[key] = list_value;
430 } 432 }
431 433
432 wxLogVerbose("Data storage %s retrieved as list: [%s]", key, 434 TrackerLog(fmt::format("Data storage {} retrieved as list: [{}]", key,
433 hatkirby::implode(list_value, ", ")); 435 hatkirby::implode(list_value, ", ")));
434 } 436 }
435 } 437 }
436 438
@@ -440,7 +442,7 @@ struct APState {
440 442
441 bool HasCheckedHuntPanel(int location_id) { 443 bool HasCheckedHuntPanel(int location_id) {
442 std::string key = 444 std::string key =
443 data_storage_prefix + "Hunt|" + std::to_string(location_id); 445 fmt::format("{}Hunt|{}", data_storage_prefix, location_id);
444 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); 446 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
445 } 447 }
446 448
@@ -449,12 +451,13 @@ struct APState {
449 } 451 }
450 452
451 bool HasAchievement(const std::string& name) { 453 bool HasAchievement(const std::string& name) {
452 std::string key = data_storage_prefix + "Achievement|" + name; 454 std::string key =
455 fmt::format("{}Achievement|{}", data_storage_prefix, name);
453 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); 456 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
454 } 457 }
455 458
456 const std::set<std::string>& GetCheckedPaintings() { 459 const std::set<std::string>& GetCheckedPaintings() {
457 std::string key = data_storage_prefix + "Paintings"; 460 std::string key = fmt::format("{}Paintings", data_storage_prefix);
458 if (!data_storage.count(key)) { 461 if (!data_storage.count(key)) {
459 data_storage[key] = std::set<std::string>(); 462 data_storage[key] = std::set<std::string>();
460 } 463 }
@@ -471,7 +474,7 @@ struct APState {
471 } 474 }
472 475
473 void RefreshTracker(bool reset) { 476 void RefreshTracker(bool reset) {
474 wxLogVerbose("Refreshing display..."); 477 TrackerLog("Refreshing display...");
475 478
476 RecalculateReachability(); 479 RecalculateReachability();
477 480
@@ -485,7 +488,7 @@ struct APState {
485 int64_t GetItemId(const std::string& item_name) { 488 int64_t GetItemId(const std::string& item_name) {
486 int64_t ap_id = apclient->get_item_id(item_name); 489 int64_t ap_id = apclient->get_item_id(item_name);
487 if (ap_id == APClient::INVALID_NAME_ID) { 490 if (ap_id == APClient::INVALID_NAME_ID) {
488 wxLogError("Could not find AP item ID for %s", item_name); 491 TrackerLog(fmt::format("Could not find AP item ID for {}", item_name));
489 } 492 }
490 493
491 return ap_id; 494 return ap_id;
@@ -564,16 +567,27 @@ int AP_GetMasteryRequirement() { return GetState().mastery_requirement; }
564int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } 567int AP_GetLevel2Requirement() { return GetState().level_2_requirement; }
565 568
566bool AP_IsLocationVisible(int classification) { 569bool AP_IsLocationVisible(int classification) {
570 int world_state = 0;
571
567 switch (GetState().location_checks) { 572 switch (GetState().location_checks) {
568 case kNORMAL_LOCATIONS: 573 case kNORMAL_LOCATIONS:
569 return classification & kLOCATION_NORMAL; 574 world_state = kLOCATION_NORMAL;
575 break;
570 case kREDUCED_LOCATIONS: 576 case kREDUCED_LOCATIONS:
571 return classification & kLOCATION_REDUCED; 577 world_state = kLOCATION_REDUCED;
578 break;
572 case kPANELSANITY: 579 case kPANELSANITY:
573 return classification & kLOCATION_INSANITY; 580 world_state = kLOCATION_INSANITY;
581 break;
574 default: 582 default:
575 return false; 583 return false;
576 } 584 }
585
586 if (GetState().door_shuffle_mode && !GetState().early_color_hallways) {
587 world_state |= kLOCATION_SMALL_SPHERE_ONE;
588 }
589
590 return (world_state & classification);
577} 591}
578 592
579VictoryCondition AP_GetVictoryCondition() { 593VictoryCondition AP_GetVictoryCondition() {
diff --git a/src/game_data.cpp b/src/game_data.cpp index 759eb0c..39ea360 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,6 +8,7 @@
13#include <sstream> 8#include <sstream>
14 9
15#include "global.h" 10#include "global.h"
11#include "logger.h"
16 12
17namespace { 13namespace {
18 14
@@ -36,7 +32,7 @@ LingoColor GetColorForString(const std::string &str) {
36 } else if (str == "purple") { 32 } else if (str == "purple") {
37 return LingoColor::kPurple; 33 return LingoColor::kPurple;
38 } else { 34 } else {
39 wxLogError("Invalid color: %s", str); 35 TrackerLog(fmt::format("Invalid color: {}", str));
40 36
41 return LingoColor::kNone; 37 return LingoColor::kNone;
42 } 38 }
@@ -90,7 +86,7 @@ struct GameData {
90 ap_id_by_color_[GetColorForString(input_name)] = 86 ap_id_by_color_[GetColorForString(input_name)] =
91 ids_config["special_items"][color_name].as<int>(); 87 ids_config["special_items"][color_name].as<int>();
92 } else { 88 } else {
93 wxLogError("Missing AP item ID for color %s", color_name); 89 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name));
94 } 90 }
95 }; 91 };
96 92
@@ -115,6 +111,7 @@ struct GameData {
115 auto process_single_entrance = 111 auto process_single_entrance =
116 [this, room_id, from_room_id](const YAML::Node &option) { 112 [this, room_id, from_room_id](const YAML::Node &option) {
117 Exit exit_obj; 113 Exit exit_obj;
114 exit_obj.source_room = from_room_id;
118 exit_obj.destination_room = room_id; 115 exit_obj.destination_room = room_id;
119 116
120 if (option["door"]) { 117 if (option["door"]) {
@@ -149,7 +146,7 @@ struct GameData {
149 switch (entrance_it.second.Type()) { 146 switch (entrance_it.second.Type()) {
150 case YAML::NodeType::Scalar: { 147 case YAML::NodeType::Scalar: {
151 // This is just "true". 148 // This is just "true".
152 rooms_[from_room_id].exits.push_back({.destination_room = room_id}); 149 rooms_[from_room_id].exits.push_back({.source_room = from_room_id, .destination_room = room_id});
153 break; 150 break;
154 } 151 }
155 case YAML::NodeType::Map: { 152 case YAML::NodeType::Map: {
@@ -167,7 +164,8 @@ struct GameData {
167 // This shouldn't happen. 164 // This shouldn't happen.
168 std::ostringstream formatted; 165 std::ostringstream formatted;
169 formatted << entrance_it; 166 formatted << entrance_it;
170 wxLogError("Error reading game data: %s", formatted.str()); 167 TrackerLog(
168 fmt::format("Error reading game data: {}", formatted.str()));
171 break; 169 break;
172 } 170 }
173 } 171 }
@@ -292,8 +290,9 @@ struct GameData {
292 [panels_[panel_id].name] 290 [panels_[panel_id].name]
293 .as<int>(); 291 .as<int>();
294 } else { 292 } else {
295 wxLogError("Missing AP location ID for panel %s - %s", 293 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}",
296 rooms_[room_id].name, panels_[panel_id].name); 294 rooms_[room_id].name,
295 panels_[panel_id].name));
297 } 296 }
298 } 297 }
299 } 298 }
@@ -356,8 +355,9 @@ struct GameData {
356 [doors_[door_id].name]["item"] 355 [doors_[door_id].name]["item"]
357 .as<int>(); 356 .as<int>();
358 } else { 357 } else {
359 wxLogError("Missing AP item ID for door %s - %s", 358 TrackerLog(fmt::format("Missing AP item ID for door {} - {}",
360 rooms_[room_id].name, doors_[door_id].name); 359 rooms_[room_id].name,
360 doors_[door_id].name));
361 } 361 }
362 } 362 }
363 363
@@ -371,8 +371,8 @@ struct GameData {
371 ids_config["door_groups"][doors_[door_id].group_name] 371 ids_config["door_groups"][doors_[door_id].group_name]
372 .as<int>(); 372 .as<int>();
373 } else { 373 } else {
374 wxLogError("Missing AP item ID for door group %s", 374 TrackerLog(fmt::format("Missing AP item ID for door group {}",
375 doors_[door_id].group_name); 375 doors_[door_id].group_name));
376 } 376 }
377 } 377 }
378 378
@@ -382,11 +382,11 @@ struct GameData {
382 } else if (!door_it.second["skip_location"] && 382 } else if (!door_it.second["skip_location"] &&
383 !door_it.second["event"]) { 383 !door_it.second["event"]) {
384 if (has_external_panels) { 384 if (has_external_panels) {
385 wxLogError( 385 TrackerLog(fmt::format(
386 "%s - %s has panels from other rooms but does not have an " 386 "{} - {} has panels from other rooms but does not have an "
387 "explicit location name and is not marked skip_location or " 387 "explicit location name and is not marked skip_location or "
388 "event", 388 "event",
389 rooms_[room_id].name, doors_[door_id].name); 389 rooms_[room_id].name, doors_[door_id].name));
390 } 390 }
391 391
392 doors_[door_id].location_name = 392 doors_[door_id].location_name =
@@ -406,8 +406,9 @@ struct GameData {
406 [doors_[door_id].name]["location"] 406 [doors_[door_id].name]["location"]
407 .as<int>(); 407 .as<int>();
408 } else { 408 } else {
409 wxLogError("Missing AP location ID for door %s - %s", 409 TrackerLog(fmt::format("Missing AP location ID for door {} - {}",
410 rooms_[room_id].name, doors_[door_id].name); 410 rooms_[room_id].name,
411 doors_[door_id].name));
411 } 412 }
412 } 413 }
413 414
@@ -524,8 +525,8 @@ struct GameData {
524 progressive_item_id = 525 progressive_item_id =
525 ids_config["progression"][progressive_item_name].as<int>(); 526 ids_config["progression"][progressive_item_name].as<int>();
526 } else { 527 } else {
527 wxLogError("Missing AP item ID for progressive item %s", 528 TrackerLog(fmt::format("Missing AP item ID for progressive item {}",
528 progressive_item_name); 529 progressive_item_name));
529 } 530 }
530 531
531 if (progression_it.second["doors"]) { 532 if (progression_it.second["doors"]) {
@@ -625,17 +626,20 @@ struct GameData {
625 } 626 }
626 } 627 }
627 628
629 if (room_name == "Starting Room") {
630 classification |= kLOCATION_SMALL_SPHERE_ONE;
631 }
632
628 int area_id = AddOrGetArea(area_name); 633 int area_id = AddOrGetArea(area_name);
629 MapArea &map_area = map_areas_[area_id]; 634 MapArea &map_area = map_areas_[area_id];
630 // room field should be the original room ID 635 // room field should be the original room ID
631 map_area.locations.push_back( 636 map_area.locations.push_back({.name = section_name,
632 {.name = section_name, 637 .ap_location_name = location_name,
633 .ap_location_name = location_name, 638 .ap_location_id = panel.ap_location_id,
634 .ap_location_id = panel.ap_location_id, 639 .room = panel.room,
635 .room = panel.room, 640 .panels = {panel.id},
636 .panels = {panel.id}, 641 .classification = classification,
637 .classification = classification, 642 .hunt = panel.hunt});
638 .hunt = panel.hunt});
639 locations_by_name[location_name] = {area_id, 643 locations_by_name[location_name] = {area_id,
640 map_area.locations.size() - 1}; 644 map_area.locations.size() - 1};
641 } 645 }
@@ -713,7 +717,7 @@ struct GameData {
713 717
714 // Report errors. 718 // Report errors.
715 for (const std::string &area : malconfigured_areas_) { 719 for (const std::string &area : malconfigured_areas_) {
716 wxLogError("Area data not found for: %s", area); 720 TrackerLog(fmt::format("Area data not found for: {}", area));
717 } 721 }
718 722
719 // Read in subway items. 723 // Read in subway items.
@@ -784,7 +788,7 @@ struct GameData {
784 788
785 for (const auto &[tag, items] : subway_tags) { 789 for (const auto &[tag, items] : subway_tags) {
786 if (items.size() == 1) { 790 if (items.size() == 1) {
787 wxLogWarning("Singleton subway item tag: %s", tag); 791 TrackerLog(fmt::format("Singleton subway item tag: {}", tag));
788 } 792 }
789 } 793 }
790 } 794 }
@@ -930,13 +934,11 @@ const SubwayItem &GD_GetSubwayItem(int id) {
930 return GetState().subway_items_.at(id); 934 return GetState().subway_items_.at(id);
931} 935}
932 936
933int GD_GetSubwayItemForPainting(const std::string &painting_id) { 937std::optional<int> GD_GetSubwayItemForPainting(const std::string &painting_id) {
934#ifndef NDEBUG 938 if (GetState().subway_item_by_painting_.count(painting_id)) {
935 if (!GetState().subway_item_by_painting_.count(painting_id)) { 939 return GetState().subway_item_by_painting_.at(painting_id);
936 wxLogError("No subway item for painting %s", painting_id);
937 } 940 }
938#endif 941 return std::nullopt;
939 return GetState().subway_item_by_painting_.at(painting_id);
940} 942}
941 943
942int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { 944int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) {
diff --git a/src/game_data.h b/src/game_data.h index aca4c3d..197585c 100644 --- a/src/game_data.h +++ b/src/game_data.h
@@ -22,6 +22,7 @@ enum class LingoColor {
22constexpr int kLOCATION_NORMAL = 1; 22constexpr int kLOCATION_NORMAL = 1;
23constexpr int kLOCATION_REDUCED = 2; 23constexpr int kLOCATION_REDUCED = 2;
24constexpr int kLOCATION_INSANITY = 4; 24constexpr int kLOCATION_INSANITY = 4;
25constexpr int kLOCATION_SMALL_SPHERE_ONE = 8;
25 26
26enum class EntranceType { 27enum class EntranceType {
27 kNormal, 28 kNormal,
@@ -89,6 +90,7 @@ struct PanelDoor {
89}; 90};
90 91
91struct Exit { 92struct Exit {
93 int source_room;
92 int destination_room; 94 int destination_room;
93 std::optional<int> door; 95 std::optional<int> door;
94 EntranceType type = EntranceType::kNormal; 96 EntranceType type = EntranceType::kNormal;
@@ -172,7 +174,7 @@ const std::vector<int>& GD_GetSunwarpDoors();
172int GD_GetRoomForSunwarp(int index); 174int GD_GetRoomForSunwarp(int index);
173const std::vector<SubwayItem>& GD_GetSubwayItems(); 175const std::vector<SubwayItem>& GD_GetSubwayItems();
174const SubwayItem& GD_GetSubwayItem(int id); 176const SubwayItem& GD_GetSubwayItem(int id);
175int GD_GetSubwayItemForPainting(const std::string& painting_id); 177std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id);
176int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); 178int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp);
177 179
178#endif /* end of include guard: GAME_DATA_H_9C42AC51 */ 180#endif /* end of include guard: GAME_DATA_H_9C42AC51 */
diff --git a/src/logger.cpp b/src/logger.cpp new file mode 100644 index 0000000..09fc331 --- /dev/null +++ b/src/logger.cpp
@@ -0,0 +1,32 @@
1#include "logger.h"
2
3#include <chrono>
4#include <fstream>
5#include <mutex>
6
7#include "global.h"
8
9namespace {
10
11class Logger {
12 public:
13 Logger() : logfile_(GetAbsolutePath("debug.log")) {}
14
15 void LogLine(const std::string& text) {
16 std::lock_guard guard(file_mutex_);
17 logfile_ << "[" << std::chrono::system_clock::now() << "] " << text
18 << std::endl;
19 logfile_.flush();
20 }
21
22 private:
23 std::ofstream logfile_;
24 std::mutex file_mutex_;
25};
26
27} // namespace
28
29void TrackerLog(std::string text) {
30 static Logger* instance = new Logger();
31 instance->LogLine(text);
32}
diff --git a/src/logger.h b/src/logger.h new file mode 100644 index 0000000..a27839f --- /dev/null +++ b/src/logger.h
@@ -0,0 +1,8 @@
1#ifndef LOGGER_H_9BDD07EA
2#define LOGGER_H_9BDD07EA
3
4#include <string>
5
6void TrackerLog(std::string message);
7
8#endif /* end of include guard: LOGGER_H_9BDD07EA */
diff --git a/src/main.cpp b/src/main.cpp index b327b25..1d7cc9e 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -4,25 +4,13 @@
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
13static std::ofstream* logfile;
14
15class TrackerApp : public wxApp { 11class TrackerApp : public wxApp {
16 public: 12 public:
17 virtual bool OnInit() { 13 virtual bool OnInit() {
18 logfile = new std::ofstream(GetAbsolutePath("debug.log"));
19 wxLog::SetActiveTarget(new wxLogStream(logfile));
20 wxLog::SetVerbose(true);
21
22#ifndef NDEBUG
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();
diff --git a/src/subway_map.cpp b/src/subway_map.cpp index 408c4f0..044e6fa 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp
@@ -1,5 +1,6 @@
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>
4#include <wx/dcgraph.h> 5#include <wx/dcgraph.h>
5 6
@@ -15,6 +16,29 @@ constexpr int OWL_ACTUAL_SIZE = 32;
15 16
16enum class ItemDrawType { kNone, kBox, kOwl }; 17enum class ItemDrawType { kNone, kBox, kOwl };
17 18
19namespace {
20
21std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item) {
22 std::optional<int> subway_door = subway_item.door;
23 if (AP_IsSunwarpShuffle() && subway_item.sunwarp &&
24 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
25 int sunwarp_index = subway_item.sunwarp->dots - 1;
26 if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) {
27 sunwarp_index += 6;
28 }
29
30 for (const auto &[start_index, mapping] : AP_GetSunwarpMapping()) {
31 if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) {
32 subway_door = GD_GetSunwarpDoors().at(mapping.dots - 1);
33 }
34 }
35
36 return subway_door;
37 }
38}
39
40} // namespace
41
18SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { 42SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
19 SetBackgroundStyle(wxBG_STYLE_PAINT); 43 SetBackgroundStyle(wxBG_STYLE_PAINT);
20 44
@@ -70,9 +94,8 @@ void SubwayMap::OnConnect() {
70 std::map<std::string, std::vector<int>> tagged; 94 std::map<std::string, std::vector<int>> tagged;
71 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 95 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
72 if (AP_HasEarlyColorHallways() && 96 if (AP_HasEarlyColorHallways() &&
73 (subway_item.special == "starting_room_paintings" || 97 subway_item.special == "starting_room_paintings") {
74 subway_item.special == "early_color_hallways")) { 98 tagged["early_ch"].push_back(subway_item.id);
75 tagged["early_color_hallways"].push_back(subway_item.id);
76 } 99 }
77 100
78 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { 101 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) {
@@ -85,10 +108,8 @@ void SubwayMap::OnConnect() {
85 108
86 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && 109 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp &&
87 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { 110 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
88 std::ostringstream tag; 111 std::string tag = fmt::format("subway{}", subway_item.sunwarp->dots);
89 tag << "sunwarp" << subway_item.sunwarp->dots; 112 tagged[tag].push_back(subway_item.id);
90
91 tagged[tag.str()].push_back(subway_item.id);
92 } 113 }
93 114
94 if (!AP_IsPilgrimageEnabled() && 115 if (!AP_IsPilgrimageEnabled() &&
@@ -100,8 +121,7 @@ void SubwayMap::OnConnect() {
100 121
101 if (AP_IsSunwarpShuffle()) { 122 if (AP_IsSunwarpShuffle()) {
102 for (const auto &[index, mapping] : AP_GetSunwarpMapping()) { 123 for (const auto &[index, mapping] : AP_GetSunwarpMapping()) {
103 std::ostringstream tag; 124 std::string tag = fmt::format("sunwarp{}", mapping.dots);
104 tag << "sunwarp" << mapping.dots;
105 125
106 SubwaySunwarp fromWarp; 126 SubwaySunwarp fromWarp;
107 if (index < 6) { 127 if (index < 6) {
@@ -121,8 +141,8 @@ void SubwayMap::OnConnect() {
121 toWarp.type = SubwaySunwarpType::kExit; 141 toWarp.type = SubwaySunwarpType::kExit;
122 } 142 }
123 143
124 tagged[tag.str()].push_back(GD_GetSubwayItemForSunwarp(fromWarp)); 144 tagged[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp));
125 tagged[tag.str()].push_back(GD_GetSubwayItemForSunwarp(toWarp)); 145 tagged[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp));
126 } 146 }
127 } 147 }
128 148
@@ -147,9 +167,13 @@ void SubwayMap::UpdateIndicators() {
147 checked_paintings_.insert(painting_id); 167 checked_paintings_.insert(painting_id);
148 168
149 if (AP_GetPaintingMapping().count(painting_id)) { 169 if (AP_GetPaintingMapping().count(painting_id)) {
150 networks_.AddLink(GD_GetSubwayItemForPainting(painting_id), 170 std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id);
151 GD_GetSubwayItemForPainting( 171 std::optional<int> to_id = GD_GetSubwayItemForPainting(
152 AP_GetPaintingMapping().at(painting_id))); 172 AP_GetPaintingMapping().at(painting_id));
173
174 if (from_id && to_id) {
175 networks_.AddLink(*from_id, *to_id);
176 }
153 } 177 }
154 } 178 }
155 } 179 }
@@ -266,9 +290,11 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
266 // Note that these requirements are duplicated on OnMouseClick so that it 290 // Note that these requirements are duplicated on OnMouseClick so that it
267 // knows when an item has a hover effect. 291 // knows when an item has a hover effect.
268 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_); 292 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
269 if (subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) { 293 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
294
295 if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
270 const std::map<std::string, bool> &report = 296 const std::map<std::string, bool> &report =
271 GetDoorRequirements(*subway_item.door); 297 GetDoorRequirements(*subway_door);
272 298
273 int acc_height = 10; 299 int acc_height = 10;
274 int col_width = 0; 300 int col_width = 0;
@@ -404,7 +430,7 @@ void SubwayMap::OnMouseMove(wxMouseEvent &event) {
404 std::vector<int> hovered = tree_->query( 430 std::vector<int> hovered = tree_->query(
405 {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2}); 431 {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
406 if (!hovered.empty()) { 432 if (!hovered.empty()) {
407 actual_hover_= hovered[0]; 433 actual_hover_ = hovered[0];
408 } else { 434 } else {
409 actual_hover_ = std::nullopt; 435 actual_hover_ = std::nullopt;
410 } 436 }
@@ -449,7 +475,9 @@ void SubwayMap::OnMouseClick(wxMouseEvent &event) {
449 475
450 if (actual_hover_) { 476 if (actual_hover_) {
451 const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_); 477 const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_);
452 if ((subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) || 478 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
479
480 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) ||
453 networks_.IsItemInNetwork(*hovered_item_)) { 481 networks_.IsItemInNetwork(*hovered_item_)) {
454 if (actual_hover_ != hovered_item_) { 482 if (actual_hover_ != hovered_item_) {
455 hovered_item_ = actual_hover_; 483 hovered_item_ = actual_hover_;
@@ -530,15 +558,9 @@ void SubwayMap::Redraw() {
530 std::optional<wxColour> shade_color; 558 std::optional<wxColour> shade_color;
531 559
532 if (AP_HasEarlyColorHallways() && 560 if (AP_HasEarlyColorHallways() &&
533 (subway_item.special == "starting_room_paintings" || 561 subway_item.special == "starting_room_paintings") {
534 subway_item.special == "early_color_hallways")) {
535 draw_type = ItemDrawType::kOwl; 562 draw_type = ItemDrawType::kOwl;
536 563 shade_color = wxColour(0, 255, 0, 128);
537 if (subway_item.special == "starting_room_paintings") {
538 shade_color = wxColour(0, 255, 0, 128);
539 } else {
540 shade_color = wxColour(255, 0, 0, 128);
541 }
542 } else if (subway_item.special == "sun_painting") { 564 } else if (subway_item.special == "sun_painting") {
543 if (!AP_IsPilgrimageEnabled()) { 565 if (!AP_IsPilgrimageEnabled()) {
544 if (IsDoorOpen(*subway_item.door)) { 566 if (IsDoorOpen(*subway_item.door)) {
@@ -570,7 +592,8 @@ void SubwayMap::Redraw() {
570 } 592 }
571 } 593 }
572 594
573 if (has_unchecked_painting || has_mapped_painting || has_codomain_painting) { 595 if (has_unchecked_painting || has_mapped_painting ||
596 has_codomain_painting) {
574 draw_type = ItemDrawType::kOwl; 597 draw_type = ItemDrawType::kOwl;
575 598
576 if (has_checked_painting) { 599 if (has_checked_painting) {
diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index 107ae49..80fd137 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp
@@ -157,7 +157,7 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) {
157 } 157 }
158 } 158 }
159 159
160 while (new_history.size() > 5) { 160 while (new_history.size() > 10) {
161 new_history.pop_back(); 161 new_history.pop_back();
162 } 162 }
163 163
diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index eaf3e6b..14d302b 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,6 +12,7 @@
9 12
10#include "ap_state.h" 13#include "ap_state.h"
11#include "game_data.h" 14#include "game_data.h"
15#include "logger.h"
12 16
13namespace { 17namespace {
14 18
@@ -231,7 +235,9 @@ class StateCalculator {
231 PaintingExit target_painting = 235 PaintingExit target_painting =
232 GD_GetPaintingExit(GD_GetPaintingByName( 236 GD_GetPaintingExit(GD_GetPaintingByName(
233 AP_GetPaintingMapping().at(cur_painting.internal_id))); 237 AP_GetPaintingMapping().at(cur_painting.internal_id)));
238 painting_exit.source_room = cur_painting.room;
234 painting_exit.destination_room = target_painting.room; 239 painting_exit.destination_room = target_painting.room;
240 painting_exit.type = EntranceType::kPainting;
235 241
236 new_boundary.push_back(painting_exit); 242 new_boundary.push_back(painting_exit);
237 } 243 }
@@ -258,6 +264,12 @@ class StateCalculator {
258 reachable_rooms_.insert(room_exit.destination_room); 264 reachable_rooms_.insert(room_exit.destination_room);
259 reachable_changed = true; 265 reachable_changed = true;
260 266
267#ifndef NDEBUG
268 std::list<int> room_path = paths_[room_exit.source_room];
269 room_path.push_back(room_exit.destination_room);
270 paths_[room_exit.destination_room] = room_path;
271#endif
272
261 const Room& room_obj = GD_GetRoom(room_exit.destination_room); 273 const Room& room_obj = GD_GetRoom(room_exit.destination_room);
262 for (const Exit& out_edge : room_obj.exits) { 274 for (const Exit& out_edge : room_obj.exits) {
263 if (out_edge.type == EntranceType::kPainting && 275 if (out_edge.type == EntranceType::kPainting &&
@@ -296,20 +308,33 @@ class StateCalculator {
296 308
297 if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") { 309 if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") {
298 new_boundary.push_back( 310 new_boundary.push_back(
299 {.destination_room = GD_GetRoomByName("Outside The Undeterred"), 311 {.source_room = room_exit.destination_room,
312 .destination_room = GD_GetRoomByName("Outside The Undeterred"),
300 .type = EntranceType::kPainting}); 313 .type = EntranceType::kPainting});
301 } 314 }
302 315
303 if (AP_IsPilgrimageEnabled()) { 316 if (AP_IsPilgrimageEnabled()) {
304 if (room_obj.name == "Hub Room") { 317 int pilgrimage_start_id = GD_GetRoomByName("Hub Room");
318 if (AP_IsSunwarpShuffle()) {
319 for (const auto& [start_index, mapping] :
320 AP_GetSunwarpMapping()) {
321 if (mapping.dots == 1) {
322 pilgrimage_start_id = GD_GetRoomForSunwarp(start_index);
323 }
324 }
325 }
326
327 if (room_exit.destination_room == pilgrimage_start_id) {
305 new_boundary.push_back( 328 new_boundary.push_back(
306 {.destination_room = GD_GetRoomByName("Pilgrim Antechamber"), 329 {.source_room = room_exit.destination_room,
330 .destination_room = GD_GetRoomByName("Pilgrim Antechamber"),
307 .type = EntranceType::kPilgrimage}); 331 .type = EntranceType::kPilgrimage});
308 } 332 }
309 } else { 333 } else {
310 if (room_obj.name == "Starting Room") { 334 if (room_obj.name == "Starting Room") {
311 new_boundary.push_back( 335 new_boundary.push_back(
312 {.destination_room = GD_GetRoomByName("Pilgrim Antechamber"), 336 {.source_room = room_exit.destination_room,
337 .destination_room = GD_GetRoomByName("Pilgrim Antechamber"),
313 .door = 338 .door =
314 GD_GetDoorByName("Pilgrim Antechamber - Sun Painting"), 339 GD_GetDoorByName("Pilgrim Antechamber - Sun Painting"),
315 .type = EntranceType::kPainting}); 340 .type = EntranceType::kPainting});
@@ -350,6 +375,19 @@ class StateCalculator {
350 return door_report_; 375 return door_report_;
351 } 376 }
352 377
378 std::string GetPathToRoom(int room_id) const {
379 if (!paths_.count(room_id)) {
380 return "";
381 }
382
383 const std::list<int>& path = paths_.at(room_id);
384 std::vector<std::string> room_names;
385 for (int room_id : path) {
386 room_names.push_back(GD_GetRoom(room_id).name);
387 }
388 return hatkirby::implode(room_names, " -> ");
389 }
390
353 private: 391 private:
354 template <typename T> 392 template <typename T>
355 Decision IsNonGroupedDoorReachable(const T& door_obj) { 393 Decision IsNonGroupedDoorReachable(const T& door_obj) {
@@ -603,6 +641,8 @@ class StateCalculator {
603 std::set<int> solveable_panels_; 641 std::set<int> solveable_panels_;
604 std::set<int> reachable_paintings_; 642 std::set<int> reachable_paintings_;
605 std::map<int, std::map<std::string, bool>> door_report_; 643 std::map<int, std::map<std::string, bool>> door_report_;
644
645 std::map<int, std::list<int>> paths_;
606}; 646};
607 647
608} // namespace 648} // namespace
diff --git a/src/version.h b/src/version.h index 7aab91b..8f93e18 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
7struct Version { 8struct 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
40constexpr const Version kTrackerVersion = Version(0, 10, 2); 39constexpr const Version kTrackerVersion = Version(0, 10, 6);
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