diff options
| -rw-r--r-- | CHANGELOG.md | 21 | ||||
| -rw-r--r-- | CMakeLists.txt | 1 | ||||
| -rw-r--r-- | VERSION | 2 | ||||
| -rw-r--r-- | src/ap_state.cpp | 7 | ||||
| -rw-r--r-- | src/ap_state.h | 2 | ||||
| -rw-r--r-- | src/area_popup.cpp | 48 | ||||
| -rw-r--r-- | src/game_data.cpp | 9 | ||||
| -rw-r--r-- | src/game_data.h | 3 | ||||
| -rw-r--r-- | src/godot_variant.cpp | 83 | ||||
| -rw-r--r-- | src/godot_variant.h | 28 | ||||
| -rw-r--r-- | src/subway_map.cpp | 7 | ||||
| -rw-r--r-- | src/tracker_frame.cpp | 35 | ||||
| -rw-r--r-- | src/tracker_frame.h | 2 | ||||
| -rw-r--r-- | src/tracker_panel.cpp | 54 | ||||
| -rw-r--r-- | src/tracker_panel.h | 19 | ||||
| -rw-r--r-- | src/tracker_state.cpp | 14 | ||||
| -rw-r--r-- | src/version.h | 2 |
17 files changed, 307 insertions, 30 deletions
| diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e32ac..a9ce76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
| @@ -1,5 +1,26 @@ | |||
| 1 | # lingo-ap-tracker Releases | 1 | # lingo-ap-tracker Releases |
| 2 | 2 | ||
| 3 | ## v0.11.0 - 2024-07-19 | ||
| 4 | |||
| 5 | - Added a savedata analyzer. When connected to a world, the user can open up the | ||
| 6 | Lingo save file associated with the connected world, and a new tab will open | ||
| 7 | up showing unsolved panels that are accessible, even if the world is not a | ||
| 8 | panelsanity world. | ||
| 9 | |||
| 10 | Download: | ||
| 11 | [lingo-ap-tracker-v0.11.0-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.0-win64.zip)<br/> | ||
| 12 | Source: [v0.11.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.0) | ||
| 13 | |||
| 14 | ## v0.10.7 - 2024-07-17 | ||
| 15 | |||
| 16 | - Fixed issue with pilgrimage detection when sunwarps are shuffled where it | ||
| 17 | would expect you to use sunwarps mid-pilgrimage. | ||
| 18 | - Fixed unreachable paintings sometimes being shown as already checked. | ||
| 19 | |||
| 20 | Download: | ||
| 21 | [lingo-ap-tracker-v0.10.7-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.10.7-win64.zip)<br/> | ||
| 22 | Source: [v0.10.7](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.7) | ||
| 23 | |||
| 3 | ## v0.10.6 - 2024-07-16 | 24 | ## v0.10.6 - 2024-07-16 |
| 4 | 25 | ||
| 5 | - The status bar now shows the name and server for the connected slot. | 26 | - The status bar now shows the name and server for the connected slot. |
| diff --git a/CMakeLists.txt b/CMakeLists.txt index f9f1117..e1cb7f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
| @@ -48,6 +48,7 @@ add_executable(lingo_ap_tracker | |||
| 48 | "src/subway_map.cpp" | 48 | "src/subway_map.cpp" |
| 49 | "src/network_set.cpp" | 49 | "src/network_set.cpp" |
| 50 | "src/logger.cpp" | 50 | "src/logger.cpp" |
| 51 | "src/godot_variant.cpp" | ||
| 51 | "vendor/whereami/whereami.c" | 52 | "vendor/whereami/whereami.c" |
| 52 | ) | 53 | ) |
| 53 | set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) | 54 | set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) |
| diff --git a/VERSION b/VERSION index 7b63f4b..e88c34f 100644 --- a/VERSION +++ b/VERSION | |||
| @@ -1 +1 @@ | |||
| v0.10.6 \ No newline at end of file | v0.11.0 \ No newline at end of file | ||
| diff --git a/src/ap_state.cpp b/src/ap_state.cpp index d501e81..fbd8d12 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp | |||
| @@ -52,6 +52,8 @@ struct APState { | |||
| 52 | std::list<std::string> tracked_data_storage_keys; | 52 | std::list<std::string> tracked_data_storage_keys; |
| 53 | std::string victory_data_storage_key; | 53 | std::string victory_data_storage_key; |
| 54 | 54 | ||
| 55 | std::string save_name; | ||
| 56 | |||
| 55 | std::map<int64_t, int> inventory; | 57 | std::map<int64_t, int> inventory; |
| 56 | std::set<int64_t> checked_locations; | 58 | std::set<int64_t> checked_locations; |
| 57 | std::map<std::string, std::any> data_storage; | 59 | std::map<std::string, std::any> data_storage; |
| @@ -132,6 +134,7 @@ struct APState { | |||
| 132 | cert_store); | 134 | cert_store); |
| 133 | } | 135 | } |
| 134 | 136 | ||
| 137 | save_name.clear(); | ||
| 135 | inventory.clear(); | 138 | inventory.clear(); |
| 136 | checked_locations.clear(); | 139 | checked_locations.clear(); |
| 137 | data_storage.clear(); | 140 | data_storage.clear(); |
| @@ -230,6 +233,8 @@ struct APState { | |||
| 230 | fmt::format("Connected to Archipelago! ({}@{})", player, server)); | 233 | fmt::format("Connected to Archipelago! ({}@{})", player, server)); |
| 231 | TrackerLog("Connected to Archipelago!"); | 234 | TrackerLog("Connected to Archipelago!"); |
| 232 | 235 | ||
| 236 | save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(), | ||
| 237 | apclient->get_player_number()); | ||
| 233 | data_storage_prefix = | 238 | data_storage_prefix = |
| 234 | fmt::format("Lingo_{}_", apclient->get_player_number()); | 239 | fmt::format("Lingo_{}_", apclient->get_player_number()); |
| 235 | door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>(); | 240 | door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>(); |
| @@ -522,6 +527,8 @@ void AP_Connect(std::string server, std::string player, std::string password) { | |||
| 522 | GetState().Connect(server, player, password); | 527 | GetState().Connect(server, player, password); |
| 523 | } | 528 | } |
| 524 | 529 | ||
| 530 | std::string AP_GetSaveName() { return GetState().save_name; } | ||
| 531 | |||
| 525 | bool AP_HasCheckedGameLocation(int location_id) { | 532 | bool AP_HasCheckedGameLocation(int location_id) { |
| 526 | return GetState().HasCheckedGameLocation(location_id); | 533 | return GetState().HasCheckedGameLocation(location_id); |
| 527 | } | 534 | } |
| diff --git a/src/ap_state.h b/src/ap_state.h index 190b21f..e06d4ff 100644 --- a/src/ap_state.h +++ b/src/ap_state.h | |||
| @@ -43,6 +43,8 @@ void AP_SetTrackerFrame(TrackerFrame* tracker_frame); | |||
| 43 | 43 | ||
| 44 | void AP_Connect(std::string server, std::string player, std::string password); | 44 | void AP_Connect(std::string server, std::string player, std::string password); |
| 45 | 45 | ||
| 46 | std::string AP_GetSaveName(); | ||
| 47 | |||
| 46 | bool AP_HasCheckedGameLocation(int location_id); | 48 | bool AP_HasCheckedGameLocation(int location_id); |
| 47 | 49 | ||
| 48 | bool AP_HasCheckedHuntPanel(int location_id); | 50 | bool AP_HasCheckedHuntPanel(int location_id); |
| diff --git a/src/area_popup.cpp b/src/area_popup.cpp index 58d8897..b18ba62 100644 --- a/src/area_popup.cpp +++ b/src/area_popup.cpp | |||
| @@ -2,10 +2,13 @@ | |||
| 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" |
| 8 | #include "tracker_config.h" | 10 | #include "tracker_config.h" |
| 11 | #include "tracker_panel.h" | ||
| 9 | #include "tracker_state.h" | 12 | #include "tracker_state.h" |
| 10 | 13 | ||
| 11 | AreaPopup::AreaPopup(wxWindow* parent, int area_id) | 14 | AreaPopup::AreaPopup(wxWindow* parent, int area_id) |
| @@ -43,15 +46,23 @@ void AreaPopup::UpdateIndicators() { | |||
| 43 | 46 | ||
| 44 | mem_dc.SetFont(GetFont()); | 47 | mem_dc.SetFont(GetFont()); |
| 45 | 48 | ||
| 49 | TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent()); | ||
| 50 | |||
| 46 | std::vector<int> real_locations; | 51 | std::vector<int> real_locations; |
| 47 | 52 | ||
| 48 | for (int section_id = 0; section_id < map_area.locations.size(); | 53 | for (int section_id = 0; section_id < map_area.locations.size(); |
| 49 | section_id++) { | 54 | section_id++) { |
| 50 | const Location& location = map_area.locations.at(section_id); | 55 | const Location& location = map_area.locations.at(section_id); |
| 51 | 56 | ||
| 52 | if (!AP_IsLocationVisible(location.classification) && | 57 | if (tracker_panel->IsPanelsMode()) { |
| 53 | !(location.hunt && GetTrackerConfig().show_hunt_panels)) { | 58 | if (!location.panel) { |
| 54 | continue; | 59 | continue; |
| 60 | } | ||
| 61 | } else { | ||
| 62 | if (!AP_IsLocationVisible(location.classification) && | ||
| 63 | !(location.hunt && GetTrackerConfig().show_hunt_panels)) { | ||
| 64 | continue; | ||
| 65 | } | ||
| 55 | } | 66 | } |
| 56 | 67 | ||
| 57 | real_locations.push_back(section_id); | 68 | real_locations.push_back(section_id); |
| @@ -65,7 +76,7 @@ void AreaPopup::UpdateIndicators() { | |||
| 65 | } | 76 | } |
| 66 | } | 77 | } |
| 67 | 78 | ||
| 68 | if (AP_IsPaintingShuffle()) { | 79 | if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) { |
| 69 | for (int painting_id : map_area.paintings) { | 80 | for (int painting_id : map_area.paintings) { |
| 70 | const PaintingExit& painting = GD_GetPaintingExit(painting_id); | 81 | const PaintingExit& painting = GD_GetPaintingExit(painting_id); |
| 71 | wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with a friendly name. | 82 | wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with a friendly name. |
| @@ -102,10 +113,21 @@ void AreaPopup::UpdateIndicators() { | |||
| 102 | for (int section_id : real_locations) { | 113 | for (int section_id : real_locations) { |
| 103 | const Location& location = map_area.locations.at(section_id); | 114 | const Location& location = map_area.locations.at(section_id); |
| 104 | 115 | ||
| 105 | bool checked = | 116 | bool checked = false; |
| 106 | AP_HasCheckedGameLocation(location.ap_location_id) || | 117 | if (IsLocationWinCondition(location)) { |
| 107 | (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id)) || | 118 | checked = AP_HasReachedGoal(); |
| 108 | (IsLocationWinCondition(location) && AP_HasReachedGoal()); | 119 | } else if (tracker_panel->IsPanelsMode()) { |
| 120 | checked = location.panel && std::any_of( | ||
| 121 | location.panels.begin(), location.panels.end(), | ||
| 122 | [tracker_panel](int panel_id) { | ||
| 123 | const Panel& panel = GD_GetPanel(panel_id); | ||
| 124 | return tracker_panel->GetSolvedPanels().contains(panel.nodepath); | ||
| 125 | }); | ||
| 126 | } else { | ||
| 127 | checked = | ||
| 128 | AP_HasCheckedGameLocation(location.ap_location_id) || | ||
| 129 | (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id)); | ||
| 130 | } | ||
| 109 | 131 | ||
| 110 | wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; | 132 | wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; |
| 111 | 133 | ||
| @@ -123,18 +145,18 @@ void AreaPopup::UpdateIndicators() { | |||
| 123 | cur_height += 10 + 32; | 145 | cur_height += 10 + 32; |
| 124 | } | 146 | } |
| 125 | 147 | ||
| 126 | if (AP_IsPaintingShuffle()) { | 148 | if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) { |
| 127 | for (int painting_id : map_area.paintings) { | 149 | for (int painting_id : map_area.paintings) { |
| 128 | const PaintingExit& painting = GD_GetPaintingExit(painting_id); | 150 | const PaintingExit& painting = GD_GetPaintingExit(painting_id); |
| 129 | bool checked = AP_IsPaintingChecked(painting.internal_id); | ||
| 130 | wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; | ||
| 131 | |||
| 132 | mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); | ||
| 133 | 151 | ||
| 134 | bool reachable = IsPaintingReachable(painting_id); | 152 | bool reachable = IsPaintingReachable(painting_id); |
| 135 | const wxColour* text_color = reachable ? wxWHITE : wxRED; | 153 | const wxColour* text_color = reachable ? wxWHITE : wxRED; |
| 136 | mem_dc.SetTextForeground(*text_color); | 154 | mem_dc.SetTextForeground(*text_color); |
| 137 | 155 | ||
| 156 | bool checked = reachable && AP_IsPaintingChecked(painting.internal_id); | ||
| 157 | wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; | ||
| 158 | mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); | ||
| 159 | |||
| 138 | wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with friendly name. | 160 | wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with friendly name. |
| 139 | mem_dc.DrawText(painting.internal_id, | 161 | mem_dc.DrawText(painting.internal_id, |
| 140 | {10 + 32 + 10, | 162 | {10 + 32 + 10, |
| diff --git a/src/game_data.cpp b/src/game_data.cpp index ec8d8f5..0786edb 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp | |||
| @@ -267,6 +267,11 @@ struct GameData { | |||
| 267 | panel_it.second["location_name"].as<std::string>(); | 267 | panel_it.second["location_name"].as<std::string>(); |
| 268 | } | 268 | } |
| 269 | 269 | ||
| 270 | if (panel_it.second["id"]) { | ||
| 271 | panels_[panel_id].nodepath = | ||
| 272 | panel_it.second["id"].as<std::string>(); | ||
| 273 | } | ||
| 274 | |||
| 270 | if (panel_it.second["hunt"]) { | 275 | if (panel_it.second["hunt"]) { |
| 271 | panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>(); | 276 | panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>(); |
| 272 | } | 277 | } |
| @@ -639,7 +644,8 @@ struct GameData { | |||
| 639 | .room = panel.room, | 644 | .room = panel.room, |
| 640 | .panels = {panel.id}, | 645 | .panels = {panel.id}, |
| 641 | .classification = classification, | 646 | .classification = classification, |
| 642 | .hunt = panel.hunt}); | 647 | .hunt = panel.hunt, |
| 648 | .panel = true}); | ||
| 643 | locations_by_name[location_name] = {area_id, | 649 | locations_by_name[location_name] = {area_id, |
| 644 | map_area.locations.size() - 1}; | 650 | map_area.locations.size() - 1}; |
| 645 | } | 651 | } |
| @@ -692,6 +698,7 @@ struct GameData { | |||
| 692 | for (const Location &location : map_area.locations) { | 698 | for (const Location &location : map_area.locations) { |
| 693 | map_area.classification |= location.classification; | 699 | map_area.classification |= location.classification; |
| 694 | map_area.hunt |= location.hunt; | 700 | map_area.hunt |= location.hunt; |
| 701 | map_area.panel |= location.panel; | ||
| 695 | } | 702 | } |
| 696 | } | 703 | } |
| 697 | 704 | ||
| diff --git a/src/game_data.h b/src/game_data.h index 197585c..6f287cf 100644 --- a/src/game_data.h +++ b/src/game_data.h | |||
| @@ -43,6 +43,7 @@ struct Panel { | |||
| 43 | int id; | 43 | int id; |
| 44 | int room; | 44 | int room; |
| 45 | std::string name; | 45 | std::string name; |
| 46 | std::string nodepath; | ||
| 46 | std::vector<LingoColor> colors; | 47 | std::vector<LingoColor> colors; |
| 47 | std::vector<int> required_rooms; | 48 | std::vector<int> required_rooms; |
| 48 | std::vector<int> required_doors; | 49 | std::vector<int> required_doors; |
| @@ -120,6 +121,7 @@ struct Location { | |||
| 120 | std::vector<int> panels; | 121 | std::vector<int> panels; |
| 121 | int classification = 0; | 122 | int classification = 0; |
| 122 | bool hunt = false; | 123 | bool hunt = false; |
| 124 | bool panel = false; | ||
| 123 | }; | 125 | }; |
| 124 | 126 | ||
| 125 | struct MapArea { | 127 | struct MapArea { |
| @@ -131,6 +133,7 @@ struct MapArea { | |||
| 131 | int map_y; | 133 | int map_y; |
| 132 | int classification = 0; | 134 | int classification = 0; |
| 133 | bool hunt = false; | 135 | bool hunt = false; |
| 136 | bool panel = false; | ||
| 134 | }; | 137 | }; |
| 135 | 138 | ||
| 136 | enum class SubwaySunwarpType { | 139 | enum class SubwaySunwarpType { |
| diff --git a/src/godot_variant.cpp b/src/godot_variant.cpp new file mode 100644 index 0000000..1bc906f --- /dev/null +++ b/src/godot_variant.cpp | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | // Godot save decoder algorithm by Chris Souvey. | ||
| 2 | |||
| 3 | #include "godot_variant.h" | ||
| 4 | |||
| 5 | #include <algorithm> | ||
| 6 | #include <charconv> | ||
| 7 | #include <cstddef> | ||
| 8 | #include <fstream> | ||
| 9 | #include <string> | ||
| 10 | #include <tuple> | ||
| 11 | #include <variant> | ||
| 12 | #include <vector> | ||
| 13 | |||
| 14 | namespace { | ||
| 15 | |||
| 16 | uint16_t ReadUint16(std::basic_istream<char>& stream) { | ||
| 17 | uint16_t result; | ||
| 18 | stream.read(reinterpret_cast<char*>(&result), 2); | ||
| 19 | return result; | ||
| 20 | } | ||
| 21 | |||
| 22 | uint32_t ReadUint32(std::basic_istream<char>& stream) { | ||
| 23 | uint32_t result; | ||
| 24 | stream.read(reinterpret_cast<char*>(&result), 4); | ||
| 25 | return result; | ||
| 26 | } | ||
| 27 | |||
| 28 | GodotVariant ParseVariant(std::basic_istream<char>& stream) { | ||
| 29 | uint16_t type = ReadUint16(stream); | ||
| 30 | stream.ignore(2); | ||
| 31 | |||
| 32 | switch (type) { | ||
| 33 | case 1: { | ||
| 34 | // bool | ||
| 35 | bool boolval = (ReadUint32(stream) == 1); | ||
| 36 | return {boolval}; | ||
| 37 | } | ||
| 38 | case 15: { | ||
| 39 | // nodepath | ||
| 40 | uint32_t name_length = ReadUint32(stream) & 0x7fffffff; | ||
| 41 | uint32_t subname_length = ReadUint32(stream) & 0x7fffffff; | ||
| 42 | uint32_t flags = ReadUint32(stream); | ||
| 43 | |||
| 44 | std::vector<std::string> result; | ||
| 45 | for (size_t i = 0; i < name_length + subname_length; i++) { | ||
| 46 | uint32_t char_length = ReadUint32(stream); | ||
| 47 | uint32_t padded_length = (char_length % 4 == 0) | ||
| 48 | ? char_length | ||
| 49 | : (char_length + 4 - (char_length % 4)); | ||
| 50 | std::vector<char> next_bytes(padded_length); | ||
| 51 | stream.read(next_bytes.data(), padded_length); | ||
| 52 | std::string next_piece; | ||
| 53 | std::copy(next_bytes.begin(), | ||
| 54 | std::next(next_bytes.begin(), char_length), | ||
| 55 | std::back_inserter(next_piece)); | ||
| 56 | result.push_back(next_piece); | ||
| 57 | } | ||
| 58 | |||
| 59 | return {result}; | ||
| 60 | } | ||
| 61 | case 19: { | ||
| 62 | // array | ||
| 63 | uint32_t length = ReadUint32(stream) & 0x7fffffff; | ||
| 64 | std::vector<GodotVariant> result; | ||
| 65 | for (size_t i = 0; i < length; i++) { | ||
| 66 | result.push_back(ParseVariant(stream)); | ||
| 67 | } | ||
| 68 | return {result}; | ||
| 69 | } | ||
| 70 | default: { | ||
| 71 | // eh | ||
| 72 | return {std::monostate{}}; | ||
| 73 | } | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | } // namespace | ||
| 78 | |||
| 79 | GodotVariant ParseGodotFile(std::string filename) { | ||
| 80 | std::ifstream file_stream(filename, std::ios_base::binary); | ||
| 81 | file_stream.ignore(4); | ||
| 82 | return ParseVariant(file_stream); | ||
| 83 | } | ||
| diff --git a/src/godot_variant.h b/src/godot_variant.h new file mode 100644 index 0000000..620e569 --- /dev/null +++ b/src/godot_variant.h | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | #ifndef GODOT_VARIANT_H_ED7F2EB6 | ||
| 2 | #define GODOT_VARIANT_H_ED7F2EB6 | ||
| 3 | |||
| 4 | #include <string> | ||
| 5 | #include <variant> | ||
| 6 | #include <vector> | ||
| 7 | |||
| 8 | struct GodotVariant { | ||
| 9 | using value_type = std::variant<std::monostate, bool, std::vector<std::string>, std::vector<GodotVariant>>; | ||
| 10 | |||
| 11 | value_type value; | ||
| 12 | |||
| 13 | GodotVariant(value_type v) : value(v) {} | ||
| 14 | |||
| 15 | bool AsBool() const { return std::get<bool>(value); } | ||
| 16 | |||
| 17 | const std::vector<std::string>& AsNodePath() const { | ||
| 18 | return std::get<std::vector<std::string>>(value); | ||
| 19 | } | ||
| 20 | |||
| 21 | const std::vector<GodotVariant>& AsArray() const { | ||
| 22 | return std::get<std::vector<GodotVariant>>(value); | ||
| 23 | } | ||
| 24 | }; | ||
| 25 | |||
| 26 | GodotVariant ParseGodotFile(std::string filename); | ||
| 27 | |||
| 28 | #endif /* end of include guard: GODOT_VARIANT_H_ED7F2EB6 */ | ||
| diff --git a/src/subway_map.cpp b/src/subway_map.cpp index 044e6fa..9bfedf9 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp | |||
| @@ -19,7 +19,6 @@ enum class ItemDrawType { kNone, kBox, kOwl }; | |||
| 19 | namespace { | 19 | namespace { |
| 20 | 20 | ||
| 21 | std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item) { | 21 | std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item) { |
| 22 | std::optional<int> subway_door = subway_item.door; | ||
| 23 | if (AP_IsSunwarpShuffle() && subway_item.sunwarp && | 22 | if (AP_IsSunwarpShuffle() && subway_item.sunwarp && |
| 24 | subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { | 23 | subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { |
| 25 | int sunwarp_index = subway_item.sunwarp->dots - 1; | 24 | int sunwarp_index = subway_item.sunwarp->dots - 1; |
| @@ -29,12 +28,12 @@ std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item) { | |||
| 29 | 28 | ||
| 30 | for (const auto &[start_index, mapping] : AP_GetSunwarpMapping()) { | 29 | for (const auto &[start_index, mapping] : AP_GetSunwarpMapping()) { |
| 31 | if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) { | 30 | if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) { |
| 32 | subway_door = GD_GetSunwarpDoors().at(mapping.dots - 1); | 31 | return GD_GetSunwarpDoors().at(mapping.dots - 1); |
| 33 | } | 32 | } |
| 34 | } | 33 | } |
| 35 | |||
| 36 | return subway_door; | ||
| 37 | } | 34 | } |
| 35 | |||
| 36 | return subway_item.door; | ||
| 38 | } | 37 | } |
| 39 | 38 | ||
| 40 | } // namespace | 39 | } // namespace |
| diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index 80fd137..3b6beda 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp | |||
| @@ -2,9 +2,12 @@ | |||
| 2 | 2 | ||
| 3 | #include <wx/aboutdlg.h> | 3 | #include <wx/aboutdlg.h> |
| 4 | #include <wx/choicebk.h> | 4 | #include <wx/choicebk.h> |
| 5 | #include <wx/filedlg.h> | ||
| 5 | #include <wx/notebook.h> | 6 | #include <wx/notebook.h> |
| 7 | #include <wx/stdpaths.h> | ||
| 6 | #include <wx/webrequest.h> | 8 | #include <wx/webrequest.h> |
| 7 | 9 | ||
| 10 | #include <fmt/core.h> | ||
| 8 | #include <nlohmann/json.hpp> | 11 | #include <nlohmann/json.hpp> |
| 9 | #include <sstream> | 12 | #include <sstream> |
| 10 | 13 | ||
| @@ -23,6 +26,7 @@ enum TrackerFrameIds { | |||
| 23 | ID_SETTINGS = 3, | 26 | ID_SETTINGS = 3, |
| 24 | ID_ZOOM_IN = 4, | 27 | ID_ZOOM_IN = 4, |
| 25 | ID_ZOOM_OUT = 5, | 28 | ID_ZOOM_OUT = 5, |
| 29 | ID_OPEN_SAVE_FILE = 6, | ||
| 26 | }; | 30 | }; |
| 27 | 31 | ||
| 28 | wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); | 32 | wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); |
| @@ -38,6 +42,7 @@ TrackerFrame::TrackerFrame() | |||
| 38 | 42 | ||
| 39 | wxMenu *menuFile = new wxMenu(); | 43 | wxMenu *menuFile = new wxMenu(); |
| 40 | menuFile->Append(ID_CONNECT, "&Connect"); | 44 | menuFile->Append(ID_CONNECT, "&Connect"); |
| 45 | menuFile->Append(ID_OPEN_SAVE_FILE, "&Open Save Data\tCtrl-O"); | ||
| 41 | menuFile->Append(ID_SETTINGS, "&Settings"); | 46 | menuFile->Append(ID_SETTINGS, "&Settings"); |
| 42 | menuFile->Append(wxID_EXIT); | 47 | menuFile->Append(wxID_EXIT); |
| 43 | 48 | ||
| @@ -71,6 +76,7 @@ TrackerFrame::TrackerFrame() | |||
| 71 | Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN); | 76 | Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN); |
| 72 | Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT); | 77 | Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT); |
| 73 | Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this); | 78 | Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this); |
| 79 | Bind(wxEVT_MENU, &TrackerFrame::OnOpenFile, this, ID_OPEN_SAVE_FILE); | ||
| 74 | Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); | 80 | Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); |
| 75 | Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); | 81 | Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); |
| 76 | Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); | 82 | Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); |
| @@ -204,10 +210,36 @@ void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) { | |||
| 204 | zoom_out_menu_item_->Enable(event.GetSelection() == 1); | 210 | zoom_out_menu_item_->Enable(event.GetSelection() == 1); |
| 205 | } | 211 | } |
| 206 | 212 | ||
| 213 | void TrackerFrame::OnOpenFile(wxCommandEvent& event) { | ||
| 214 | wxFileDialog open_file_dialog( | ||
| 215 | this, "Open Lingo Save File", | ||
| 216 | fmt::format("{}\\Godot\\app_userdata\\Lingo\\level1_stable", | ||
| 217 | wxStandardPaths::Get().GetUserConfigDir().ToStdString()), | ||
| 218 | AP_GetSaveName(), "Lingo save file (*.save)|*.save", | ||
| 219 | wxFD_OPEN | wxFD_FILE_MUST_EXIST); | ||
| 220 | if (open_file_dialog.ShowModal() == wxID_CANCEL) { | ||
| 221 | return; | ||
| 222 | } | ||
| 223 | |||
| 224 | std::string savedata_path = open_file_dialog.GetPath().ToStdString(); | ||
| 225 | |||
| 226 | if (panels_panel_ == nullptr) { | ||
| 227 | panels_panel_ = new TrackerPanel(notebook_); | ||
| 228 | notebook_->AddPage(panels_panel_, "Panels"); | ||
| 229 | } | ||
| 230 | |||
| 231 | notebook_->SetSelection(notebook_->FindPage(panels_panel_)); | ||
| 232 | panels_panel_->SetSavedataPath(savedata_path); | ||
| 233 | } | ||
| 234 | |||
| 207 | void TrackerFrame::OnStateReset(wxCommandEvent& event) { | 235 | void TrackerFrame::OnStateReset(wxCommandEvent& event) { |
| 208 | tracker_panel_->UpdateIndicators(); | 236 | tracker_panel_->UpdateIndicators(); |
| 209 | achievements_pane_->UpdateIndicators(); | 237 | achievements_pane_->UpdateIndicators(); |
| 210 | subway_map_->OnConnect(); | 238 | subway_map_->OnConnect(); |
| 239 | if (panels_panel_ != nullptr) { | ||
| 240 | notebook_->DeletePage(notebook_->FindPage(panels_panel_)); | ||
| 241 | panels_panel_ = nullptr; | ||
| 242 | } | ||
| 211 | Refresh(); | 243 | Refresh(); |
| 212 | } | 244 | } |
| 213 | 245 | ||
| @@ -215,6 +247,9 @@ void TrackerFrame::OnStateChanged(wxCommandEvent &event) { | |||
| 215 | tracker_panel_->UpdateIndicators(); | 247 | tracker_panel_->UpdateIndicators(); |
| 216 | achievements_pane_->UpdateIndicators(); | 248 | achievements_pane_->UpdateIndicators(); |
| 217 | subway_map_->UpdateIndicators(); | 249 | subway_map_->UpdateIndicators(); |
| 250 | if (panels_panel_ != nullptr) { | ||
| 251 | panels_panel_->UpdateIndicators(); | ||
| 252 | } | ||
| 218 | Refresh(); | 253 | Refresh(); |
| 219 | } | 254 | } |
| 220 | 255 | ||
| diff --git a/src/tracker_frame.h b/src/tracker_frame.h index f7cb3f2..19bd0b3 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h | |||
| @@ -35,6 +35,7 @@ class TrackerFrame : public wxFrame { | |||
| 35 | void OnZoomIn(wxCommandEvent &event); | 35 | void OnZoomIn(wxCommandEvent &event); |
| 36 | void OnZoomOut(wxCommandEvent &event); | 36 | void OnZoomOut(wxCommandEvent &event); |
| 37 | void OnChangePage(wxBookCtrlEvent &event); | 37 | void OnChangePage(wxBookCtrlEvent &event); |
| 38 | void OnOpenFile(wxCommandEvent &event); | ||
| 38 | 39 | ||
| 39 | void OnStateReset(wxCommandEvent &event); | 40 | void OnStateReset(wxCommandEvent &event); |
| 40 | void OnStateChanged(wxCommandEvent &event); | 41 | void OnStateChanged(wxCommandEvent &event); |
| @@ -46,6 +47,7 @@ class TrackerFrame : public wxFrame { | |||
| 46 | TrackerPanel *tracker_panel_; | 47 | TrackerPanel *tracker_panel_; |
| 47 | AchievementsPane *achievements_pane_; | 48 | AchievementsPane *achievements_pane_; |
| 48 | SubwayMap *subway_map_; | 49 | SubwayMap *subway_map_; |
| 50 | TrackerPanel *panels_panel_ = nullptr; | ||
| 49 | 51 | ||
| 50 | wxMenuItem *zoom_in_menu_item_; | 52 | wxMenuItem *zoom_in_menu_item_; |
| 51 | wxMenuItem *zoom_out_menu_item_; | 53 | wxMenuItem *zoom_out_menu_item_; |
| diff --git a/src/tracker_panel.cpp b/src/tracker_panel.cpp index d60c1b6..2e1497b 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 "godot_variant.h" | ||
| 9 | #include "tracker_config.h" | 13 | #include "tracker_config.h" |
| 10 | #include "tracker_state.h" | 14 | #include "tracker_state.h" |
| 11 | 15 | ||
| @@ -53,6 +57,35 @@ void TrackerPanel::UpdateIndicators() { | |||
| 53 | Redraw(); | 57 | Redraw(); |
| 54 | } | 58 | } |
| 55 | 59 | ||
| 60 | void TrackerPanel::SetSavedataPath(std::string savedata_path) { | ||
| 61 | if (!panels_mode_) { | ||
| 62 | wxButton *refresh_button = new wxButton(this, wxID_ANY, "Refresh", {15, 15}); | ||
| 63 | refresh_button->Bind(wxEVT_BUTTON, &TrackerPanel::OnRefreshSavedata, this); | ||
| 64 | } | ||
| 65 | |||
| 66 | savedata_path_ = savedata_path; | ||
| 67 | panels_mode_ = true; | ||
| 68 | |||
| 69 | RefreshSavedata(); | ||
| 70 | } | ||
| 71 | |||
| 72 | void TrackerPanel::RefreshSavedata() { | ||
| 73 | solved_panels_.clear(); | ||
| 74 | |||
| 75 | GodotVariant godot_variant = ParseGodotFile(*savedata_path_); | ||
| 76 | for (const GodotVariant &panel_node : godot_variant.AsArray()) { | ||
| 77 | const std::vector<GodotVariant> &fields = panel_node.AsArray(); | ||
| 78 | if (fields[1].AsBool()) { | ||
| 79 | const std::vector<std::string> &nodepath = fields[0].AsNodePath(); | ||
| 80 | std::string key = fmt::format("{}/{}", nodepath[3], nodepath[4]); | ||
| 81 | solved_panels_.insert(key); | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | UpdateIndicators(); | ||
| 86 | Refresh(); | ||
| 87 | } | ||
| 88 | |||
| 56 | void TrackerPanel::OnPaint(wxPaintEvent &event) { | 89 | void TrackerPanel::OnPaint(wxPaintEvent &event) { |
| 57 | if (GetSize() != rendered_.GetSize()) { | 90 | if (GetSize() != rendered_.GetSize()) { |
| 58 | Redraw(); | 91 | Redraw(); |
| @@ -92,6 +125,10 @@ void TrackerPanel::OnMouseMove(wxMouseEvent &event) { | |||
| 92 | event.Skip(); | 125 | event.Skip(); |
| 93 | } | 126 | } |
| 94 | 127 | ||
| 128 | void TrackerPanel::OnRefreshSavedata(wxCommandEvent &event) { | ||
| 129 | RefreshSavedata(); | ||
| 130 | } | ||
| 131 | |||
| 95 | void TrackerPanel::Redraw() { | 132 | void TrackerPanel::Redraw() { |
| 96 | wxSize panel_size = GetSize(); | 133 | wxSize panel_size = GetSize(); |
| 97 | wxSize image_size = map_image_.GetSize(); | 134 | wxSize image_size = map_image_.GetSize(); |
| @@ -142,21 +179,32 @@ void TrackerPanel::Redraw() { | |||
| 142 | 179 | ||
| 143 | for (AreaIndicator &area : areas_) { | 180 | for (AreaIndicator &area : areas_) { |
| 144 | const MapArea &map_area = GD_GetMapArea(area.area_id); | 181 | const MapArea &map_area = GD_GetMapArea(area.area_id); |
| 145 | if (!AP_IsLocationVisible(map_area.classification) && | 182 | if (panels_mode_) { |
| 183 | area.active = map_area.panel; | ||
| 184 | } else if (!AP_IsLocationVisible(map_area.classification) && | ||
| 146 | !(map_area.hunt && GetTrackerConfig().show_hunt_panels) && | 185 | !(map_area.hunt && GetTrackerConfig().show_hunt_panels) && |
| 147 | !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) { | 186 | !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) { |
| 148 | area.active = false; | 187 | area.active = false; |
| 149 | continue; | ||
| 150 | } else { | 188 | } else { |
| 151 | area.active = true; | 189 | area.active = true; |
| 152 | } | 190 | } |
| 153 | 191 | ||
| 192 | if (!area.active) { | ||
| 193 | continue; | ||
| 194 | } | ||
| 195 | |||
| 154 | bool has_reachable_unchecked = false; | 196 | bool has_reachable_unchecked = false; |
| 155 | bool has_unreachable_unchecked = false; | 197 | bool has_unreachable_unchecked = false; |
| 156 | for (const Location §ion : map_area.locations) { | 198 | for (const Location §ion : map_area.locations) { |
| 157 | bool has_unchecked = false; | 199 | bool has_unchecked = false; |
| 158 | if (IsLocationWinCondition(section)) { | 200 | if (IsLocationWinCondition(section)) { |
| 159 | has_unchecked = !AP_HasReachedGoal(); | 201 | has_unchecked = !AP_HasReachedGoal(); |
| 202 | } else if (panels_mode_) { | ||
| 203 | has_unchecked = section.panel && std::any_of( | ||
| 204 | section.panels.begin(), section.panels.end(), [this](int panel_id) { | ||
| 205 | const Panel &panel = GD_GetPanel(panel_id); | ||
| 206 | return !solved_panels_.contains(panel.nodepath); | ||
| 207 | }); | ||
| 160 | } else if (AP_IsLocationVisible(section.classification)) { | 208 | } else if (AP_IsLocationVisible(section.classification)) { |
| 161 | has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); | 209 | has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); |
| 162 | } else if (section.hunt && GetTrackerConfig().show_hunt_panels) { | 210 | } else if (section.hunt && GetTrackerConfig().show_hunt_panels) { |
| @@ -172,7 +220,7 @@ void TrackerPanel::Redraw() { | |||
| 172 | } | 220 | } |
| 173 | } | 221 | } |
| 174 | 222 | ||
| 175 | if (AP_IsPaintingShuffle()) { | 223 | if (AP_IsPaintingShuffle() && !panels_mode_) { |
| 176 | for (int painting_id : map_area.paintings) { | 224 | for (int painting_id : map_area.paintings) { |
| 177 | const PaintingExit &painting = GD_GetPaintingExit(painting_id); | 225 | const PaintingExit &painting = GD_GetPaintingExit(painting_id); |
| 178 | if (!AP_IsPaintingChecked(painting.internal_id)) { | 226 | if (!AP_IsPaintingChecked(painting.internal_id)) { |
| diff --git a/src/tracker_panel.h b/src/tracker_panel.h index 06ec7a0..e1f515d 100644 --- a/src/tracker_panel.h +++ b/src/tracker_panel.h | |||
| @@ -7,6 +7,10 @@ | |||
| 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 { |
| @@ -15,6 +19,14 @@ class TrackerPanel : public wxPanel { | |||
| 15 | 19 | ||
| 16 | void UpdateIndicators(); | 20 | void UpdateIndicators(); |
| 17 | 21 | ||
| 22 | void SetSavedataPath(std::string savedata_path); | ||
| 23 | |||
| 24 | bool IsPanelsMode() const { return panels_mode_; } | ||
| 25 | |||
| 26 | const std::set<std::string> &GetSolvedPanels() const { | ||
| 27 | return solved_panels_; | ||
| 28 | } | ||
| 29 | |||
| 18 | private: | 30 | private: |
| 19 | struct AreaIndicator { | 31 | struct AreaIndicator { |
| 20 | int area_id = -1; | 32 | int area_id = -1; |
| @@ -28,9 +40,12 @@ class TrackerPanel : public wxPanel { | |||
| 28 | 40 | ||
| 29 | void OnPaint(wxPaintEvent &event); | 41 | void OnPaint(wxPaintEvent &event); |
| 30 | void OnMouseMove(wxMouseEvent &event); | 42 | void OnMouseMove(wxMouseEvent &event); |
| 43 | void OnRefreshSavedata(wxCommandEvent &event); | ||
| 31 | 44 | ||
| 32 | void Redraw(); | 45 | void Redraw(); |
| 33 | 46 | ||
| 47 | void RefreshSavedata(); | ||
| 48 | |||
| 34 | wxImage map_image_; | 49 | wxImage map_image_; |
| 35 | wxImage player_image_; | 50 | wxImage player_image_; |
| 36 | wxBitmap rendered_; | 51 | wxBitmap rendered_; |
| @@ -42,6 +57,10 @@ class TrackerPanel : public wxPanel { | |||
| 42 | double scale_y_ = 0; | 57 | double scale_y_ = 0; |
| 43 | 58 | ||
| 44 | std::vector<AreaIndicator> areas_; | 59 | std::vector<AreaIndicator> areas_; |
| 60 | |||
| 61 | bool panels_mode_ = false; | ||
| 62 | std::optional<std::string> savedata_path_; | ||
| 63 | std::set<std::string> solved_panels_; | ||
| 45 | }; | 64 | }; |
| 46 | 65 | ||
| 47 | #endif /* end of include guard: TRACKER_PANEL_H_D675A54D */ | 66 | #endif /* end of include guard: TRACKER_PANEL_H_D675A54D */ |
| diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index 66a9f94..18bb499 100644 --- a/src/tracker_state.cpp +++ b/src/tracker_state.cpp | |||
| @@ -194,7 +194,8 @@ class StateCalculator { | |||
| 194 | std::list<int> panel_boundary; | 194 | std::list<int> panel_boundary; |
| 195 | std::list<int> painting_boundary; | 195 | std::list<int> painting_boundary; |
| 196 | std::list<Exit> flood_boundary; | 196 | std::list<Exit> flood_boundary; |
| 197 | flood_boundary.push_back({.destination_room = options_.start}); | 197 | flood_boundary.push_back( |
| 198 | {.source_room = -1, .destination_room = options_.start}); | ||
| 198 | 199 | ||
| 199 | bool reachable_changed = true; | 200 | bool reachable_changed = true; |
| 200 | while (reachable_changed) { | 201 | while (reachable_changed) { |
| @@ -296,12 +297,11 @@ class StateCalculator { | |||
| 296 | if (AP_GetSunwarpMapping().count(index)) { | 297 | if (AP_GetSunwarpMapping().count(index)) { |
| 297 | const SunwarpMapping& sm = AP_GetSunwarpMapping().at(index); | 298 | const SunwarpMapping& sm = AP_GetSunwarpMapping().at(index); |
| 298 | 299 | ||
| 299 | Exit sunwarp_exit; | 300 | new_boundary.push_back( |
| 300 | sunwarp_exit.destination_room = | 301 | {.source_room = room_exit.destination_room, |
| 301 | GD_GetRoomForSunwarp(sm.exit_index); | 302 | .destination_room = GD_GetRoomForSunwarp(sm.exit_index), |
| 302 | sunwarp_exit.door = GD_GetSunwarpDoors().at(sm.dots - 1); | 303 | .door = GD_GetSunwarpDoors().at(sm.dots - 1), |
| 303 | 304 | .type = EntranceType::kSunwarp}); | |
| 304 | new_boundary.push_back(sunwarp_exit); | ||
| 305 | } | 305 | } |
| 306 | } | 306 | } |
| 307 | } | 307 | } |
| diff --git a/src/version.h b/src/version.h index 8f93e18..ed5e97c 100644 --- a/src/version.h +++ b/src/version.h | |||
| @@ -36,6 +36,6 @@ struct Version { | |||
| 36 | } | 36 | } |
| 37 | }; | 37 | }; |
| 38 | 38 | ||
| 39 | constexpr const Version kTrackerVersion = Version(0, 10, 6); | 39 | constexpr const Version kTrackerVersion = Version(0, 11, 0); |
| 40 | 40 | ||
| 41 | #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 |
