From 534e0aae81261990c1160378a085e2aeac9a6b7a Mon Sep 17 00:00:00 2001
From: Star Rauchenberger <fefferburbia@gmail.com>
Date: Tue, 16 Jul 2024 15:21:59 -0400
Subject: Fixed undefined behavior in GetRealSubwayDoor

---
 src/subway_map.cpp | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

(limited to 'src')

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 };
 namespace {
 
 std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item) {
-  std::optional<int> subway_door = subway_item.door;
   if (AP_IsSunwarpShuffle() && subway_item.sunwarp &&
       subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
     int sunwarp_index = subway_item.sunwarp->dots - 1;
@@ -29,12 +28,12 @@ std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item) {
 
     for (const auto &[start_index, mapping] : AP_GetSunwarpMapping()) {
       if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) {
-        subway_door = GD_GetSunwarpDoors().at(mapping.dots - 1);
+        return GD_GetSunwarpDoors().at(mapping.dots - 1);
       }
     }
-
-    return subway_door;
   }
+
+  return subway_item.door;
 }
 
 }  // namespace
-- 
cgit 1.4.1


From 52657e9eaa7520a841f0eb384472dbde6522e748 Mon Sep 17 00:00:00 2001
From: Star Rauchenberger <fefferburbia@gmail.com>
Date: Wed, 17 Jul 2024 14:07:35 -0400
Subject: Fix pilgrimage detection allowing sunwarps when shuffled

---
 src/tracker_state.cpp | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

(limited to 'src')

diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp
index ba615d1..bd63076 100644
--- a/src/tracker_state.cpp
+++ b/src/tracker_state.cpp
@@ -180,7 +180,8 @@ class StateCalculator {
     std::list<int> panel_boundary;
     std::list<int> painting_boundary;
     std::list<Exit> flood_boundary;
-    flood_boundary.push_back({.destination_room = options_.start});
+    flood_boundary.push_back(
+        {.source_room = -1, .destination_room = options_.start});
 
     bool reachable_changed = true;
     while (reachable_changed) {
@@ -282,12 +283,11 @@ class StateCalculator {
               if (AP_GetSunwarpMapping().count(index)) {
                 const SunwarpMapping& sm = AP_GetSunwarpMapping().at(index);
 
-                Exit sunwarp_exit;
-                sunwarp_exit.destination_room =
-                    GD_GetRoomForSunwarp(sm.exit_index);
-                sunwarp_exit.door = GD_GetSunwarpDoors().at(sm.dots - 1);
-
-                new_boundary.push_back(sunwarp_exit);
+                new_boundary.push_back(
+                    {.source_room = room_exit.destination_room,
+                     .destination_room = GD_GetRoomForSunwarp(sm.exit_index),
+                     .door = GD_GetSunwarpDoors().at(sm.dots - 1),
+                     .type = EntranceType::kSunwarp});
               }
             }
           }
-- 
cgit 1.4.1


From 3cea704b86a60be2178d1faf5e1be0f927a8c56d Mon Sep 17 00:00:00 2001
From: Star Rauchenberger <fefferburbia@gmail.com>
Date: Wed, 17 Jul 2024 14:11:08 -0400
Subject: Don't show unreachable paintings as checked

---
 src/area_popup.cpp | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

(limited to 'src')

diff --git a/src/area_popup.cpp b/src/area_popup.cpp
index 58d8897..ca3b352 100644
--- a/src/area_popup.cpp
+++ b/src/area_popup.cpp
@@ -126,15 +126,15 @@ void AreaPopup::UpdateIndicators() {
   if (AP_IsPaintingShuffle()) {
     for (int painting_id : map_area.paintings) {
       const PaintingExit& painting = GD_GetPaintingExit(painting_id);
-      bool checked = AP_IsPaintingChecked(painting.internal_id);
-      wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_;
-
-      mem_dc.DrawBitmap(*eye_ptr, {10, cur_height});
 
       bool reachable = IsPaintingReachable(painting_id);
       const wxColour* text_color = reachable ? wxWHITE : wxRED;
       mem_dc.SetTextForeground(*text_color);
 
+      bool checked = reachable && AP_IsPaintingChecked(painting.internal_id);
+      wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_;
+      mem_dc.DrawBitmap(*eye_ptr, {10, cur_height});
+
       wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id);  // TODO: Replace with friendly name.
       mem_dc.DrawText(painting.internal_id,
                       {10 + 32 + 10,
-- 
cgit 1.4.1


From deaa92cd5f6f83afaac608f22992163fb3a1a662 Mon Sep 17 00:00:00 2001
From: Star Rauchenberger <fefferburbia@gmail.com>
Date: Wed, 17 Jul 2024 14:11:31 -0400
Subject: Bump version

---
 src/version.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src')

diff --git a/src/version.h b/src/version.h
index 8f93e18..f89a32d 100644
--- a/src/version.h
+++ b/src/version.h
@@ -36,6 +36,6 @@ struct Version {
   }
 };
 
-constexpr const Version kTrackerVersion = Version(0, 10, 6);
+constexpr const Version kTrackerVersion = Version(0, 10, 7);
 
 #endif /* end of include guard: VERSION_H_C757E53C */
\ No newline at end of file
-- 
cgit 1.4.1


From 8c5b719469bc61e33a451d9b3aeb66c7b0a6d68e Mon Sep 17 00:00:00 2001
From: Star Rauchenberger <fefferburbia@gmail.com>
Date: Fri, 19 Jul 2024 03:51:23 -0400
Subject: Added savedata analyzer

---
 CMakeLists.txt        |  1 +
 src/ap_state.cpp      |  7 +++++
 src/ap_state.h        |  2 ++
 src/area_popup.cpp    | 40 +++++++++++++++++++------
 src/game_data.cpp     |  9 +++++-
 src/game_data.h       |  3 ++
 src/godot_variant.cpp | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/godot_variant.h   | 28 ++++++++++++++++++
 src/tracker_frame.cpp | 35 ++++++++++++++++++++++
 src/tracker_frame.h   |  2 ++
 src/tracker_panel.cpp | 54 +++++++++++++++++++++++++++++++--
 src/tracker_panel.h   | 19 ++++++++++++
 12 files changed, 269 insertions(+), 13 deletions(-)
 create mode 100644 src/godot_variant.cpp
 create mode 100644 src/godot_variant.h

(limited to 'src')

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
   "src/subway_map.cpp"
   "src/network_set.cpp"
   "src/logger.cpp"
+  "src/godot_variant.cpp"
   "vendor/whereami/whereami.c"
 )
 set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20)
diff --git a/src/ap_state.cpp b/src/ap_state.cpp
index ebc5fc9..f8d4ee0 100644
--- a/src/ap_state.cpp
+++ b/src/ap_state.cpp
@@ -52,6 +52,8 @@ struct APState {
   std::list<std::string> tracked_data_storage_keys;
   std::string victory_data_storage_key;
 
+  std::string save_name;
+
   std::map<int64_t, int> inventory;
   std::set<int64_t> checked_locations;
   std::map<std::string, std::any> data_storage;
@@ -131,6 +133,7 @@ struct APState {
                                             cert_store);
     }
 
+    save_name.clear();
     inventory.clear();
     checked_locations.clear();
     data_storage.clear();
@@ -228,6 +231,8 @@ struct APState {
           fmt::format("Connected to Archipelago! ({}@{})", player, server));
       TrackerLog("Connected to Archipelago!");
 
+      save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(),
+                              apclient->get_player_number());
       data_storage_prefix =
           fmt::format("Lingo_{}_", apclient->get_player_number());
       door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
@@ -509,6 +514,8 @@ void AP_Connect(std::string server, std::string player, std::string password) {
   GetState().Connect(server, player, password);
 }
 
+std::string AP_GetSaveName() { return GetState().save_name; }
+
 bool AP_HasCheckedGameLocation(int location_id) {
   return GetState().HasCheckedGameLocation(location_id);
 }
diff --git a/src/ap_state.h b/src/ap_state.h
index 7af7395..f8936e5 100644
--- a/src/ap_state.h
+++ b/src/ap_state.h
@@ -43,6 +43,8 @@ void AP_SetTrackerFrame(TrackerFrame* tracker_frame);
 
 void AP_Connect(std::string server, std::string player, std::string password);
 
+std::string AP_GetSaveName();
+
 bool AP_HasCheckedGameLocation(int location_id);
 
 bool AP_HasCheckedHuntPanel(int location_id);
diff --git a/src/area_popup.cpp b/src/area_popup.cpp
index ca3b352..b18ba62 100644
--- a/src/area_popup.cpp
+++ b/src/area_popup.cpp
@@ -2,10 +2,13 @@
 
 #include <wx/dcbuffer.h>
 
+#include <algorithm>
+
 #include "ap_state.h"
 #include "game_data.h"
 #include "global.h"
 #include "tracker_config.h"
+#include "tracker_panel.h"
 #include "tracker_state.h"
 
 AreaPopup::AreaPopup(wxWindow* parent, int area_id)
@@ -43,15 +46,23 @@ void AreaPopup::UpdateIndicators() {
 
   mem_dc.SetFont(GetFont());
 
+  TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
+
   std::vector<int> real_locations;
 
   for (int section_id = 0; section_id < map_area.locations.size();
        section_id++) {
     const Location& location = map_area.locations.at(section_id);
 
-    if (!AP_IsLocationVisible(location.classification) &&
-        !(location.hunt && GetTrackerConfig().show_hunt_panels)) {
-      continue;
+    if (tracker_panel->IsPanelsMode()) {
+      if (!location.panel) {
+        continue;
+      }
+    } else {
+      if (!AP_IsLocationVisible(location.classification) &&
+          !(location.hunt && GetTrackerConfig().show_hunt_panels)) {
+        continue;
+      }
     }
 
     real_locations.push_back(section_id);
@@ -65,7 +76,7 @@ void AreaPopup::UpdateIndicators() {
     }
   }
 
-  if (AP_IsPaintingShuffle()) {
+  if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) {
     for (int painting_id : map_area.paintings) {
       const PaintingExit& painting = GD_GetPaintingExit(painting_id);
       wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id);  // TODO: Replace with a friendly name.
@@ -102,10 +113,21 @@ void AreaPopup::UpdateIndicators() {
   for (int section_id : real_locations) {
     const Location& location = map_area.locations.at(section_id);
 
-    bool checked =
-        AP_HasCheckedGameLocation(location.ap_location_id) ||
-        (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id)) ||
-        (IsLocationWinCondition(location) && AP_HasReachedGoal());
+    bool checked = false;
+    if (IsLocationWinCondition(location)) {
+      checked = AP_HasReachedGoal();
+    } else if (tracker_panel->IsPanelsMode()) {
+      checked = location.panel && std::any_of(
+          location.panels.begin(), location.panels.end(),
+          [tracker_panel](int panel_id) {
+            const Panel& panel = GD_GetPanel(panel_id);
+            return tracker_panel->GetSolvedPanels().contains(panel.nodepath);
+          });
+    } else {
+      checked =
+          AP_HasCheckedGameLocation(location.ap_location_id) ||
+          (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id));
+    }
 
     wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_;
 
@@ -123,7 +145,7 @@ void AreaPopup::UpdateIndicators() {
     cur_height += 10 + 32;
   }
 
-  if (AP_IsPaintingShuffle()) {
+  if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) {
     for (int painting_id : map_area.paintings) {
       const PaintingExit& painting = GD_GetPaintingExit(painting_id);
 
diff --git a/src/game_data.cpp b/src/game_data.cpp
index 4c0104f..b8e1386 100644
--- a/src/game_data.cpp
+++ b/src/game_data.cpp
@@ -265,6 +265,11 @@ struct GameData {
                 panel_it.second["location_name"].as<std::string>();
           }
 
+          if (panel_it.second["id"]) {
+            panels_[panel_id].nodepath =
+                panel_it.second["id"].as<std::string>();
+          }
+
           if (panel_it.second["hunt"]) {
             panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>();
           }
@@ -564,7 +569,8 @@ struct GameData {
                                     .room = panel.room,
                                     .panels = {panel.id},
                                     .classification = classification,
-                                    .hunt = panel.hunt});
+                                    .hunt = panel.hunt,
+                                    .panel = true});
       locations_by_name[location_name] = {area_id,
                                           map_area.locations.size() - 1};
     }
@@ -617,6 +623,7 @@ struct GameData {
       for (const Location &location : map_area.locations) {
         map_area.classification |= location.classification;
         map_area.hunt |= location.hunt;
+        map_area.panel |= location.panel;
       }
     }
 
diff --git a/src/game_data.h b/src/game_data.h
index 23f7b3a..71bc533 100644
--- a/src/game_data.h
+++ b/src/game_data.h
@@ -43,6 +43,7 @@ struct Panel {
   int id;
   int room;
   std::string name;
+  std::string nodepath;
   std::vector<LingoColor> colors;
   std::vector<int> required_rooms;
   std::vector<int> required_doors;
@@ -113,6 +114,7 @@ struct Location {
   std::vector<int> panels;
   int classification = 0;
   bool hunt = false;
+  bool panel = false;
 };
 
 struct MapArea {
@@ -124,6 +126,7 @@ struct MapArea {
   int map_y;
   int classification = 0;
   bool hunt = false;
+  bool panel = false;
 };
 
 enum class SubwaySunwarpType {
diff --git a/src/godot_variant.cpp b/src/godot_variant.cpp
new file mode 100644
index 0000000..152408f
--- /dev/null
+++ b/src/godot_variant.cpp
@@ -0,0 +1,82 @@
+// Godot save decoder algorithm by Chris Souvey.
+
+#include "godot_variant.h"
+
+#include <algorithm>
+#include <charconv>
+#include <cstddef>
+#include <fstream>
+#include <string>
+#include <tuple>
+#include <variant>
+#include <vector>
+
+namespace {
+
+uint16_t ReadUint16(std::basic_istream<char>& stream) {
+  uint16_t result;
+  stream.read(reinterpret_cast<char*>(&result), 2);
+  return result;
+}
+
+uint32_t ReadUint32(std::basic_istream<char>& stream) {
+  uint32_t result;
+  stream.read(reinterpret_cast<char*>(&result), 4);
+  return result;
+}
+
+GodotVariant ParseVariant(std::basic_istream<char>& stream) {
+  uint16_t type = ReadUint16(stream);
+  stream.ignore(2);
+
+  switch (type) {
+    case 1: {
+      // bool
+      bool boolval = (ReadUint32(stream) == 1);
+      return {boolval};
+    }
+    case 15: {
+      // nodepath
+      uint32_t name_length = ReadUint32(stream) & 0x7fffffff;
+      uint32_t subname_length = ReadUint32(stream) & 0x7fffffff;
+      uint32_t flags = ReadUint32(stream);
+
+      std::vector<std::string> result;
+      for (size_t i = 0; i < name_length + subname_length; i++) {
+        uint32_t char_length = ReadUint32(stream);
+        uint32_t padded_length = (char_length % 4 == 0)
+                                     ? char_length
+                                     : (char_length + 4 - (char_length % 4));
+        std::vector<char> next_bytes(padded_length);
+        stream.read(next_bytes.data(), padded_length);
+        std::string next_piece;
+        std::copy(next_bytes.begin(),
+                  std::next(next_bytes.begin(), char_length),
+                  std::back_inserter(next_piece));
+        result.push_back(next_piece);
+      }
+
+      return {result};
+    }
+    case 19: {
+      // array
+      uint32_t length = ReadUint32(stream) & 0x7fffffff;
+      std::vector<GodotVariant> result;
+      for (size_t i = 0; i < length; i++) {
+        result.push_back(ParseVariant(stream));
+      }
+      return {result};
+    }
+    default: {
+      // eh
+    }
+  }
+}
+
+}  // namespace
+
+GodotVariant ParseGodotFile(std::string filename) {
+  std::ifstream file_stream(filename, std::ios_base::binary);
+  file_stream.ignore(4);
+  return ParseVariant(file_stream);
+}
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 @@
+#ifndef GODOT_VARIANT_H_ED7F2EB6
+#define GODOT_VARIANT_H_ED7F2EB6
+
+#include <string>
+#include <variant>
+#include <vector>
+
+struct GodotVariant {
+  using value_type = std::variant<std::monostate, bool, std::vector<std::string>, std::vector<GodotVariant>>;
+
+  value_type value;
+
+  GodotVariant(value_type v) : value(v) {}
+
+  bool AsBool() const { return std::get<bool>(value); }
+
+  const std::vector<std::string>& AsNodePath() const {
+    return std::get<std::vector<std::string>>(value);
+  }
+
+  const std::vector<GodotVariant>& AsArray() const {
+    return std::get<std::vector<GodotVariant>>(value);
+  }
+};
+
+GodotVariant ParseGodotFile(std::string filename);
+
+#endif /* end of include guard: GODOT_VARIANT_H_ED7F2EB6 */
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 @@
 
 #include <wx/aboutdlg.h>
 #include <wx/choicebk.h>
+#include <wx/filedlg.h>
 #include <wx/notebook.h>
+#include <wx/stdpaths.h>
 #include <wx/webrequest.h>
 
+#include <fmt/core.h>
 #include <nlohmann/json.hpp>
 #include <sstream>
 
@@ -23,6 +26,7 @@ enum TrackerFrameIds {
   ID_SETTINGS = 3,
   ID_ZOOM_IN = 4,
   ID_ZOOM_OUT = 5,
+  ID_OPEN_SAVE_FILE = 6,
 };
 
 wxDEFINE_EVENT(STATE_RESET, wxCommandEvent);
@@ -38,6 +42,7 @@ TrackerFrame::TrackerFrame()
 
   wxMenu *menuFile = new wxMenu();
   menuFile->Append(ID_CONNECT, "&Connect");
+  menuFile->Append(ID_OPEN_SAVE_FILE, "&Open Save Data\tCtrl-O");
   menuFile->Append(ID_SETTINGS, "&Settings");
   menuFile->Append(wxID_EXIT);
 
@@ -71,6 +76,7 @@ TrackerFrame::TrackerFrame()
   Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN);
   Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT);
   Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this);
+  Bind(wxEVT_MENU, &TrackerFrame::OnOpenFile, this, ID_OPEN_SAVE_FILE);
   Bind(STATE_RESET, &TrackerFrame::OnStateReset, this);
   Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
   Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);
@@ -204,10 +210,36 @@ void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) {
   zoom_out_menu_item_->Enable(event.GetSelection() == 1);
 }
 
+void TrackerFrame::OnOpenFile(wxCommandEvent& event) {
+  wxFileDialog open_file_dialog(
+      this, "Open Lingo Save File",
+      fmt::format("{}\\Godot\\app_userdata\\Lingo\\level1_stable",
+                  wxStandardPaths::Get().GetUserConfigDir().ToStdString()),
+      AP_GetSaveName(), "Lingo save file (*.save)|*.save",
+      wxFD_OPEN | wxFD_FILE_MUST_EXIST);
+  if (open_file_dialog.ShowModal() == wxID_CANCEL) {
+    return;
+  }
+
+  std::string savedata_path = open_file_dialog.GetPath().ToStdString();
+
+  if (panels_panel_ == nullptr) {
+    panels_panel_ = new TrackerPanel(notebook_);
+    notebook_->AddPage(panels_panel_, "Panels");
+  }
+
+  notebook_->SetSelection(notebook_->FindPage(panels_panel_));
+  panels_panel_->SetSavedataPath(savedata_path);
+}
+
 void TrackerFrame::OnStateReset(wxCommandEvent& event) {
   tracker_panel_->UpdateIndicators();
   achievements_pane_->UpdateIndicators();
   subway_map_->OnConnect();
+  if (panels_panel_ != nullptr) {
+    notebook_->DeletePage(notebook_->FindPage(panels_panel_));
+    panels_panel_ = nullptr;
+  }
   Refresh();
 }
 
@@ -215,6 +247,9 @@ void TrackerFrame::OnStateChanged(wxCommandEvent &event) {
   tracker_panel_->UpdateIndicators();
   achievements_pane_->UpdateIndicators();
   subway_map_->UpdateIndicators();
+  if (panels_panel_ != nullptr) {
+    panels_panel_->UpdateIndicators();
+  }
   Refresh();
 }
 
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 {
   void OnZoomIn(wxCommandEvent &event);
   void OnZoomOut(wxCommandEvent &event);
   void OnChangePage(wxBookCtrlEvent &event);
+  void OnOpenFile(wxCommandEvent &event);
 
   void OnStateReset(wxCommandEvent &event);
   void OnStateChanged(wxCommandEvent &event);
@@ -46,6 +47,7 @@ class TrackerFrame : public wxFrame {
   TrackerPanel *tracker_panel_;
   AchievementsPane *achievements_pane_;
   SubwayMap *subway_map_;
+  TrackerPanel *panels_panel_ = nullptr;
 
   wxMenuItem *zoom_in_menu_item_;
   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 @@
 #include "tracker_panel.h"
 
+#include <fmt/core.h>
 #include <wx/dcbuffer.h>
 
+#include <algorithm>
+
 #include "ap_state.h"
 #include "area_popup.h"
 #include "game_data.h"
 #include "global.h"
+#include "godot_variant.h"
 #include "tracker_config.h"
 #include "tracker_state.h"
 
@@ -53,6 +57,35 @@ void TrackerPanel::UpdateIndicators() {
   Redraw();
 }
 
+void TrackerPanel::SetSavedataPath(std::string savedata_path) {
+  if (!panels_mode_) {
+    wxButton *refresh_button = new wxButton(this, wxID_ANY, "Refresh", {15, 15});
+    refresh_button->Bind(wxEVT_BUTTON, &TrackerPanel::OnRefreshSavedata, this);
+  }
+
+  savedata_path_ = savedata_path;
+  panels_mode_ = true;
+
+  RefreshSavedata();
+}
+
+void TrackerPanel::RefreshSavedata() {
+  solved_panels_.clear();
+
+  GodotVariant godot_variant = ParseGodotFile(*savedata_path_);
+  for (const GodotVariant &panel_node : godot_variant.AsArray()) {
+    const std::vector<GodotVariant> &fields = panel_node.AsArray();
+    if (fields[1].AsBool()) {
+      const std::vector<std::string> &nodepath = fields[0].AsNodePath();
+      std::string key = fmt::format("{}/{}", nodepath[3], nodepath[4]);
+      solved_panels_.insert(key);
+    }
+  }
+
+  UpdateIndicators();
+  Refresh();
+}
+
 void TrackerPanel::OnPaint(wxPaintEvent &event) {
   if (GetSize() != rendered_.GetSize()) {
     Redraw();
@@ -92,6 +125,10 @@ void TrackerPanel::OnMouseMove(wxMouseEvent &event) {
   event.Skip();
 }
 
+void TrackerPanel::OnRefreshSavedata(wxCommandEvent &event) {
+  RefreshSavedata();
+}
+
 void TrackerPanel::Redraw() {
   wxSize panel_size = GetSize();
   wxSize image_size = map_image_.GetSize();
@@ -142,21 +179,32 @@ void TrackerPanel::Redraw() {
 
   for (AreaIndicator &area : areas_) {
     const MapArea &map_area = GD_GetMapArea(area.area_id);
-    if (!AP_IsLocationVisible(map_area.classification) &&
+    if (panels_mode_) {
+      area.active = map_area.panel;
+    } else if (!AP_IsLocationVisible(map_area.classification) &&
         !(map_area.hunt && GetTrackerConfig().show_hunt_panels) &&
         !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
       area.active = false;
-      continue;
     } else {
       area.active = true;
     }
 
+    if (!area.active) {
+      continue;
+    }
+
     bool has_reachable_unchecked = false;
     bool has_unreachable_unchecked = false;
     for (const Location &section : map_area.locations) {
       bool has_unchecked = false;
       if (IsLocationWinCondition(section)) {
         has_unchecked = !AP_HasReachedGoal();
+      } else if (panels_mode_) {
+        has_unchecked = section.panel && std::any_of(
+            section.panels.begin(), section.panels.end(), [this](int panel_id) {
+              const Panel &panel = GD_GetPanel(panel_id);
+              return !solved_panels_.contains(panel.nodepath);
+            });
       } else if (AP_IsLocationVisible(section.classification)) {
         has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
       } else if (section.hunt && GetTrackerConfig().show_hunt_panels) {
@@ -172,7 +220,7 @@ void TrackerPanel::Redraw() {
       }
     }
 
-    if (AP_IsPaintingShuffle()) {
+    if (AP_IsPaintingShuffle() && !panels_mode_) {
       for (int painting_id : map_area.paintings) {
         const PaintingExit &painting = GD_GetPaintingExit(painting_id);
         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 @@
 #include <wx/wx.h>
 #endif
 
+#include <optional>
+#include <set>
+#include <string>
+
 class AreaPopup;
 
 class TrackerPanel : public wxPanel {
@@ -15,6 +19,14 @@ class TrackerPanel : public wxPanel {
 
   void UpdateIndicators();
 
+  void SetSavedataPath(std::string savedata_path);
+
+  bool IsPanelsMode() const { return panels_mode_; }
+
+  const std::set<std::string> &GetSolvedPanels() const {
+    return solved_panels_;
+  }
+
  private:
   struct AreaIndicator {
     int area_id = -1;
@@ -28,9 +40,12 @@ class TrackerPanel : public wxPanel {
 
   void OnPaint(wxPaintEvent &event);
   void OnMouseMove(wxMouseEvent &event);
+  void OnRefreshSavedata(wxCommandEvent &event);
 
   void Redraw();
 
+  void RefreshSavedata();
+
   wxImage map_image_;
   wxImage player_image_;
   wxBitmap rendered_;
@@ -42,6 +57,10 @@ class TrackerPanel : public wxPanel {
   double scale_y_ = 0;
 
   std::vector<AreaIndicator> areas_;
+
+  bool panels_mode_ = false;
+  std::optional<std::string> savedata_path_;
+  std::set<std::string> solved_panels_;
 };
 
 #endif /* end of include guard: TRACKER_PANEL_H_D675A54D */
-- 
cgit 1.4.1


From 5709482f51d16ee93838deee990091f2c9f21818 Mon Sep 17 00:00:00 2001
From: Star Rauchenberger <fefferburbia@gmail.com>
Date: Fri, 19 Jul 2024 03:51:41 -0400
Subject: Bump version

---
 src/version.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src')

diff --git a/src/version.h b/src/version.h
index f89a32d..ed5e97c 100644
--- a/src/version.h
+++ b/src/version.h
@@ -36,6 +36,6 @@ struct Version {
   }
 };
 
-constexpr const Version kTrackerVersion = Version(0, 10, 7);
+constexpr const Version kTrackerVersion = Version(0, 11, 0);
 
 #endif /* end of include guard: VERSION_H_C757E53C */
\ No newline at end of file
-- 
cgit 1.4.1


From 6507e05f4029985137ad8deef21142bec90cd65c Mon Sep 17 00:00:00 2001
From: Star Rauchenberger <fefferburbia@gmail.com>
Date: Fri, 19 Jul 2024 03:55:14 -0400
Subject: Added monostate return for ParseVariant

---
 src/godot_variant.cpp | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src')

diff --git a/src/godot_variant.cpp b/src/godot_variant.cpp
index 152408f..1bc906f 100644
--- a/src/godot_variant.cpp
+++ b/src/godot_variant.cpp
@@ -69,6 +69,7 @@ GodotVariant ParseVariant(std::basic_istream<char>& stream) {
     }
     default: {
       // eh
+      return {std::monostate{}};
     }
   }
 }
-- 
cgit 1.4.1