From 149e7c0836927e14a926a952bd1a7f0d1b49e779 Mon Sep 17 00:00:00 2001
From: Star Rauchenberger <fefferburbia@gmail.com>
Date: Fri, 5 May 2023 15:46:58 -0400
Subject: Organised repo

---
 CMakeLists.txt            |  20 +--
 ap_state.cpp              | 330 -------------------------------------
 ap_state.h                |  33 ----
 area_popup.cpp            |  54 ------
 area_popup.h              |  25 ---
 connection_dialog.cpp     |  40 -----
 connection_dialog.h       |  30 ----
 eye_indicator.cpp         |  49 ------
 eye_indicator.h           |  30 ----
 game_data.cpp             | 406 ----------------------------------------------
 game_data.h               | 135 ---------------
 main.cpp                  |  21 ---
 src/ap_state.cpp          | 330 +++++++++++++++++++++++++++++++++++++
 src/ap_state.h            |  33 ++++
 src/area_popup.cpp        |  54 ++++++
 src/area_popup.h          |  25 +++
 src/connection_dialog.cpp |  40 +++++
 src/connection_dialog.h   |  30 ++++
 src/eye_indicator.cpp     |  49 ++++++
 src/eye_indicator.h       |  30 ++++
 src/game_data.cpp         | 406 ++++++++++++++++++++++++++++++++++++++++++++++
 src/game_data.h           | 135 +++++++++++++++
 src/main.cpp              |  21 +++
 src/tracker_config.cpp    |  33 ++++
 src/tracker_config.h      |  19 +++
 src/tracker_frame.cpp     |  86 ++++++++++
 src/tracker_frame.h       |  33 ++++
 src/tracker_panel.cpp     | 149 +++++++++++++++++
 src/tracker_panel.h       |  39 +++++
 src/tracker_state.cpp     | 181 +++++++++++++++++++++
 src/tracker_state.h       |  19 +++
 tracker_config.cpp        |  33 ----
 tracker_config.h          |  19 ---
 tracker_frame.cpp         |  86 ----------
 tracker_frame.h           |  33 ----
 tracker_panel.cpp         | 149 -----------------
 tracker_panel.h           |  39 -----
 tracker_state.cpp         | 181 ---------------------
 tracker_state.h           |  19 ---
 39 files changed, 1722 insertions(+), 1722 deletions(-)
 delete mode 100644 ap_state.cpp
 delete mode 100644 ap_state.h
 delete mode 100644 area_popup.cpp
 delete mode 100644 area_popup.h
 delete mode 100644 connection_dialog.cpp
 delete mode 100644 connection_dialog.h
 delete mode 100644 eye_indicator.cpp
 delete mode 100644 eye_indicator.h
 delete mode 100644 game_data.cpp
 delete mode 100644 game_data.h
 delete mode 100644 main.cpp
 create mode 100644 src/ap_state.cpp
 create mode 100644 src/ap_state.h
 create mode 100644 src/area_popup.cpp
 create mode 100644 src/area_popup.h
 create mode 100644 src/connection_dialog.cpp
 create mode 100644 src/connection_dialog.h
 create mode 100644 src/eye_indicator.cpp
 create mode 100644 src/eye_indicator.h
 create mode 100644 src/game_data.cpp
 create mode 100644 src/game_data.h
 create mode 100644 src/main.cpp
 create mode 100644 src/tracker_config.cpp
 create mode 100644 src/tracker_config.h
 create mode 100644 src/tracker_frame.cpp
 create mode 100644 src/tracker_frame.h
 create mode 100644 src/tracker_panel.cpp
 create mode 100644 src/tracker_panel.h
 create mode 100644 src/tracker_state.cpp
 create mode 100644 src/tracker_state.h
 delete mode 100644 tracker_config.cpp
 delete mode 100644 tracker_config.h
 delete mode 100644 tracker_frame.cpp
 delete mode 100644 tracker_frame.h
 delete mode 100644 tracker_panel.cpp
 delete mode 100644 tracker_panel.h
 delete mode 100644 tracker_state.cpp
 delete mode 100644 tracker_state.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6e29d18..af3b0ba 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -30,16 +30,16 @@ include_directories(${SYSTEM_INCLUDE_DIR})
 link_directories(${openssl_LIBRARY_DIRS})
 
 add_executable(lingo_ap_tracker
-  main.cpp
-  tracker_frame.cpp
-  tracker_panel.cpp
-  game_data.cpp
-  area_popup.cpp
-  ap_state.cpp
-  connection_dialog.cpp
-  eye_indicator.cpp
-  tracker_state.cpp
-  tracker_config.cpp
+  "src/main.cpp"
+  "src/tracker_frame.cpp"
+  "src/tracker_panel.cpp"
+  "src/game_data.cpp"
+  "src/area_popup.cpp"
+  "src/ap_state.cpp"
+  "src/connection_dialog.cpp"
+  "src/eye_indicator.cpp"
+  "src/tracker_state.cpp"
+  "src/tracker_config.cpp"
 )
 set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20)
 set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON)
diff --git a/ap_state.cpp b/ap_state.cpp
deleted file mode 100644
index 22a51ee..0000000
--- a/ap_state.cpp
+++ /dev/null
@@ -1,330 +0,0 @@
-#include "ap_state.h"
-
-#define HAS_STD_FILESYSTEM
-#define _WEBSOCKETPP_CPP11_STRICT_
-#pragma comment(lib, "crypt32")
-
-#include <hkutil/string.h>
-
-#include <apclient.hpp>
-#include <apuuid.hpp>
-#include <chrono>
-#include <exception>
-#include <list>
-#include <memory>
-#include <mutex>
-#include <set>
-#include <thread>
-#include <tuple>
-
-#include "game_data.h"
-#include "tracker_frame.h"
-#include "tracker_state.h"
-
-constexpr int AP_MAJOR = 0;
-constexpr int AP_MINOR = 4;
-constexpr int AP_REVISION = 0;
-
-constexpr int ITEM_HANDLING = 7;  // <- all
-
-namespace {
-
-APClient* apclient = nullptr;
-
-bool initialized = false;
-
-TrackerFrame* tracker_frame;
-
-bool client_active = false;
-std::mutex client_mutex;
-
-bool connected = false;
-bool has_connection_result = false;
-
-std::map<int64_t, int> inventory;
-std::set<int64_t> checked_locations;
-
-std::map<std::tuple<int, int>, int64_t> ap_id_by_location_id;
-std::map<std::string, int64_t> ap_id_by_item_name;
-std::map<LingoColor, int64_t> ap_id_by_color;
-std::map<int64_t, std::string> progressive_item_by_ap_id;
-
-DoorShuffleMode door_shuffle_mode = kNO_DOORS;
-bool color_shuffle = false;
-bool painting_shuffle = false;
-int mastery_requirement = 21;
-
-std::map<std::string, std::string> painting_mapping;
-
-void RefreshTracker() {
-  GetTrackerState().CalculateState();
-  tracker_frame->UpdateIndicators();
-}
-
-int64_t GetItemId(const std::string& item_name) {
-  int64_t ap_id = apclient->get_item_id(item_name);
-  if (ap_id == APClient::INVALID_NAME_ID) {
-    std::cout << "Could not find AP item ID for " << item_name << std::endl;
-  }
-
-  return ap_id;
-}
-
-void DestroyClient() {
-  client_active = false;
-  apclient->reset();
-  delete apclient;
-  apclient = nullptr;
-}
-
-}  // namespace
-
-void AP_SetTrackerFrame(TrackerFrame* arg) { tracker_frame = arg; }
-
-void AP_Connect(std::string server, std::string player, std::string password) {
-  if (!initialized) {
-    std::thread([]() {
-      for (;;) {
-        {
-          std::lock_guard client_guard(client_mutex);
-          if (apclient) {
-            apclient->poll();
-          }
-        }
-
-        std::this_thread::sleep_for(std::chrono::milliseconds(100));
-      }
-    }).detach();
-
-    initialized = true;
-  }
-
-  tracker_frame->SetStatusMessage("Connecting to Archipelago server....");
-
-  {
-    std::lock_guard client_guard(client_mutex);
-
-    if (apclient) {
-      DestroyClient();
-    }
-
-    apclient = new APClient(ap_get_uuid(""), "Lingo", server);
-  }
-
-  inventory.clear();
-  checked_locations.clear();
-  door_shuffle_mode = kNO_DOORS;
-  color_shuffle = false;
-  painting_shuffle = false;
-  painting_mapping.clear();
-  mastery_requirement = 21;
-
-  connected = false;
-  has_connection_result = false;
-
-  apclient->set_room_info_handler([player, password]() {
-    inventory.clear();
-
-    tracker_frame->SetStatusMessage(
-        "Connected to Archipelago server. Authenticating...");
-
-    apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
-                          {AP_MAJOR, AP_MINOR, AP_REVISION});
-  });
-
-  apclient->set_location_checked_handler(
-      [](const std::list<int64_t>& locations) {
-        for (const int64_t location_id : locations) {
-          checked_locations.insert(location_id);
-          std::cout << "Location: " << location_id << std::endl;
-        }
-
-        RefreshTracker();
-      });
-
-  apclient->set_slot_disconnected_handler([]() {
-    tracker_frame->SetStatusMessage(
-        "Disconnected from Archipelago. Attempting to reconnect...");
-  });
-
-  apclient->set_socket_disconnected_handler([]() {
-    tracker_frame->SetStatusMessage(
-        "Disconnected from Archipelago. Attempting to reconnect...");
-  });
-
-  apclient->set_items_received_handler(
-      [](const std::list<APClient::NetworkItem>& items) {
-        for (const APClient::NetworkItem& item : items) {
-          inventory[item.item]++;
-          std::cout << "Item: " << item.item << std::endl;
-        }
-
-        RefreshTracker();
-      });
-
-  apclient->set_slot_connected_handler([](const nlohmann::json& slot_data) {
-    tracker_frame->SetStatusMessage("Connected to Archipelago!");
-
-    door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
-    color_shuffle = slot_data["shuffle_colors"].get<bool>();
-    painting_shuffle = slot_data["shuffle_paintings"].get<bool>();
-    mastery_requirement = slot_data["mastery_achievements"].get<int>();
-
-    if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) {
-      painting_mapping.clear();
-
-      for (const auto& mapping_it :
-           slot_data["painting_entrance_to_exit"].items()) {
-        painting_mapping[mapping_it.key()] = mapping_it.value();
-      }
-    }
-
-    connected = true;
-    has_connection_result = true;
-
-    RefreshTracker();
-  });
-
-  apclient->set_slot_refused_handler([](const std::list<std::string>& errors) {
-    connected = false;
-    has_connection_result = true;
-
-    tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
-
-    std::vector<std::string> error_messages;
-    error_messages.push_back("Could not connect to Archipelago.");
-
-    for (const std::string& error : errors) {
-      if (error == "InvalidSlot") {
-        error_messages.push_back("Invalid player name.");
-      } else if (error == "InvalidGame") {
-        error_messages.push_back("The specified player is not playing Lingo.");
-      } else if (error == "IncompatibleVersion") {
-        error_messages.push_back(
-            "The Archipelago server is not the correct version for this "
-            "client.");
-      } else if (error == "InvalidPassword") {
-        error_messages.push_back("Incorrect password.");
-      } else if (error == "InvalidItemsHandling") {
-        error_messages.push_back(
-            "Invalid item handling flag. This is a bug with the tracker. "
-            "Please report it to the lingo-ap-tracker GitHub.");
-      } else {
-        error_messages.push_back("Unknown error.");
-      }
-    }
-
-    std::string full_message = hatkirby::implode(error_messages, " ");
-
-    wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
-  });
-
-  client_active = true;
-
-  int timeout = 5000;  // 5 seconds
-  int interval = 100;
-  int remaining_loops = timeout / interval;
-  while (!has_connection_result) {
-    if (interval == 0) {
-      connected = false;
-      has_connection_result = true;
-
-      DestroyClient();
-
-      tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
-
-      wxMessageBox("Timeout while connecting to Archipelago server.",
-                   "Connection failed", wxOK | wxICON_ERROR);
-    }
-
-    std::this_thread::sleep_for(std::chrono::milliseconds(100));
-
-    interval--;
-  }
-
-  if (connected) {
-    for (const MapArea& map_area : GetGameData().GetMapAreas()) {
-      for (int section_id = 0; section_id < map_area.locations.size();
-           section_id++) {
-        const Location& location = map_area.locations.at(section_id);
-
-        int64_t ap_id = apclient->get_location_id(location.ap_location_name);
-        if (ap_id == APClient::INVALID_NAME_ID) {
-          std::cout << "Could not find AP location ID for "
-                    << location.ap_location_name << std::endl;
-        } else {
-          ap_id_by_location_id[{map_area.id, section_id}] = ap_id;
-        }
-      }
-    }
-
-    for (const Door& door : GetGameData().GetDoors()) {
-      if (!door.skip_item) {
-        ap_id_by_item_name[door.item_name] = GetItemId(door.item_name);
-
-        if (!door.group_name.empty() &&
-            !ap_id_by_item_name.count(door.group_name)) {
-          ap_id_by_item_name[door.group_name] = GetItemId(door.group_name);
-        }
-
-        for (const ProgressiveRequirement& prog_req : door.progressives) {
-          ap_id_by_item_name[prog_req.item_name] =
-              GetItemId(prog_req.item_name);
-        }
-      }
-    }
-
-    ap_id_by_color[LingoColor::kBlack] = GetItemId("Black");
-    ap_id_by_color[LingoColor::kRed] = GetItemId("Red");
-    ap_id_by_color[LingoColor::kBlue] = GetItemId("Blue");
-    ap_id_by_color[LingoColor::kYellow] = GetItemId("Yellow");
-    ap_id_by_color[LingoColor::kPurple] = GetItemId("Purple");
-    ap_id_by_color[LingoColor::kOrange] = GetItemId("Orange");
-    ap_id_by_color[LingoColor::kGreen] = GetItemId("Green");
-    ap_id_by_color[LingoColor::kBrown] = GetItemId("Brown");
-    ap_id_by_color[LingoColor::kGray] = GetItemId("Gray");
-
-    RefreshTracker();
-  } else {
-    client_active = false;
-  }
-}
-
-bool AP_HasCheckedGameLocation(int area_id, int section_id) {
-  std::tuple<int, int> location_key = {area_id, section_id};
-
-  if (ap_id_by_location_id.count(location_key)) {
-    return checked_locations.count(ap_id_by_location_id.at(location_key));
-  } else {
-    return false;
-  }
-}
-
-bool AP_HasColorItem(LingoColor color) {
-  if (ap_id_by_color.count(color)) {
-    return inventory.count(ap_id_by_color.at(color));
-  } else {
-    return false;
-  }
-}
-
-bool AP_HasItem(const std::string& item, int quantity) {
-  if (ap_id_by_item_name.count(item)) {
-    int64_t ap_id = ap_id_by_item_name.at(item);
-    return inventory.count(ap_id) && inventory.at(ap_id) >= quantity;
-  } else {
-    return false;
-  }
-}
-
-DoorShuffleMode AP_GetDoorShuffleMode() { return door_shuffle_mode; }
-
-bool AP_IsColorShuffle() { return color_shuffle; }
-
-bool AP_IsPaintingShuffle() { return painting_shuffle; }
-
-const std::map<std::string, std::string> AP_GetPaintingMapping() {
-  return painting_mapping;
-}
-
-int AP_GetMasteryRequirement() { return mastery_requirement; }
diff --git a/ap_state.h b/ap_state.h
deleted file mode 100644
index d880c71..0000000
--- a/ap_state.h
+++ /dev/null
@@ -1,33 +0,0 @@
-#ifndef AP_STATE_H_664A4180
-#define AP_STATE_H_664A4180
-
-#include <map>
-#include <string>
-
-#include "game_data.h"
-
-class TrackerFrame;
-
-enum DoorShuffleMode { kNO_DOORS = 0, kSIMPLE_DOORS = 1, kCOMPLEX_DOORS = 2 };
-
-void AP_SetTrackerFrame(TrackerFrame* tracker_frame);
-
-void AP_Connect(std::string server, std::string player, std::string password);
-
-bool AP_HasCheckedGameLocation(int area_id, int section_id);
-
-bool AP_HasColorItem(LingoColor color);
-
-bool AP_HasItem(const std::string& item, int quantity = 1);
-
-DoorShuffleMode AP_GetDoorShuffleMode();
-
-bool AP_IsColorShuffle();
-
-bool AP_IsPaintingShuffle();
-
-const std::map<std::string, std::string> AP_GetPaintingMapping();
-
-int AP_GetMasteryRequirement();
-
-#endif /* end of include guard: AP_STATE_H_664A4180 */
diff --git a/area_popup.cpp b/area_popup.cpp
deleted file mode 100644
index a8ff612..0000000
--- a/area_popup.cpp
+++ /dev/null
@@ -1,54 +0,0 @@
-#include "area_popup.h"
-
-#include "ap_state.h"
-#include "game_data.h"
-#include "tracker_state.h"
-
-AreaPopup::AreaPopup(wxWindow* parent, int area_id)
-    : wxPanel(parent, wxID_ANY), area_id_(area_id) {
-  const MapArea& map_area = GetGameData().GetMapArea(area_id);
-
-  wxFlexGridSizer* section_sizer = new wxFlexGridSizer(2, 10, 10);
-
-  for (const Location& location : map_area.locations) {
-    EyeIndicator* eye_indicator = new EyeIndicator(this);
-    section_sizer->Add(eye_indicator, wxSizerFlags().Expand());
-    eye_indicators_.push_back(eye_indicator);
-
-    wxStaticText* section_label = new wxStaticText(this, -1, location.name);
-    section_label->SetForegroundColour(*wxWHITE);
-    section_sizer->Add(
-        section_label,
-        wxSizerFlags().Align(wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL));
-    section_labels_.push_back(section_label);
-  }
-
-  wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
-
-  wxStaticText* top_label = new wxStaticText(this, -1, map_area.name);
-  top_label->SetForegroundColour(*wxWHITE);
-  top_label->SetFont(top_label->GetFont().Bold());
-  top_sizer->Add(top_label,
-                 wxSizerFlags().Center().DoubleBorder(wxUP | wxLEFT | wxRIGHT));
-
-  top_sizer->Add(section_sizer, wxSizerFlags().DoubleBorder(wxALL).Expand());
-
-  SetSizerAndFit(top_sizer);
-
-  SetBackgroundColour(*wxBLACK);
-  Hide();
-}
-
-void AreaPopup::UpdateIndicators() {
-  const MapArea& map_area = GetGameData().GetMapArea(area_id_);
-  for (int section_id = 0; section_id < map_area.locations.size();
-       section_id++) {
-    bool checked = AP_HasCheckedGameLocation(area_id_, section_id);
-    bool reachable =
-        GetTrackerState().IsLocationReachable(area_id_, section_id);
-    const wxColour* text_color = reachable ? wxWHITE : wxRED;
-
-    section_labels_[section_id]->SetForegroundColour(*text_color);
-    eye_indicators_[section_id]->SetChecked(checked);
-  }
-}
diff --git a/area_popup.h b/area_popup.h
deleted file mode 100644
index b602b63..0000000
--- a/area_popup.h
+++ /dev/null
@@ -1,25 +0,0 @@
-#ifndef AREA_POPUP_H_03FAC988
-#define AREA_POPUP_H_03FAC988
-
-#include <wx/wxprec.h>
-
-#ifndef WX_PRECOMP
-#include <wx/wx.h>
-#endif
-
-#include "eye_indicator.h"
-
-class AreaPopup : public wxPanel {
- public:
-  AreaPopup(wxWindow* parent, int area_id);
-
-  void UpdateIndicators();
-
- private:
-  int area_id_;
-
-  std::vector<wxStaticText*> section_labels_;
-  std::vector<EyeIndicator*> eye_indicators_;
-};
-
-#endif /* end of include guard: AREA_POPUP_H_03FAC988 */
diff --git a/connection_dialog.cpp b/connection_dialog.cpp
deleted file mode 100644
index 9dd9984..0000000
--- a/connection_dialog.cpp
+++ /dev/null
@@ -1,40 +0,0 @@
-#include "connection_dialog.h"
-
-#include "tracker_config.h"
-
-ConnectionDialog::ConnectionDialog()
-    : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") {
-  server_box_ = new wxTextCtrl(this, -1, GetTrackerConfig().ap_server, wxDefaultPosition,
-                               {300, -1});
-  player_box_ = new wxTextCtrl(this, -1, GetTrackerConfig().ap_player, wxDefaultPosition,
-                               {300, -1});
-  password_box_ = new wxTextCtrl(this, -1, GetTrackerConfig().ap_password,
-                                 wxDefaultPosition, {300, -1});
-
-  wxFlexGridSizer* form_sizer = new wxFlexGridSizer(2, 10, 10);
-
-  form_sizer->Add(
-      new wxStaticText(this, -1, "Server:"),
-      wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
-  form_sizer->Add(server_box_, wxSizerFlags().Expand());
-  form_sizer->Add(
-      new wxStaticText(this, -1, "Player:"),
-      wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
-  form_sizer->Add(player_box_, wxSizerFlags().Expand());
-  form_sizer->Add(
-      new wxStaticText(this, -1, "Password:"),
-      wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
-  form_sizer->Add(password_box_, wxSizerFlags().Expand());
-
-  wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
-  top_sizer->Add(new wxStaticText(
-                     this, -1, "Enter the details to connect to Archipelago."),
-                 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder());
-  top_sizer->Add(form_sizer, wxSizerFlags().DoubleBorder().Expand());
-  top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Center());
-
-  SetSizerAndFit(top_sizer);
-
-  Center();
-  server_box_->SetFocus();
-}
diff --git a/connection_dialog.h b/connection_dialog.h
deleted file mode 100644
index 5d2703d..0000000
--- a/connection_dialog.h
+++ /dev/null
@@ -1,30 +0,0 @@
-#ifndef CONNECTION_DIALOG_H_E526D0E7
-#define CONNECTION_DIALOG_H_E526D0E7
-
-#include <wx/wxprec.h>
-
-#ifndef WX_PRECOMP
-#include <wx/wx.h>
-#endif
-
-#include <string>
-
-class ConnectionDialog : public wxDialog {
- public:
-  ConnectionDialog();
-
-  std::string GetServerValue() { return server_box_->GetValue().ToStdString(); }
-
-  std::string GetPlayerValue() { return player_box_->GetValue().ToStdString(); }
-
-  std::string GetPasswordValue() {
-    return password_box_->GetValue().ToStdString();
-  }
-
- private:
-  wxTextCtrl* server_box_;
-  wxTextCtrl* player_box_;
-  wxTextCtrl* password_box_;
-};
-
-#endif /* end of include guard: CONNECTION_DIALOG_H_E526D0E7 */
diff --git a/eye_indicator.cpp b/eye_indicator.cpp
deleted file mode 100644
index 03e234e..0000000
--- a/eye_indicator.cpp
+++ /dev/null
@@ -1,49 +0,0 @@
-#include "eye_indicator.h"
-
-EyeIndicator::EyeIndicator(wxWindow* parent) : wxWindow(parent, wxID_ANY) {
-  SetMinSize({32, 32});
-
-  Redraw();
-
-  Bind(wxEVT_PAINT, &EyeIndicator::OnPaint, this);
-}
-
-void EyeIndicator::SetChecked(bool checked) {
-  if (intended_checked_ != checked) {
-    intended_checked_ = checked;
-
-    Redraw();
-  }
-}
-
-const wxImage& EyeIndicator::GetUncheckedImage() {
-  static wxImage* unchecked_image =
-      new wxImage("assets/unchecked.png", wxBITMAP_TYPE_PNG);
-  return *unchecked_image;
-}
-
-const wxImage& EyeIndicator::GetCheckedImage() {
-  static wxImage* checked_image =
-      new wxImage("assets/checked.png", wxBITMAP_TYPE_PNG);
-  return *checked_image;
-}
-
-void EyeIndicator::OnPaint(wxPaintEvent& event) {
-  if (GetSize() != rendered_.GetSize() ||
-      intended_checked_ != rendered_checked_) {
-    Redraw();
-  }
-
-  wxPaintDC dc(this);
-  dc.DrawBitmap(rendered_, 0, 0);
-
-  event.Skip();
-}
-
-void EyeIndicator::Redraw() {
-  rendered_ =
-      wxBitmap((intended_checked_ ? GetCheckedImage() : GetUncheckedImage())
-                   .Scale(GetSize().GetWidth(), GetSize().GetHeight(),
-                          wxIMAGE_QUALITY_NORMAL));
-  rendered_checked_ = intended_checked_;
-}
diff --git a/eye_indicator.h b/eye_indicator.h
deleted file mode 100644
index e8fd890..0000000
--- a/eye_indicator.h
+++ /dev/null
@@ -1,30 +0,0 @@
-#ifndef EYE_INDICATOR_H_778150F2
-#define EYE_INDICATOR_H_778150F2
-
-#include <wx/wxprec.h>
-
-#ifndef WX_PRECOMP
-#include <wx/wx.h>
-#endif
-
-class EyeIndicator : public wxWindow {
- public:
-  EyeIndicator(wxWindow* parent);
-
-  void SetChecked(bool checked);
-
- private:
-  static const wxImage& GetUncheckedImage();
-  static const wxImage& GetCheckedImage();
-
-  void OnPaint(wxPaintEvent& event);
-
-  void Redraw();
-
-  bool intended_checked_ = false;
-
-  wxBitmap rendered_;
-  bool rendered_checked_ = false;
-};
-
-#endif /* end of include guard: EYE_INDICATOR_H_778150F2 */
diff --git a/game_data.cpp b/game_data.cpp
deleted file mode 100644
index e15847e..0000000
--- a/game_data.cpp
+++ /dev/null
@@ -1,406 +0,0 @@
-#include "game_data.h"
-
-#include <hkutil/string.h>
-#include <yaml-cpp/yaml.h>
-
-#include <iostream>
-
-LingoColor GetColorForString(const std::string &str) {
-  if (str == "black") {
-    return LingoColor::kBlack;
-  } else if (str == "red") {
-    return LingoColor::kRed;
-  } else if (str == "blue") {
-    return LingoColor::kBlue;
-  } else if (str == "yellow") {
-    return LingoColor::kYellow;
-  } else if (str == "orange") {
-    return LingoColor::kOrange;
-  } else if (str == "green") {
-    return LingoColor::kGreen;
-  } else if (str == "gray") {
-    return LingoColor::kGray;
-  } else if (str == "brown") {
-    return LingoColor::kBrown;
-  } else if (str == "purple") {
-    return LingoColor::kPurple;
-  } else {
-    std::cout << "Invalid color: " << str << std::endl;
-    return LingoColor::kNone;
-  }
-}
-
-GameData::GameData() {
-  YAML::Node lingo_config = YAML::LoadFile("assets/LL1.yaml");
-  YAML::Node areas_config = YAML::LoadFile("assets/areas.yaml");
-
-  rooms_.reserve(lingo_config.size() * 2);
-
-  for (const auto &room_it : lingo_config) {
-    int room_id = AddOrGetRoom(room_it.first.as<std::string>());
-    Room &room_obj = rooms_[room_id];
-
-    for (const auto &entrance_it : room_it.second["entrances"]) {
-      int from_room_id = AddOrGetRoom(entrance_it.first.as<std::string>());
-      Room &from_room_obj = rooms_[from_room_id];
-
-      switch (entrance_it.second.Type()) {
-        case YAML::NodeType::Scalar: {
-          // This is just "true".
-          from_room_obj.exits.push_back({.destination_room = room_id});
-          break;
-        }
-        case YAML::NodeType::Map: {
-          Exit exit_obj;
-          exit_obj.destination_room = room_id;
-
-          if (entrance_it.second["door"]) {
-            std::string door_room = room_obj.name;
-            if (entrance_it.second["room"]) {
-              door_room = entrance_it.second["room"].as<std::string>();
-            }
-            exit_obj.door = AddOrGetDoor(
-                door_room, entrance_it.second["door"].as<std::string>());
-          }
-
-          if (entrance_it.second["painting"]) {
-            exit_obj.painting = entrance_it.second["painting"].as<bool>();
-          }
-
-          from_room_obj.exits.push_back(exit_obj);
-          break;
-        }
-        case YAML::NodeType::Sequence: {
-          for (const auto &option : entrance_it.second) {
-            Exit exit_obj;
-            exit_obj.destination_room = room_id;
-
-            std::string door_room = room_obj.name;
-            if (option["room"]) {
-              door_room = option["room"].as<std::string>();
-            }
-            exit_obj.door =
-                AddOrGetDoor(door_room, option["door"].as<std::string>());
-
-            if (option["painting"]) {
-              exit_obj.painting = option["painting"].as<bool>();
-            }
-
-            from_room_obj.exits.push_back(exit_obj);
-          }
-
-          break;
-        }
-        default: {
-          // This shouldn't happen.
-          std::cout << "Error reading game data: " << entrance_it << std::endl;
-          break;
-        }
-      }
-    }
-
-    if (room_it.second["panels"]) {
-      for (const auto &panel_it : room_it.second["panels"]) {
-        int panel_id =
-            AddOrGetPanel(room_obj.name, panel_it.first.as<std::string>());
-        Panel &panel_obj = panels_[panel_id];
-
-        if (panel_it.second["colors"]) {
-          if (panel_it.second["colors"].IsScalar()) {
-            panel_obj.colors.push_back(
-                GetColorForString(panel_it.second["colors"].as<std::string>()));
-          } else {
-            for (const auto &color_node : panel_it.second["colors"]) {
-              panel_obj.colors.push_back(
-                  GetColorForString(color_node.as<std::string>()));
-            }
-          }
-        }
-
-        if (panel_it.second["required_room"]) {
-          if (panel_it.second["required_room"].IsScalar()) {
-            panel_obj.required_rooms.push_back(AddOrGetRoom(
-                panel_it.second["required_room"].as<std::string>()));
-          } else {
-            for (const auto &rr_node : panel_it.second["required_room"]) {
-              panel_obj.required_rooms.push_back(
-                  AddOrGetRoom(rr_node.as<std::string>()));
-            }
-          }
-        }
-
-        if (panel_it.second["required_door"]) {
-          if (panel_it.second["required_door"].IsMap()) {
-            std::string rd_room = room_obj.name;
-            if (panel_it.second["required_door"]["room"]) {
-              rd_room =
-                  panel_it.second["required_door"]["room"].as<std::string>();
-            }
-
-            panel_obj.required_doors.push_back(AddOrGetDoor(
-                rd_room,
-                panel_it.second["required_door"]["door"].as<std::string>()));
-          } else {
-            for (const auto &rr_node : panel_it.second["required_door"]) {
-              std::string rd_room = room_obj.name;
-              if (rr_node["room"]) {
-                rd_room = rr_node["room"].as<std::string>();
-              }
-
-              panel_obj.required_doors.push_back(
-                  AddOrGetDoor(rd_room, rr_node["door"].as<std::string>()));
-            }
-          }
-        }
-
-        if (panel_it.second["check"]) {
-          panel_obj.check = panel_it.second["check"].as<bool>();
-        }
-
-        if (panel_it.second["achievement"]) {
-          panel_obj.achievement = panel_it.second["achievement"].as<bool>();
-
-          if (panel_obj.achievement) {
-            achievement_panels_.push_back(panel_id);
-          }
-        }
-
-        if (panel_it.second["exclude_reduce"]) {
-          panel_obj.exclude_reduce =
-              panel_it.second["exclude_reduce"].as<bool>();
-        }
-      }
-    }
-
-    if (room_it.second["doors"]) {
-      for (const auto &door_it : room_it.second["doors"]) {
-        int door_id =
-            AddOrGetDoor(room_obj.name, door_it.first.as<std::string>());
-        Door &door_obj = doors_[door_id];
-
-        bool has_external_panels = false;
-        std::vector<std::string> panel_names;
-
-        for (const auto &panel_node : door_it.second["panels"]) {
-          if (panel_node.IsScalar()) {
-            panel_names.push_back(panel_node.as<std::string>());
-            door_obj.panels.push_back(
-                AddOrGetPanel(room_obj.name, panel_node.as<std::string>()));
-          } else {
-            has_external_panels = true;
-            panel_names.push_back(panel_node["panel"].as<std::string>());
-            door_obj.panels.push_back(
-                AddOrGetPanel(panel_node["room"].as<std::string>(),
-                              panel_node["panel"].as<std::string>()));
-          }
-        }
-
-        if (door_it.second["skip_location"]) {
-          door_obj.skip_location = door_it.second["skip_location"].as<bool>();
-        }
-
-        if (door_it.second["skip_item"]) {
-          door_obj.skip_item = door_it.second["skip_item"].as<bool>();
-        }
-
-        if (door_it.second["event"]) {
-          door_obj.skip_location = door_it.second["event"].as<bool>();
-          door_obj.skip_item = door_it.second["event"].as<bool>();
-        }
-
-        if (door_it.second["item_name"]) {
-          door_obj.item_name = door_it.second["item_name"].as<std::string>();
-        } else if (!door_it.second["skip_item"] && !door_it.second["event"]) {
-          door_obj.item_name = room_obj.name + " - " + door_obj.name;
-        }
-
-        if (door_it.second["group"]) {
-          door_obj.group_name = door_it.second["group"].as<std::string>();
-        }
-
-        if (door_it.second["location_name"]) {
-          door_obj.location_name =
-              door_it.second["location_name"].as<std::string>();
-        } else if (!door_it.second["skip_location"] &&
-                   !door_it.second["event"]) {
-          if (has_external_panels) {
-            std::cout
-                << room_obj.name << " - " << door_obj.name
-                << " has panels from other rooms but does not have an explicit "
-                   "location name and is not marked skip_location or event"
-                << std::endl;
-          }
-
-          door_obj.location_name =
-              room_obj.name + " - " + hatkirby::implode(panel_names, ", ");
-        }
-
-        if (door_it.second["include_reduce"]) {
-          door_obj.exclude_reduce =
-              !door_it.second["include_reduce"].as<bool>();
-        }
-      }
-    }
-
-    if (room_it.second["paintings"]) {
-      for (const auto &painting : room_it.second["paintings"]) {
-        std::string painting_id = painting["id"].as<std::string>();
-        room_by_painting_[painting_id] = room_id;
-
-        if (!painting["exit_only"] || !painting["exit_only"].as<bool>()) {
-          PaintingExit painting_exit;
-          painting_exit.id = painting_id;
-
-          if (painting["required_door"]) {
-            std::string rd_room = room_obj.name;
-            if (painting["required_door"]["room"]) {
-              rd_room = painting["required_door"]["room"].as<std::string>();
-            }
-
-            painting_exit.door = AddOrGetDoor(
-                rd_room, painting["required_door"]["door"].as<std::string>());
-          }
-
-          room_obj.paintings.push_back(painting_exit);
-        }
-      }
-    }
-
-    if (room_it.second["progression"]) {
-      for (const auto &progression_it : room_it.second["progression"]) {
-        std::string progressive_item_name =
-            progression_it.first.as<std::string>();
-
-        int index = 1;
-        for (const auto &stage : progression_it.second) {
-          int door_id = -1;
-
-          if (stage.IsScalar()) {
-            door_id = AddOrGetDoor(room_obj.name, stage.as<std::string>());
-          } else {
-            door_id = AddOrGetDoor(stage["room"].as<std::string>(),
-                                   stage["door"].as<std::string>());
-          }
-
-          doors_[door_id].progressives.push_back(
-              {.item_name = progressive_item_name, .quantity = index});
-          index++;
-        }
-      }
-    }
-  }
-
-  map_areas_.reserve(areas_config.size());
-
-  std::map<std::string, int> fold_areas;
-  for (const auto &area_it : areas_config) {
-    if (area_it.second["map"]) {
-      int area_id = AddOrGetArea(area_it.first.as<std::string>());
-      MapArea &area_obj = map_areas_[area_id];
-      area_obj.map_x = area_it.second["map"][0].as<int>();
-      area_obj.map_y = area_it.second["map"][1].as<int>();
-    } else if (area_it.second["fold_into"]) {
-      fold_areas[area_it.first.as<std::string>()] =
-          AddOrGetArea(area_it.second["fold_into"].as<std::string>());
-    }
-  }
-
-  for (const Panel &panel : panels_) {
-    if (panel.check) {
-      int room_id = panel.room;
-      std::string room_name = rooms_[room_id].name;
-
-      std::string area_name = room_name;
-      if (fold_areas.count(room_name)) {
-        int fold_area_id = fold_areas[room_name];
-        area_name = map_areas_[fold_area_id].name;
-      }
-
-      int area_id = AddOrGetArea(area_name);
-      MapArea &map_area = map_areas_[area_id];
-      // room field should be the original room ID
-      map_area.locations.push_back(
-          {.name = panel.name,
-           .ap_location_name = room_name + " - " + panel.name,
-           .room = panel.room,
-           .panels = {panel.id}});
-    }
-  }
-
-  for (const Door &door : doors_) {
-    if (!door.skip_location) {
-      int room_id = door.room;
-      std::string area_name = rooms_[room_id].name;
-      std::string section_name;
-
-      size_t divider_pos = door.location_name.find(" - ");
-      if (divider_pos == std::string::npos) {
-        section_name = door.location_name;
-      } else {
-        area_name = door.location_name.substr(0, divider_pos);
-        section_name = door.location_name.substr(divider_pos + 3);
-      }
-
-      if (fold_areas.count(area_name)) {
-        int fold_area_id = fold_areas[area_name];
-        area_name = map_areas_[fold_area_id].name;
-      }
-
-      int area_id = AddOrGetArea(area_name);
-      MapArea &map_area = map_areas_[area_id];
-      // room field should be the original room ID
-      map_area.locations.push_back({.name = section_name,
-                                    .ap_location_name = door.location_name,
-                                    .room = door.room,
-                                    .panels = door.panels});
-    }
-  }
-}
-
-int GameData::AddOrGetRoom(std::string room) {
-  if (!room_by_id_.count(room)) {
-    room_by_id_[room] = rooms_.size();
-    rooms_.push_back({.name = room});
-  }
-
-  return room_by_id_[room];
-}
-
-int GameData::AddOrGetDoor(std::string room, std::string door) {
-  std::string full_name = room + " - " + door;
-
-  if (!door_by_id_.count(full_name)) {
-    door_by_id_[full_name] = doors_.size();
-    doors_.push_back({.room = AddOrGetRoom(room), .name = door});
-  }
-
-  return door_by_id_[full_name];
-}
-
-int GameData::AddOrGetPanel(std::string room, std::string panel) {
-  std::string full_name = room + " - " + panel;
-
-  if (!panel_by_id_.count(full_name)) {
-    int panel_id = panels_.size();
-    panel_by_id_[full_name] = panel_id;
-    panels_.push_back(
-        {.id = panel_id, .room = AddOrGetRoom(room), .name = panel});
-  }
-
-  return panel_by_id_[full_name];
-}
-
-int GameData::AddOrGetArea(std::string area) {
-  if (!area_by_id_.count(area)) {
-    int area_id = map_areas_.size();
-    area_by_id_[area] = area_id;
-    map_areas_.push_back({.id = area_id, .name = area});
-  }
-
-  return area_by_id_[area];
-}
-
-const GameData &GetGameData() {
-  static GameData *instance = new GameData();
-  return *instance;
-}
diff --git a/game_data.h b/game_data.h
deleted file mode 100644
index 0cc7a7b..0000000
--- a/game_data.h
+++ /dev/null
@@ -1,135 +0,0 @@
-#ifndef GAME_DATA_H_9C42AC51
-#define GAME_DATA_H_9C42AC51
-
-#include <map>
-#include <optional>
-#include <string>
-#include <vector>
-
-enum class LingoColor {
-  kNone,
-  kBlack,
-  kRed,
-  kBlue,
-  kYellow,
-  kGreen,
-  kOrange,
-  kPurple,
-  kBrown,
-  kGray
-};
-
-struct Panel {
-  int id;
-  int room;
-  std::string name;
-  std::vector<LingoColor> colors;
-  std::vector<int> required_rooms;
-  std::vector<int> required_doors;
-  bool check = false;
-  bool exclude_reduce = false;
-  bool achievement = false;
-};
-
-struct ProgressiveRequirement {
-  std::string item_name;
-  int quantity = 0;
-};
-
-struct Door {
-  int room;
-  std::string name;
-  std::string location_name;
-  std::string item_name;
-  std::string group_name;
-  bool skip_location = false;
-  bool skip_item = false;
-  std::vector<int> panels;
-  bool exclude_reduce = true;
-  std::vector<ProgressiveRequirement> progressives;
-};
-
-struct Exit {
-  int destination_room;
-  std::optional<int> door;
-  bool painting = false;
-};
-
-struct PaintingExit {
-  std::string id;
-  std::optional<int> door;
-};
-
-struct Room {
-  std::string name;
-  std::vector<Exit> exits;
-  std::vector<PaintingExit> paintings;
-};
-
-struct Location {
-  std::string name;
-  std::string ap_location_name;
-  int room;
-  std::vector<int> panels;
-};
-
-struct MapArea {
-  int id;
-  std::string name;
-  std::vector<Location> locations;
-  int map_x;
-  int map_y;
-};
-
-class GameData {
- public:
-  GameData();
-
-  const std::vector<MapArea>& GetMapAreas() const { return map_areas_; }
-
-  const MapArea& GetMapArea(int id) const { return map_areas_.at(id); }
-
-  int GetRoomByName(const std::string& name) const {
-    return room_by_id_.at(name);
-  }
-
-  const Room& GetRoom(int room_id) const { return rooms_.at(room_id); }
-
-  const std::vector<Door>& GetDoors() const { return doors_; }
-
-  const Door& GetDoor(int door_id) const { return doors_.at(door_id); }
-
-  const Panel& GetPanel(int panel_id) const { return panels_.at(panel_id); }
-
-  int GetRoomForPainting(const std::string& painting_id) const {
-    return room_by_painting_.at(painting_id);
-  }
-
-  const std::vector<int>& GetAchievementPanels() const {
-    return achievement_panels_;
-  }
-
- private:
-  int AddOrGetRoom(std::string room);
-  int AddOrGetDoor(std::string room, std::string door);
-  int AddOrGetPanel(std::string room, std::string panel);
-  int AddOrGetArea(std::string area);
-
-  std::vector<Room> rooms_;
-  std::vector<Door> doors_;
-  std::vector<Panel> panels_;
-  std::vector<MapArea> map_areas_;
-
-  std::map<std::string, int> room_by_id_;
-  std::map<std::string, int> door_by_id_;
-  std::map<std::string, int> panel_by_id_;
-  std::map<std::string, int> area_by_id_;
-
-  std::map<std::string, int> room_by_painting_;
-
-  std::vector<int> achievement_panels_;
-};
-
-const GameData& GetGameData();
-
-#endif /* end of include guard: GAME_DATA_H_9C42AC51 */
diff --git a/main.cpp b/main.cpp
deleted file mode 100644
index fe9aceb..0000000
--- a/main.cpp
+++ /dev/null
@@ -1,21 +0,0 @@
-#include <wx/wxprec.h>
-
-#ifndef WX_PRECOMP
-#include <wx/wx.h>
-#endif
-
-#include "tracker_config.h"
-#include "tracker_frame.h"
-
-class TrackerApp : public wxApp {
- public:
-  virtual bool OnInit() {
-    GetTrackerConfig().Load();
-
-    TrackerFrame *frame = new TrackerFrame();
-    frame->Show(true);
-    return true;
-  }
-};
-
-wxIMPLEMENT_APP(TrackerApp);
diff --git a/src/ap_state.cpp b/src/ap_state.cpp
new file mode 100644
index 0000000..22a51ee
--- /dev/null
+++ b/src/ap_state.cpp
@@ -0,0 +1,330 @@
+#include "ap_state.h"
+
+#define HAS_STD_FILESYSTEM
+#define _WEBSOCKETPP_CPP11_STRICT_
+#pragma comment(lib, "crypt32")
+
+#include <hkutil/string.h>
+
+#include <apclient.hpp>
+#include <apuuid.hpp>
+#include <chrono>
+#include <exception>
+#include <list>
+#include <memory>
+#include <mutex>
+#include <set>
+#include <thread>
+#include <tuple>
+
+#include "game_data.h"
+#include "tracker_frame.h"
+#include "tracker_state.h"
+
+constexpr int AP_MAJOR = 0;
+constexpr int AP_MINOR = 4;
+constexpr int AP_REVISION = 0;
+
+constexpr int ITEM_HANDLING = 7;  // <- all
+
+namespace {
+
+APClient* apclient = nullptr;
+
+bool initialized = false;
+
+TrackerFrame* tracker_frame;
+
+bool client_active = false;
+std::mutex client_mutex;
+
+bool connected = false;
+bool has_connection_result = false;
+
+std::map<int64_t, int> inventory;
+std::set<int64_t> checked_locations;
+
+std::map<std::tuple<int, int>, int64_t> ap_id_by_location_id;
+std::map<std::string, int64_t> ap_id_by_item_name;
+std::map<LingoColor, int64_t> ap_id_by_color;
+std::map<int64_t, std::string> progressive_item_by_ap_id;
+
+DoorShuffleMode door_shuffle_mode = kNO_DOORS;
+bool color_shuffle = false;
+bool painting_shuffle = false;
+int mastery_requirement = 21;
+
+std::map<std::string, std::string> painting_mapping;
+
+void RefreshTracker() {
+  GetTrackerState().CalculateState();
+  tracker_frame->UpdateIndicators();
+}
+
+int64_t GetItemId(const std::string& item_name) {
+  int64_t ap_id = apclient->get_item_id(item_name);
+  if (ap_id == APClient::INVALID_NAME_ID) {
+    std::cout << "Could not find AP item ID for " << item_name << std::endl;
+  }
+
+  return ap_id;
+}
+
+void DestroyClient() {
+  client_active = false;
+  apclient->reset();
+  delete apclient;
+  apclient = nullptr;
+}
+
+}  // namespace
+
+void AP_SetTrackerFrame(TrackerFrame* arg) { tracker_frame = arg; }
+
+void AP_Connect(std::string server, std::string player, std::string password) {
+  if (!initialized) {
+    std::thread([]() {
+      for (;;) {
+        {
+          std::lock_guard client_guard(client_mutex);
+          if (apclient) {
+            apclient->poll();
+          }
+        }
+
+        std::this_thread::sleep_for(std::chrono::milliseconds(100));
+      }
+    }).detach();
+
+    initialized = true;
+  }
+
+  tracker_frame->SetStatusMessage("Connecting to Archipelago server....");
+
+  {
+    std::lock_guard client_guard(client_mutex);
+
+    if (apclient) {
+      DestroyClient();
+    }
+
+    apclient = new APClient(ap_get_uuid(""), "Lingo", server);
+  }
+
+  inventory.clear();
+  checked_locations.clear();
+  door_shuffle_mode = kNO_DOORS;
+  color_shuffle = false;
+  painting_shuffle = false;
+  painting_mapping.clear();
+  mastery_requirement = 21;
+
+  connected = false;
+  has_connection_result = false;
+
+  apclient->set_room_info_handler([player, password]() {
+    inventory.clear();
+
+    tracker_frame->SetStatusMessage(
+        "Connected to Archipelago server. Authenticating...");
+
+    apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
+                          {AP_MAJOR, AP_MINOR, AP_REVISION});
+  });
+
+  apclient->set_location_checked_handler(
+      [](const std::list<int64_t>& locations) {
+        for (const int64_t location_id : locations) {
+          checked_locations.insert(location_id);
+          std::cout << "Location: " << location_id << std::endl;
+        }
+
+        RefreshTracker();
+      });
+
+  apclient->set_slot_disconnected_handler([]() {
+    tracker_frame->SetStatusMessage(
+        "Disconnected from Archipelago. Attempting to reconnect...");
+  });
+
+  apclient->set_socket_disconnected_handler([]() {
+    tracker_frame->SetStatusMessage(
+        "Disconnected from Archipelago. Attempting to reconnect...");
+  });
+
+  apclient->set_items_received_handler(
+      [](const std::list<APClient::NetworkItem>& items) {
+        for (const APClient::NetworkItem& item : items) {
+          inventory[item.item]++;
+          std::cout << "Item: " << item.item << std::endl;
+        }
+
+        RefreshTracker();
+      });
+
+  apclient->set_slot_connected_handler([](const nlohmann::json& slot_data) {
+    tracker_frame->SetStatusMessage("Connected to Archipelago!");
+
+    door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
+    color_shuffle = slot_data["shuffle_colors"].get<bool>();
+    painting_shuffle = slot_data["shuffle_paintings"].get<bool>();
+    mastery_requirement = slot_data["mastery_achievements"].get<int>();
+
+    if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) {
+      painting_mapping.clear();
+
+      for (const auto& mapping_it :
+           slot_data["painting_entrance_to_exit"].items()) {
+        painting_mapping[mapping_it.key()] = mapping_it.value();
+      }
+    }
+
+    connected = true;
+    has_connection_result = true;
+
+    RefreshTracker();
+  });
+
+  apclient->set_slot_refused_handler([](const std::list<std::string>& errors) {
+    connected = false;
+    has_connection_result = true;
+
+    tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
+
+    std::vector<std::string> error_messages;
+    error_messages.push_back("Could not connect to Archipelago.");
+
+    for (const std::string& error : errors) {
+      if (error == "InvalidSlot") {
+        error_messages.push_back("Invalid player name.");
+      } else if (error == "InvalidGame") {
+        error_messages.push_back("The specified player is not playing Lingo.");
+      } else if (error == "IncompatibleVersion") {
+        error_messages.push_back(
+            "The Archipelago server is not the correct version for this "
+            "client.");
+      } else if (error == "InvalidPassword") {
+        error_messages.push_back("Incorrect password.");
+      } else if (error == "InvalidItemsHandling") {
+        error_messages.push_back(
+            "Invalid item handling flag. This is a bug with the tracker. "
+            "Please report it to the lingo-ap-tracker GitHub.");
+      } else {
+        error_messages.push_back("Unknown error.");
+      }
+    }
+
+    std::string full_message = hatkirby::implode(error_messages, " ");
+
+    wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
+  });
+
+  client_active = true;
+
+  int timeout = 5000;  // 5 seconds
+  int interval = 100;
+  int remaining_loops = timeout / interval;
+  while (!has_connection_result) {
+    if (interval == 0) {
+      connected = false;
+      has_connection_result = true;
+
+      DestroyClient();
+
+      tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
+
+      wxMessageBox("Timeout while connecting to Archipelago server.",
+                   "Connection failed", wxOK | wxICON_ERROR);
+    }
+
+    std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+    interval--;
+  }
+
+  if (connected) {
+    for (const MapArea& map_area : GetGameData().GetMapAreas()) {
+      for (int section_id = 0; section_id < map_area.locations.size();
+           section_id++) {
+        const Location& location = map_area.locations.at(section_id);
+
+        int64_t ap_id = apclient->get_location_id(location.ap_location_name);
+        if (ap_id == APClient::INVALID_NAME_ID) {
+          std::cout << "Could not find AP location ID for "
+                    << location.ap_location_name << std::endl;
+        } else {
+          ap_id_by_location_id[{map_area.id, section_id}] = ap_id;
+        }
+      }
+    }
+
+    for (const Door& door : GetGameData().GetDoors()) {
+      if (!door.skip_item) {
+        ap_id_by_item_name[door.item_name] = GetItemId(door.item_name);
+
+        if (!door.group_name.empty() &&
+            !ap_id_by_item_name.count(door.group_name)) {
+          ap_id_by_item_name[door.group_name] = GetItemId(door.group_name);
+        }
+
+        for (const ProgressiveRequirement& prog_req : door.progressives) {
+          ap_id_by_item_name[prog_req.item_name] =
+              GetItemId(prog_req.item_name);
+        }
+      }
+    }
+
+    ap_id_by_color[LingoColor::kBlack] = GetItemId("Black");
+    ap_id_by_color[LingoColor::kRed] = GetItemId("Red");
+    ap_id_by_color[LingoColor::kBlue] = GetItemId("Blue");
+    ap_id_by_color[LingoColor::kYellow] = GetItemId("Yellow");
+    ap_id_by_color[LingoColor::kPurple] = GetItemId("Purple");
+    ap_id_by_color[LingoColor::kOrange] = GetItemId("Orange");
+    ap_id_by_color[LingoColor::kGreen] = GetItemId("Green");
+    ap_id_by_color[LingoColor::kBrown] = GetItemId("Brown");
+    ap_id_by_color[LingoColor::kGray] = GetItemId("Gray");
+
+    RefreshTracker();
+  } else {
+    client_active = false;
+  }
+}
+
+bool AP_HasCheckedGameLocation(int area_id, int section_id) {
+  std::tuple<int, int> location_key = {area_id, section_id};
+
+  if (ap_id_by_location_id.count(location_key)) {
+    return checked_locations.count(ap_id_by_location_id.at(location_key));
+  } else {
+    return false;
+  }
+}
+
+bool AP_HasColorItem(LingoColor color) {
+  if (ap_id_by_color.count(color)) {
+    return inventory.count(ap_id_by_color.at(color));
+  } else {
+    return false;
+  }
+}
+
+bool AP_HasItem(const std::string& item, int quantity) {
+  if (ap_id_by_item_name.count(item)) {
+    int64_t ap_id = ap_id_by_item_name.at(item);
+    return inventory.count(ap_id) && inventory.at(ap_id) >= quantity;
+  } else {
+    return false;
+  }
+}
+
+DoorShuffleMode AP_GetDoorShuffleMode() { return door_shuffle_mode; }
+
+bool AP_IsColorShuffle() { return color_shuffle; }
+
+bool AP_IsPaintingShuffle() { return painting_shuffle; }
+
+const std::map<std::string, std::string> AP_GetPaintingMapping() {
+  return painting_mapping;
+}
+
+int AP_GetMasteryRequirement() { return mastery_requirement; }
diff --git a/src/ap_state.h b/src/ap_state.h
new file mode 100644
index 0000000..d880c71
--- /dev/null
+++ b/src/ap_state.h
@@ -0,0 +1,33 @@
+#ifndef AP_STATE_H_664A4180
+#define AP_STATE_H_664A4180
+
+#include <map>
+#include <string>
+
+#include "game_data.h"
+
+class TrackerFrame;
+
+enum DoorShuffleMode { kNO_DOORS = 0, kSIMPLE_DOORS = 1, kCOMPLEX_DOORS = 2 };
+
+void AP_SetTrackerFrame(TrackerFrame* tracker_frame);
+
+void AP_Connect(std::string server, std::string player, std::string password);
+
+bool AP_HasCheckedGameLocation(int area_id, int section_id);
+
+bool AP_HasColorItem(LingoColor color);
+
+bool AP_HasItem(const std::string& item, int quantity = 1);
+
+DoorShuffleMode AP_GetDoorShuffleMode();
+
+bool AP_IsColorShuffle();
+
+bool AP_IsPaintingShuffle();
+
+const std::map<std::string, std::string> AP_GetPaintingMapping();
+
+int AP_GetMasteryRequirement();
+
+#endif /* end of include guard: AP_STATE_H_664A4180 */
diff --git a/src/area_popup.cpp b/src/area_popup.cpp
new file mode 100644
index 0000000..a8ff612
--- /dev/null
+++ b/src/area_popup.cpp
@@ -0,0 +1,54 @@
+#include "area_popup.h"
+
+#include "ap_state.h"
+#include "game_data.h"
+#include "tracker_state.h"
+
+AreaPopup::AreaPopup(wxWindow* parent, int area_id)
+    : wxPanel(parent, wxID_ANY), area_id_(area_id) {
+  const MapArea& map_area = GetGameData().GetMapArea(area_id);
+
+  wxFlexGridSizer* section_sizer = new wxFlexGridSizer(2, 10, 10);
+
+  for (const Location& location : map_area.locations) {
+    EyeIndicator* eye_indicator = new EyeIndicator(this);
+    section_sizer->Add(eye_indicator, wxSizerFlags().Expand());
+    eye_indicators_.push_back(eye_indicator);
+
+    wxStaticText* section_label = new wxStaticText(this, -1, location.name);
+    section_label->SetForegroundColour(*wxWHITE);
+    section_sizer->Add(
+        section_label,
+        wxSizerFlags().Align(wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL));
+    section_labels_.push_back(section_label);
+  }
+
+  wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
+
+  wxStaticText* top_label = new wxStaticText(this, -1, map_area.name);
+  top_label->SetForegroundColour(*wxWHITE);
+  top_label->SetFont(top_label->GetFont().Bold());
+  top_sizer->Add(top_label,
+                 wxSizerFlags().Center().DoubleBorder(wxUP | wxLEFT | wxRIGHT));
+
+  top_sizer->Add(section_sizer, wxSizerFlags().DoubleBorder(wxALL).Expand());
+
+  SetSizerAndFit(top_sizer);
+
+  SetBackgroundColour(*wxBLACK);
+  Hide();
+}
+
+void AreaPopup::UpdateIndicators() {
+  const MapArea& map_area = GetGameData().GetMapArea(area_id_);
+  for (int section_id = 0; section_id < map_area.locations.size();
+       section_id++) {
+    bool checked = AP_HasCheckedGameLocation(area_id_, section_id);
+    bool reachable =
+        GetTrackerState().IsLocationReachable(area_id_, section_id);
+    const wxColour* text_color = reachable ? wxWHITE : wxRED;
+
+    section_labels_[section_id]->SetForegroundColour(*text_color);
+    eye_indicators_[section_id]->SetChecked(checked);
+  }
+}
diff --git a/src/area_popup.h b/src/area_popup.h
new file mode 100644
index 0000000..b602b63
--- /dev/null
+++ b/src/area_popup.h
@@ -0,0 +1,25 @@
+#ifndef AREA_POPUP_H_03FAC988
+#define AREA_POPUP_H_03FAC988
+
+#include <wx/wxprec.h>
+
+#ifndef WX_PRECOMP
+#include <wx/wx.h>
+#endif
+
+#include "eye_indicator.h"
+
+class AreaPopup : public wxPanel {
+ public:
+  AreaPopup(wxWindow* parent, int area_id);
+
+  void UpdateIndicators();
+
+ private:
+  int area_id_;
+
+  std::vector<wxStaticText*> section_labels_;
+  std::vector<EyeIndicator*> eye_indicators_;
+};
+
+#endif /* end of include guard: AREA_POPUP_H_03FAC988 */
diff --git a/src/connection_dialog.cpp b/src/connection_dialog.cpp
new file mode 100644
index 0000000..9dd9984
--- /dev/null
+++ b/src/connection_dialog.cpp
@@ -0,0 +1,40 @@
+#include "connection_dialog.h"
+
+#include "tracker_config.h"
+
+ConnectionDialog::ConnectionDialog()
+    : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") {
+  server_box_ = new wxTextCtrl(this, -1, GetTrackerConfig().ap_server, wxDefaultPosition,
+                               {300, -1});
+  player_box_ = new wxTextCtrl(this, -1, GetTrackerConfig().ap_player, wxDefaultPosition,
+                               {300, -1});
+  password_box_ = new wxTextCtrl(this, -1, GetTrackerConfig().ap_password,
+                                 wxDefaultPosition, {300, -1});
+
+  wxFlexGridSizer* form_sizer = new wxFlexGridSizer(2, 10, 10);
+
+  form_sizer->Add(
+      new wxStaticText(this, -1, "Server:"),
+      wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
+  form_sizer->Add(server_box_, wxSizerFlags().Expand());
+  form_sizer->Add(
+      new wxStaticText(this, -1, "Player:"),
+      wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
+  form_sizer->Add(player_box_, wxSizerFlags().Expand());
+  form_sizer->Add(
+      new wxStaticText(this, -1, "Password:"),
+      wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
+  form_sizer->Add(password_box_, wxSizerFlags().Expand());
+
+  wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
+  top_sizer->Add(new wxStaticText(
+                     this, -1, "Enter the details to connect to Archipelago."),
+                 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder());
+  top_sizer->Add(form_sizer, wxSizerFlags().DoubleBorder().Expand());
+  top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Center());
+
+  SetSizerAndFit(top_sizer);
+
+  Center();
+  server_box_->SetFocus();
+}
diff --git a/src/connection_dialog.h b/src/connection_dialog.h
new file mode 100644
index 0000000..5d2703d
--- /dev/null
+++ b/src/connection_dialog.h
@@ -0,0 +1,30 @@
+#ifndef CONNECTION_DIALOG_H_E526D0E7
+#define CONNECTION_DIALOG_H_E526D0E7
+
+#include <wx/wxprec.h>
+
+#ifndef WX_PRECOMP
+#include <wx/wx.h>
+#endif
+
+#include <string>
+
+class ConnectionDialog : public wxDialog {
+ public:
+  ConnectionDialog();
+
+  std::string GetServerValue() { return server_box_->GetValue().ToStdString(); }
+
+  std::string GetPlayerValue() { return player_box_->GetValue().ToStdString(); }
+
+  std::string GetPasswordValue() {
+    return password_box_->GetValue().ToStdString();
+  }
+
+ private:
+  wxTextCtrl* server_box_;
+  wxTextCtrl* player_box_;
+  wxTextCtrl* password_box_;
+};
+
+#endif /* end of include guard: CONNECTION_DIALOG_H_E526D0E7 */
diff --git a/src/eye_indicator.cpp b/src/eye_indicator.cpp
new file mode 100644
index 0000000..03e234e
--- /dev/null
+++ b/src/eye_indicator.cpp
@@ -0,0 +1,49 @@
+#include "eye_indicator.h"
+
+EyeIndicator::EyeIndicator(wxWindow* parent) : wxWindow(parent, wxID_ANY) {
+  SetMinSize({32, 32});
+
+  Redraw();
+
+  Bind(wxEVT_PAINT, &EyeIndicator::OnPaint, this);
+}
+
+void EyeIndicator::SetChecked(bool checked) {
+  if (intended_checked_ != checked) {
+    intended_checked_ = checked;
+
+    Redraw();
+  }
+}
+
+const wxImage& EyeIndicator::GetUncheckedImage() {
+  static wxImage* unchecked_image =
+      new wxImage("assets/unchecked.png", wxBITMAP_TYPE_PNG);
+  return *unchecked_image;
+}
+
+const wxImage& EyeIndicator::GetCheckedImage() {
+  static wxImage* checked_image =
+      new wxImage("assets/checked.png", wxBITMAP_TYPE_PNG);
+  return *checked_image;
+}
+
+void EyeIndicator::OnPaint(wxPaintEvent& event) {
+  if (GetSize() != rendered_.GetSize() ||
+      intended_checked_ != rendered_checked_) {
+    Redraw();
+  }
+
+  wxPaintDC dc(this);
+  dc.DrawBitmap(rendered_, 0, 0);
+
+  event.Skip();
+}
+
+void EyeIndicator::Redraw() {
+  rendered_ =
+      wxBitmap((intended_checked_ ? GetCheckedImage() : GetUncheckedImage())
+                   .Scale(GetSize().GetWidth(), GetSize().GetHeight(),
+                          wxIMAGE_QUALITY_NORMAL));
+  rendered_checked_ = intended_checked_;
+}
diff --git a/src/eye_indicator.h b/src/eye_indicator.h
new file mode 100644
index 0000000..e8fd890
--- /dev/null
+++ b/src/eye_indicator.h
@@ -0,0 +1,30 @@
+#ifndef EYE_INDICATOR_H_778150F2
+#define EYE_INDICATOR_H_778150F2
+
+#include <wx/wxprec.h>
+
+#ifndef WX_PRECOMP
+#include <wx/wx.h>
+#endif
+
+class EyeIndicator : public wxWindow {
+ public:
+  EyeIndicator(wxWindow* parent);
+
+  void SetChecked(bool checked);
+
+ private:
+  static const wxImage& GetUncheckedImage();
+  static const wxImage& GetCheckedImage();
+
+  void OnPaint(wxPaintEvent& event);
+
+  void Redraw();
+
+  bool intended_checked_ = false;
+
+  wxBitmap rendered_;
+  bool rendered_checked_ = false;
+};
+
+#endif /* end of include guard: EYE_INDICATOR_H_778150F2 */
diff --git a/src/game_data.cpp b/src/game_data.cpp
new file mode 100644
index 0000000..e15847e
--- /dev/null
+++ b/src/game_data.cpp
@@ -0,0 +1,406 @@
+#include "game_data.h"
+
+#include <hkutil/string.h>
+#include <yaml-cpp/yaml.h>
+
+#include <iostream>
+
+LingoColor GetColorForString(const std::string &str) {
+  if (str == "black") {
+    return LingoColor::kBlack;
+  } else if (str == "red") {
+    return LingoColor::kRed;
+  } else if (str == "blue") {
+    return LingoColor::kBlue;
+  } else if (str == "yellow") {
+    return LingoColor::kYellow;
+  } else if (str == "orange") {
+    return LingoColor::kOrange;
+  } else if (str == "green") {
+    return LingoColor::kGreen;
+  } else if (str == "gray") {
+    return LingoColor::kGray;
+  } else if (str == "brown") {
+    return LingoColor::kBrown;
+  } else if (str == "purple") {
+    return LingoColor::kPurple;
+  } else {
+    std::cout << "Invalid color: " << str << std::endl;
+    return LingoColor::kNone;
+  }
+}
+
+GameData::GameData() {
+  YAML::Node lingo_config = YAML::LoadFile("assets/LL1.yaml");
+  YAML::Node areas_config = YAML::LoadFile("assets/areas.yaml");
+
+  rooms_.reserve(lingo_config.size() * 2);
+
+  for (const auto &room_it : lingo_config) {
+    int room_id = AddOrGetRoom(room_it.first.as<std::string>());
+    Room &room_obj = rooms_[room_id];
+
+    for (const auto &entrance_it : room_it.second["entrances"]) {
+      int from_room_id = AddOrGetRoom(entrance_it.first.as<std::string>());
+      Room &from_room_obj = rooms_[from_room_id];
+
+      switch (entrance_it.second.Type()) {
+        case YAML::NodeType::Scalar: {
+          // This is just "true".
+          from_room_obj.exits.push_back({.destination_room = room_id});
+          break;
+        }
+        case YAML::NodeType::Map: {
+          Exit exit_obj;
+          exit_obj.destination_room = room_id;
+
+          if (entrance_it.second["door"]) {
+            std::string door_room = room_obj.name;
+            if (entrance_it.second["room"]) {
+              door_room = entrance_it.second["room"].as<std::string>();
+            }
+            exit_obj.door = AddOrGetDoor(
+                door_room, entrance_it.second["door"].as<std::string>());
+          }
+
+          if (entrance_it.second["painting"]) {
+            exit_obj.painting = entrance_it.second["painting"].as<bool>();
+          }
+
+          from_room_obj.exits.push_back(exit_obj);
+          break;
+        }
+        case YAML::NodeType::Sequence: {
+          for (const auto &option : entrance_it.second) {
+            Exit exit_obj;
+            exit_obj.destination_room = room_id;
+
+            std::string door_room = room_obj.name;
+            if (option["room"]) {
+              door_room = option["room"].as<std::string>();
+            }
+            exit_obj.door =
+                AddOrGetDoor(door_room, option["door"].as<std::string>());
+
+            if (option["painting"]) {
+              exit_obj.painting = option["painting"].as<bool>();
+            }
+
+            from_room_obj.exits.push_back(exit_obj);
+          }
+
+          break;
+        }
+        default: {
+          // This shouldn't happen.
+          std::cout << "Error reading game data: " << entrance_it << std::endl;
+          break;
+        }
+      }
+    }
+
+    if (room_it.second["panels"]) {
+      for (const auto &panel_it : room_it.second["panels"]) {
+        int panel_id =
+            AddOrGetPanel(room_obj.name, panel_it.first.as<std::string>());
+        Panel &panel_obj = panels_[panel_id];
+
+        if (panel_it.second["colors"]) {
+          if (panel_it.second["colors"].IsScalar()) {
+            panel_obj.colors.push_back(
+                GetColorForString(panel_it.second["colors"].as<std::string>()));
+          } else {
+            for (const auto &color_node : panel_it.second["colors"]) {
+              panel_obj.colors.push_back(
+                  GetColorForString(color_node.as<std::string>()));
+            }
+          }
+        }
+
+        if (panel_it.second["required_room"]) {
+          if (panel_it.second["required_room"].IsScalar()) {
+            panel_obj.required_rooms.push_back(AddOrGetRoom(
+                panel_it.second["required_room"].as<std::string>()));
+          } else {
+            for (const auto &rr_node : panel_it.second["required_room"]) {
+              panel_obj.required_rooms.push_back(
+                  AddOrGetRoom(rr_node.as<std::string>()));
+            }
+          }
+        }
+
+        if (panel_it.second["required_door"]) {
+          if (panel_it.second["required_door"].IsMap()) {
+            std::string rd_room = room_obj.name;
+            if (panel_it.second["required_door"]["room"]) {
+              rd_room =
+                  panel_it.second["required_door"]["room"].as<std::string>();
+            }
+
+            panel_obj.required_doors.push_back(AddOrGetDoor(
+                rd_room,
+                panel_it.second["required_door"]["door"].as<std::string>()));
+          } else {
+            for (const auto &rr_node : panel_it.second["required_door"]) {
+              std::string rd_room = room_obj.name;
+              if (rr_node["room"]) {
+                rd_room = rr_node["room"].as<std::string>();
+              }
+
+              panel_obj.required_doors.push_back(
+                  AddOrGetDoor(rd_room, rr_node["door"].as<std::string>()));
+            }
+          }
+        }
+
+        if (panel_it.second["check"]) {
+          panel_obj.check = panel_it.second["check"].as<bool>();
+        }
+
+        if (panel_it.second["achievement"]) {
+          panel_obj.achievement = panel_it.second["achievement"].as<bool>();
+
+          if (panel_obj.achievement) {
+            achievement_panels_.push_back(panel_id);
+          }
+        }
+
+        if (panel_it.second["exclude_reduce"]) {
+          panel_obj.exclude_reduce =
+              panel_it.second["exclude_reduce"].as<bool>();
+        }
+      }
+    }
+
+    if (room_it.second["doors"]) {
+      for (const auto &door_it : room_it.second["doors"]) {
+        int door_id =
+            AddOrGetDoor(room_obj.name, door_it.first.as<std::string>());
+        Door &door_obj = doors_[door_id];
+
+        bool has_external_panels = false;
+        std::vector<std::string> panel_names;
+
+        for (const auto &panel_node : door_it.second["panels"]) {
+          if (panel_node.IsScalar()) {
+            panel_names.push_back(panel_node.as<std::string>());
+            door_obj.panels.push_back(
+                AddOrGetPanel(room_obj.name, panel_node.as<std::string>()));
+          } else {
+            has_external_panels = true;
+            panel_names.push_back(panel_node["panel"].as<std::string>());
+            door_obj.panels.push_back(
+                AddOrGetPanel(panel_node["room"].as<std::string>(),
+                              panel_node["panel"].as<std::string>()));
+          }
+        }
+
+        if (door_it.second["skip_location"]) {
+          door_obj.skip_location = door_it.second["skip_location"].as<bool>();
+        }
+
+        if (door_it.second["skip_item"]) {
+          door_obj.skip_item = door_it.second["skip_item"].as<bool>();
+        }
+
+        if (door_it.second["event"]) {
+          door_obj.skip_location = door_it.second["event"].as<bool>();
+          door_obj.skip_item = door_it.second["event"].as<bool>();
+        }
+
+        if (door_it.second["item_name"]) {
+          door_obj.item_name = door_it.second["item_name"].as<std::string>();
+        } else if (!door_it.second["skip_item"] && !door_it.second["event"]) {
+          door_obj.item_name = room_obj.name + " - " + door_obj.name;
+        }
+
+        if (door_it.second["group"]) {
+          door_obj.group_name = door_it.second["group"].as<std::string>();
+        }
+
+        if (door_it.second["location_name"]) {
+          door_obj.location_name =
+              door_it.second["location_name"].as<std::string>();
+        } else if (!door_it.second["skip_location"] &&
+                   !door_it.second["event"]) {
+          if (has_external_panels) {
+            std::cout
+                << room_obj.name << " - " << door_obj.name
+                << " has panels from other rooms but does not have an explicit "
+                   "location name and is not marked skip_location or event"
+                << std::endl;
+          }
+
+          door_obj.location_name =
+              room_obj.name + " - " + hatkirby::implode(panel_names, ", ");
+        }
+
+        if (door_it.second["include_reduce"]) {
+          door_obj.exclude_reduce =
+              !door_it.second["include_reduce"].as<bool>();
+        }
+      }
+    }
+
+    if (room_it.second["paintings"]) {
+      for (const auto &painting : room_it.second["paintings"]) {
+        std::string painting_id = painting["id"].as<std::string>();
+        room_by_painting_[painting_id] = room_id;
+
+        if (!painting["exit_only"] || !painting["exit_only"].as<bool>()) {
+          PaintingExit painting_exit;
+          painting_exit.id = painting_id;
+
+          if (painting["required_door"]) {
+            std::string rd_room = room_obj.name;
+            if (painting["required_door"]["room"]) {
+              rd_room = painting["required_door"]["room"].as<std::string>();
+            }
+
+            painting_exit.door = AddOrGetDoor(
+                rd_room, painting["required_door"]["door"].as<std::string>());
+          }
+
+          room_obj.paintings.push_back(painting_exit);
+        }
+      }
+    }
+
+    if (room_it.second["progression"]) {
+      for (const auto &progression_it : room_it.second["progression"]) {
+        std::string progressive_item_name =
+            progression_it.first.as<std::string>();
+
+        int index = 1;
+        for (const auto &stage : progression_it.second) {
+          int door_id = -1;
+
+          if (stage.IsScalar()) {
+            door_id = AddOrGetDoor(room_obj.name, stage.as<std::string>());
+          } else {
+            door_id = AddOrGetDoor(stage["room"].as<std::string>(),
+                                   stage["door"].as<std::string>());
+          }
+
+          doors_[door_id].progressives.push_back(
+              {.item_name = progressive_item_name, .quantity = index});
+          index++;
+        }
+      }
+    }
+  }
+
+  map_areas_.reserve(areas_config.size());
+
+  std::map<std::string, int> fold_areas;
+  for (const auto &area_it : areas_config) {
+    if (area_it.second["map"]) {
+      int area_id = AddOrGetArea(area_it.first.as<std::string>());
+      MapArea &area_obj = map_areas_[area_id];
+      area_obj.map_x = area_it.second["map"][0].as<int>();
+      area_obj.map_y = area_it.second["map"][1].as<int>();
+    } else if (area_it.second["fold_into"]) {
+      fold_areas[area_it.first.as<std::string>()] =
+          AddOrGetArea(area_it.second["fold_into"].as<std::string>());
+    }
+  }
+
+  for (const Panel &panel : panels_) {
+    if (panel.check) {
+      int room_id = panel.room;
+      std::string room_name = rooms_[room_id].name;
+
+      std::string area_name = room_name;
+      if (fold_areas.count(room_name)) {
+        int fold_area_id = fold_areas[room_name];
+        area_name = map_areas_[fold_area_id].name;
+      }
+
+      int area_id = AddOrGetArea(area_name);
+      MapArea &map_area = map_areas_[area_id];
+      // room field should be the original room ID
+      map_area.locations.push_back(
+          {.name = panel.name,
+           .ap_location_name = room_name + " - " + panel.name,
+           .room = panel.room,
+           .panels = {panel.id}});
+    }
+  }
+
+  for (const Door &door : doors_) {
+    if (!door.skip_location) {
+      int room_id = door.room;
+      std::string area_name = rooms_[room_id].name;
+      std::string section_name;
+
+      size_t divider_pos = door.location_name.find(" - ");
+      if (divider_pos == std::string::npos) {
+        section_name = door.location_name;
+      } else {
+        area_name = door.location_name.substr(0, divider_pos);
+        section_name = door.location_name.substr(divider_pos + 3);
+      }
+
+      if (fold_areas.count(area_name)) {
+        int fold_area_id = fold_areas[area_name];
+        area_name = map_areas_[fold_area_id].name;
+      }
+
+      int area_id = AddOrGetArea(area_name);
+      MapArea &map_area = map_areas_[area_id];
+      // room field should be the original room ID
+      map_area.locations.push_back({.name = section_name,
+                                    .ap_location_name = door.location_name,
+                                    .room = door.room,
+                                    .panels = door.panels});
+    }
+  }
+}
+
+int GameData::AddOrGetRoom(std::string room) {
+  if (!room_by_id_.count(room)) {
+    room_by_id_[room] = rooms_.size();
+    rooms_.push_back({.name = room});
+  }
+
+  return room_by_id_[room];
+}
+
+int GameData::AddOrGetDoor(std::string room, std::string door) {
+  std::string full_name = room + " - " + door;
+
+  if (!door_by_id_.count(full_name)) {
+    door_by_id_[full_name] = doors_.size();
+    doors_.push_back({.room = AddOrGetRoom(room), .name = door});
+  }
+
+  return door_by_id_[full_name];
+}
+
+int GameData::AddOrGetPanel(std::string room, std::string panel) {
+  std::string full_name = room + " - " + panel;
+
+  if (!panel_by_id_.count(full_name)) {
+    int panel_id = panels_.size();
+    panel_by_id_[full_name] = panel_id;
+    panels_.push_back(
+        {.id = panel_id, .room = AddOrGetRoom(room), .name = panel});
+  }
+
+  return panel_by_id_[full_name];
+}
+
+int GameData::AddOrGetArea(std::string area) {
+  if (!area_by_id_.count(area)) {
+    int area_id = map_areas_.size();
+    area_by_id_[area] = area_id;
+    map_areas_.push_back({.id = area_id, .name = area});
+  }
+
+  return area_by_id_[area];
+}
+
+const GameData &GetGameData() {
+  static GameData *instance = new GameData();
+  return *instance;
+}
diff --git a/src/game_data.h b/src/game_data.h
new file mode 100644
index 0000000..0cc7a7b
--- /dev/null
+++ b/src/game_data.h
@@ -0,0 +1,135 @@
+#ifndef GAME_DATA_H_9C42AC51
+#define GAME_DATA_H_9C42AC51
+
+#include <map>
+#include <optional>
+#include <string>
+#include <vector>
+
+enum class LingoColor {
+  kNone,
+  kBlack,
+  kRed,
+  kBlue,
+  kYellow,
+  kGreen,
+  kOrange,
+  kPurple,
+  kBrown,
+  kGray
+};
+
+struct Panel {
+  int id;
+  int room;
+  std::string name;
+  std::vector<LingoColor> colors;
+  std::vector<int> required_rooms;
+  std::vector<int> required_doors;
+  bool check = false;
+  bool exclude_reduce = false;
+  bool achievement = false;
+};
+
+struct ProgressiveRequirement {
+  std::string item_name;
+  int quantity = 0;
+};
+
+struct Door {
+  int room;
+  std::string name;
+  std::string location_name;
+  std::string item_name;
+  std::string group_name;
+  bool skip_location = false;
+  bool skip_item = false;
+  std::vector<int> panels;
+  bool exclude_reduce = true;
+  std::vector<ProgressiveRequirement> progressives;
+};
+
+struct Exit {
+  int destination_room;
+  std::optional<int> door;
+  bool painting = false;
+};
+
+struct PaintingExit {
+  std::string id;
+  std::optional<int> door;
+};
+
+struct Room {
+  std::string name;
+  std::vector<Exit> exits;
+  std::vector<PaintingExit> paintings;
+};
+
+struct Location {
+  std::string name;
+  std::string ap_location_name;
+  int room;
+  std::vector<int> panels;
+};
+
+struct MapArea {
+  int id;
+  std::string name;
+  std::vector<Location> locations;
+  int map_x;
+  int map_y;
+};
+
+class GameData {
+ public:
+  GameData();
+
+  const std::vector<MapArea>& GetMapAreas() const { return map_areas_; }
+
+  const MapArea& GetMapArea(int id) const { return map_areas_.at(id); }
+
+  int GetRoomByName(const std::string& name) const {
+    return room_by_id_.at(name);
+  }
+
+  const Room& GetRoom(int room_id) const { return rooms_.at(room_id); }
+
+  const std::vector<Door>& GetDoors() const { return doors_; }
+
+  const Door& GetDoor(int door_id) const { return doors_.at(door_id); }
+
+  const Panel& GetPanel(int panel_id) const { return panels_.at(panel_id); }
+
+  int GetRoomForPainting(const std::string& painting_id) const {
+    return room_by_painting_.at(painting_id);
+  }
+
+  const std::vector<int>& GetAchievementPanels() const {
+    return achievement_panels_;
+  }
+
+ private:
+  int AddOrGetRoom(std::string room);
+  int AddOrGetDoor(std::string room, std::string door);
+  int AddOrGetPanel(std::string room, std::string panel);
+  int AddOrGetArea(std::string area);
+
+  std::vector<Room> rooms_;
+  std::vector<Door> doors_;
+  std::vector<Panel> panels_;
+  std::vector<MapArea> map_areas_;
+
+  std::map<std::string, int> room_by_id_;
+  std::map<std::string, int> door_by_id_;
+  std::map<std::string, int> panel_by_id_;
+  std::map<std::string, int> area_by_id_;
+
+  std::map<std::string, int> room_by_painting_;
+
+  std::vector<int> achievement_panels_;
+};
+
+const GameData& GetGameData();
+
+#endif /* end of include guard: GAME_DATA_H_9C42AC51 */
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..fe9aceb
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,21 @@
+#include <wx/wxprec.h>
+
+#ifndef WX_PRECOMP
+#include <wx/wx.h>
+#endif
+
+#include "tracker_config.h"
+#include "tracker_frame.h"
+
+class TrackerApp : public wxApp {
+ public:
+  virtual bool OnInit() {
+    GetTrackerConfig().Load();
+
+    TrackerFrame *frame = new TrackerFrame();
+    frame->Show(true);
+    return true;
+  }
+};
+
+wxIMPLEMENT_APP(TrackerApp);
diff --git a/src/tracker_config.cpp b/src/tracker_config.cpp
new file mode 100644
index 0000000..96bb60a
--- /dev/null
+++ b/src/tracker_config.cpp
@@ -0,0 +1,33 @@
+#include "tracker_config.h"
+
+#include <fstream>
+#include <yaml-cpp/yaml.h>
+
+constexpr const char* CONFIG_FILE_NAME = "config.yaml";
+
+void TrackerConfig::Load() {
+  try {
+    YAML::Node file = YAML::LoadFile(CONFIG_FILE_NAME);
+
+    ap_server = file["ap_server"].as<std::string>();
+    ap_player = file["ap_player"].as<std::string>();
+    ap_password = file["ap_password"].as<std::string>();
+  } catch (const std::exception&) {
+    // It's fine if the file can't be loaded.
+  }
+}
+
+void TrackerConfig::Save() {
+  YAML::Node output;
+  output["ap_server"] = ap_server;
+  output["ap_player"] = ap_player;
+  output["ap_password"] = ap_password;
+
+  std::ofstream filewriter(CONFIG_FILE_NAME);
+  filewriter << output;
+}
+
+TrackerConfig& GetTrackerConfig() {
+  static TrackerConfig* instance = new TrackerConfig();
+  return *instance;
+}
diff --git a/src/tracker_config.h b/src/tracker_config.h
new file mode 100644
index 0000000..e2e6cd7
--- /dev/null
+++ b/src/tracker_config.h
@@ -0,0 +1,19 @@
+#ifndef TRACKER_CONFIG_H_36CDD648
+#define TRACKER_CONFIG_H_36CDD648
+
+#include <string>
+
+class TrackerConfig {
+ public:
+  void Load();
+
+  void Save();
+
+  std::string ap_server;
+  std::string ap_player;
+  std::string ap_password;
+};
+
+TrackerConfig& GetTrackerConfig();
+
+#endif /* end of include guard: TRACKER_CONFIG_H_36CDD648 */
diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp
new file mode 100644
index 0000000..2a862a5
--- /dev/null
+++ b/src/tracker_frame.cpp
@@ -0,0 +1,86 @@
+#include "tracker_frame.h"
+
+#include "ap_state.h"
+#include "connection_dialog.h"
+#include "tracker_config.h"
+#include "tracker_panel.h"
+
+enum TrackerFrameIds { ID_CONNECT = 1 };
+
+wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent);
+wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
+
+TrackerFrame::TrackerFrame()
+    : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition,
+              wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxFULL_REPAINT_ON_RESIZE) {
+  ::wxInitAllImageHandlers();
+
+  AP_SetTrackerFrame(this);
+
+  SetSize(1280, 728);
+
+  wxMenu *menuFile = new wxMenu();
+  menuFile->Append(ID_CONNECT, "&Connect");
+  menuFile->Append(wxID_EXIT);
+
+  wxMenu *menuHelp = new wxMenu();
+  menuHelp->Append(wxID_ABOUT);
+
+  wxMenuBar *menuBar = new wxMenuBar();
+  menuBar->Append(menuFile, "&File");
+  menuBar->Append(menuHelp, "&Help");
+
+  SetMenuBar(menuBar);
+
+  CreateStatusBar();
+  SetStatusText("Not connected to Archipelago.");
+
+  Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT);
+  Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT);
+  Bind(wxEVT_MENU, &TrackerFrame::OnConnect, this, ID_CONNECT);
+  Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
+  Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);
+
+  tracker_panel_ = new TrackerPanel(this);
+}
+
+void TrackerFrame::SetStatusMessage(std::string message) {
+  wxCommandEvent *event = new wxCommandEvent(STATUS_CHANGED);
+  event->SetString(message.c_str());
+
+  QueueEvent(event);
+}
+
+void TrackerFrame::UpdateIndicators() {
+  QueueEvent(new wxCommandEvent(STATE_CHANGED));
+}
+
+void TrackerFrame::OnAbout(wxCommandEvent &event) {
+  wxMessageBox("Lingo Archipelago Tracker by hatkirby",
+               "About lingo-ap-tracker", wxOK | wxICON_INFORMATION);
+}
+
+void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); }
+
+void TrackerFrame::OnConnect(wxCommandEvent &event) {
+  ConnectionDialog dlg;
+
+  if (dlg.ShowModal() == wxID_OK) {
+    GetTrackerConfig().ap_server = dlg.GetServerValue();
+    GetTrackerConfig().ap_player = dlg.GetPlayerValue();
+    GetTrackerConfig().ap_password = dlg.GetPasswordValue();
+    GetTrackerConfig().Save();
+
+    AP_Connect(dlg.GetServerValue(), dlg.GetPlayerValue(),
+               dlg.GetPasswordValue());
+  }
+}
+
+void TrackerFrame::OnStateChanged(wxCommandEvent &event) {
+  tracker_panel_->UpdateIndicators();
+  Refresh();
+}
+
+void TrackerFrame::OnStatusChanged(wxCommandEvent &event) {
+  SetStatusText(event.GetString());
+}
diff --git a/src/tracker_frame.h b/src/tracker_frame.h
new file mode 100644
index 0000000..6f4e4fc
--- /dev/null
+++ b/src/tracker_frame.h
@@ -0,0 +1,33 @@
+#ifndef TRACKER_FRAME_H_86BD8DFB
+#define TRACKER_FRAME_H_86BD8DFB
+
+#include <wx/wxprec.h>
+
+#ifndef WX_PRECOMP
+#include <wx/wx.h>
+#endif
+
+class TrackerPanel;
+
+wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent);
+wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
+
+class TrackerFrame : public wxFrame {
+ public:
+  TrackerFrame();
+
+  void SetStatusMessage(std::string message);
+
+  void UpdateIndicators();
+
+ private:
+  void OnExit(wxCommandEvent &event);
+  void OnAbout(wxCommandEvent &event);
+  void OnConnect(wxCommandEvent &event);
+  void OnStateChanged(wxCommandEvent &event);
+  void OnStatusChanged(wxCommandEvent &event);
+
+  TrackerPanel *tracker_panel_;
+};
+
+#endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */
diff --git a/src/tracker_panel.cpp b/src/tracker_panel.cpp
new file mode 100644
index 0000000..0e0569b
--- /dev/null
+++ b/src/tracker_panel.cpp
@@ -0,0 +1,149 @@
+#include "tracker_panel.h"
+
+#include "ap_state.h"
+#include "area_popup.h"
+#include "game_data.h"
+#include "tracker_state.h"
+
+constexpr int AREA_ACTUAL_SIZE = 64;
+constexpr int AREA_BORDER_SIZE = 5;
+constexpr int AREA_EFFECTIVE_SIZE = AREA_ACTUAL_SIZE + AREA_BORDER_SIZE * 2;
+
+TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
+  map_image_ = wxImage("assets/lingo_map.png", wxBITMAP_TYPE_PNG);
+  if (!map_image_.IsOk()) {
+    return;
+  }
+
+  for (const MapArea &map_area : GetGameData().GetMapAreas()) {
+    AreaIndicator area;
+    area.area_id = map_area.id;
+
+    area.popup = new AreaPopup(this, map_area.id);
+    area.popup->SetPosition({0, 0});
+
+    areas_.push_back(area);
+  }
+
+  Redraw();
+
+  Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this);
+  Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this);
+}
+
+void TrackerPanel::UpdateIndicators() {
+  Redraw();
+
+  for (AreaIndicator &area : areas_) {
+    area.popup->UpdateIndicators();
+  }
+}
+
+void TrackerPanel::OnPaint(wxPaintEvent &event) {
+  if (GetSize() != rendered_.GetSize()) {
+    Redraw();
+  }
+
+  wxPaintDC dc(this);
+  dc.DrawBitmap(rendered_, 0, 0);
+
+  event.Skip();
+}
+
+void TrackerPanel::OnMouseMove(wxMouseEvent &event) {
+  for (AreaIndicator &area : areas_) {
+    if (area.real_x1 <= event.GetX() && event.GetX() < area.real_x2 &&
+        area.real_y1 <= event.GetY() && event.GetY() < area.real_y2) {
+      area.popup->Show();
+    } else {
+      area.popup->Hide();
+    }
+  }
+
+  event.Skip();
+}
+
+void TrackerPanel::Redraw() {
+  wxSize panel_size = GetSize();
+  wxSize image_size = map_image_.GetSize();
+
+  int final_x = 0;
+  int final_y = 0;
+  int final_width = panel_size.GetWidth();
+  int final_height = panel_size.GetHeight();
+
+  if (image_size.GetWidth() * panel_size.GetHeight() >
+      panel_size.GetWidth() * image_size.GetHeight()) {
+    final_height = (panel_size.GetWidth() * image_size.GetHeight()) /
+                   image_size.GetWidth();
+    final_y = (panel_size.GetHeight() - final_height) / 2;
+  } else {
+    final_width = (image_size.GetWidth() * panel_size.GetHeight()) /
+                  image_size.GetHeight();
+    final_x = (panel_size.GetWidth() - final_width) / 2;
+  }
+
+  rendered_ = wxBitmap(
+      map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL)
+          .Size(panel_size, {final_x, final_y}, 0, 0, 0));
+
+  wxMemoryDC dc;
+  dc.SelectObject(rendered_);
+
+  for (AreaIndicator &area : areas_) {
+    const wxBrush *brush_color = wxGREY_BRUSH;
+
+    const MapArea &map_area = GetGameData().GetMapArea(area.area_id);
+    bool has_reachable_unchecked = false;
+    bool has_unreachable_unchecked = false;
+    for (int section_id = 0; section_id < map_area.locations.size();
+         section_id++) {
+      if (!AP_HasCheckedGameLocation(area.area_id, section_id)) {
+        if (GetTrackerState().IsLocationReachable(area.area_id, section_id)) {
+          has_reachable_unchecked = true;
+        } else {
+          has_unreachable_unchecked = true;
+        }
+      }
+    }
+
+    if (has_reachable_unchecked && has_unreachable_unchecked) {
+      brush_color = wxYELLOW_BRUSH;
+    } else if (has_reachable_unchecked) {
+      brush_color = wxGREEN_BRUSH;
+    } else if (has_unreachable_unchecked) {
+      brush_color = wxRED_BRUSH;
+    }
+
+    int real_area_size =
+        final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
+    int actual_border_size =
+        real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE;
+    int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) *
+                                    final_width / image_size.GetWidth();
+    int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
+                                    final_width / image_size.GetWidth();
+
+    dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
+    dc.SetBrush(*brush_color);
+    dc.DrawRectangle({real_area_x, real_area_y},
+                     {real_area_size, real_area_size});
+
+    area.real_x1 = real_area_x;
+    area.real_x2 = real_area_x + real_area_size;
+    area.real_y1 = real_area_y;
+    area.real_y2 = real_area_y + real_area_size;
+
+    int popup_x =
+        final_x + map_area.map_x * final_width / image_size.GetWidth();
+    int popup_y =
+        final_y + map_area.map_y * final_width / image_size.GetWidth();
+    if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
+      popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
+    }
+    if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
+      popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
+    }
+    area.popup->SetPosition({popup_x, popup_y});
+  }
+}
diff --git a/src/tracker_panel.h b/src/tracker_panel.h
new file mode 100644
index 0000000..a871f90
--- /dev/null
+++ b/src/tracker_panel.h
@@ -0,0 +1,39 @@
+#ifndef TRACKER_PANEL_H_D675A54D
+#define TRACKER_PANEL_H_D675A54D
+
+#include <wx/wxprec.h>
+
+#ifndef WX_PRECOMP
+#include <wx/wx.h>
+#endif
+
+class AreaPopup;
+
+class TrackerPanel : public wxPanel {
+ public:
+  TrackerPanel(wxWindow *parent);
+
+  void UpdateIndicators();
+
+ private:
+  struct AreaIndicator {
+    int area_id = -1;
+    AreaPopup *popup = nullptr;
+    int real_x1 = 0;
+    int real_y1 = 0;
+    int real_x2 = 0;
+    int real_y2 = 0;
+  };
+ 
+  void OnPaint(wxPaintEvent &event);
+  void OnMouseMove(wxMouseEvent &event);
+
+  void Redraw();
+
+  wxImage map_image_;
+  wxBitmap rendered_;
+
+  std::vector<AreaIndicator> areas_;
+};
+
+#endif /* end of include guard: TRACKER_PANEL_H_D675A54D */
diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp
new file mode 100644
index 0000000..858ec3e
--- /dev/null
+++ b/src/tracker_state.cpp
@@ -0,0 +1,181 @@
+#include "tracker_state.h"
+
+#include <list>
+#include <set>
+
+#include "ap_state.h"
+#include "game_data.h"
+
+bool IsDoorReachable_Helper(int door_id, const std::set<int>& reachable_rooms);
+
+bool IsPanelReachable_Helper(int panel_id,
+                             const std::set<int>& reachable_rooms) {
+  const Panel& panel_obj = GetGameData().GetPanel(panel_id);
+
+  if (!reachable_rooms.count(panel_obj.room)) {
+    return false;
+  }
+
+  if (panel_obj.name == "THE MASTER") {
+    int achievements_accessible = 0;
+
+    for (int achieve_id : GetGameData().GetAchievementPanels()) {
+      if (IsPanelReachable_Helper(achieve_id, reachable_rooms)) {
+        achievements_accessible++;
+
+        if (achievements_accessible >= AP_GetMasteryRequirement()) {
+          break;
+        }
+      }
+    }
+
+    return (achievements_accessible >= AP_GetMasteryRequirement());
+  }
+
+  for (int room_id : panel_obj.required_rooms) {
+    if (!reachable_rooms.count(room_id)) {
+      return false;
+    }
+  }
+
+  for (int door_id : panel_obj.required_doors) {
+    if (!IsDoorReachable_Helper(door_id, reachable_rooms)) {
+      return false;
+    }
+  }
+
+  if (AP_IsColorShuffle()) {
+    for (LingoColor color : panel_obj.colors) {
+      if (!AP_HasColorItem(color)) {
+        return false;
+      }
+    }
+  }
+
+  return true;
+}
+
+bool IsDoorReachable_Helper(int door_id, const std::set<int>& reachable_rooms) {
+  const Door& door_obj = GetGameData().GetDoor(door_id);
+
+  if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) {
+    if (!reachable_rooms.count(door_obj.room)) {
+      return false;
+    }
+
+    for (int panel_id : door_obj.panels) {
+      if (!IsPanelReachable_Helper(panel_id, reachable_rooms)) {
+        return false;
+      }
+    }
+
+    return true;
+  } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS &&
+             !door_obj.group_name.empty()) {
+    return AP_HasItem(door_obj.group_name);
+  } else {
+    bool has_item = AP_HasItem(door_obj.item_name);
+
+    if (!has_item) {
+      for (const ProgressiveRequirement& prog_req : door_obj.progressives) {
+        if (AP_HasItem(prog_req.item_name, prog_req.quantity)) {
+          has_item = true;
+          break;
+        }
+      }
+    }
+
+    return has_item;
+  }
+}
+
+void TrackerState::CalculateState() {
+  reachability_.clear();
+
+  std::set<int> reachable_rooms;
+
+  std::list<Exit> flood_boundary;
+  flood_boundary.push_back(
+      {.destination_room = GetGameData().GetRoomByName("Menu")});
+
+  bool reachable_changed = true;
+  while (reachable_changed) {
+    reachable_changed = false;
+
+    std::list<Exit> new_boundary;
+    for (const Exit& room_exit : flood_boundary) {
+      if (reachable_rooms.count(room_exit.destination_room)) {
+        continue;
+      }
+
+      bool valid_transition = false;
+      if (room_exit.door.has_value()) {
+        if (IsDoorReachable_Helper(*room_exit.door, reachable_rooms)) {
+          valid_transition = true;
+        } else if (AP_GetDoorShuffleMode() == kNO_DOORS) {
+          new_boundary.push_back(room_exit);
+        }
+      } else {
+        valid_transition = true;
+      }
+
+      if (valid_transition) {
+        reachable_rooms.insert(room_exit.destination_room);
+        reachable_changed = true;
+
+        const Room& room_obj =
+            GetGameData().GetRoom(room_exit.destination_room);
+        for (const Exit& out_edge : room_obj.exits) {
+          if (!out_edge.painting || !AP_IsPaintingShuffle()) {
+            new_boundary.push_back(out_edge);
+          }
+        }
+
+        if (AP_IsPaintingShuffle()) {
+          for (const PaintingExit& out_edge : room_obj.paintings) {
+            if (AP_GetPaintingMapping().count(out_edge.id)) {
+              Exit painting_exit;
+              painting_exit.destination_room = GetGameData().GetRoomForPainting(
+                  AP_GetPaintingMapping().at(out_edge.id));
+              painting_exit.door = out_edge.door;
+
+              new_boundary.push_back(painting_exit);
+            }
+          }
+        }
+      }
+    }
+
+    flood_boundary = new_boundary;
+  }
+
+  for (const MapArea& map_area : GetGameData().GetMapAreas()) {
+    for (int section_id = 0; section_id < map_area.locations.size();
+         section_id++) {
+      const Location& location_section = map_area.locations.at(section_id);
+      bool reachable = reachable_rooms.count(location_section.room);
+      if (reachable) {
+        for (int panel_id : location_section.panels) {
+          reachable &= IsPanelReachable_Helper(panel_id, reachable_rooms);
+        }
+      }
+
+      reachability_[{map_area.id, section_id}] = reachable;
+    }
+  }
+}
+
+bool TrackerState::IsLocationReachable(int area_id, int section_id) {
+  std::tuple<int, int> key = {area_id, section_id};
+
+  if (reachability_.count(key)) {
+    return reachability_.at(key);
+  } else {
+    return false;
+  }
+}
+
+TrackerState& GetTrackerState() {
+  static TrackerState* instance = new TrackerState();
+  return *instance;
+}
diff --git a/src/tracker_state.h b/src/tracker_state.h
new file mode 100644
index 0000000..879e6f2
--- /dev/null
+++ b/src/tracker_state.h
@@ -0,0 +1,19 @@
+#ifndef TRACKER_STATE_H_8639BC90
+#define TRACKER_STATE_H_8639BC90
+
+#include <map>
+#include <tuple>
+
+class TrackerState {
+ public:
+  void CalculateState();
+
+  bool IsLocationReachable(int area_id, int section_id);
+
+ private:
+  std::map<std::tuple<int, int>, bool> reachability_;
+};
+
+TrackerState& GetTrackerState();
+
+#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */
diff --git a/tracker_config.cpp b/tracker_config.cpp
deleted file mode 100644
index 96bb60a..0000000
--- a/tracker_config.cpp
+++ /dev/null
@@ -1,33 +0,0 @@
-#include "tracker_config.h"
-
-#include <fstream>
-#include <yaml-cpp/yaml.h>
-
-constexpr const char* CONFIG_FILE_NAME = "config.yaml";
-
-void TrackerConfig::Load() {
-  try {
-    YAML::Node file = YAML::LoadFile(CONFIG_FILE_NAME);
-
-    ap_server = file["ap_server"].as<std::string>();
-    ap_player = file["ap_player"].as<std::string>();
-    ap_password = file["ap_password"].as<std::string>();
-  } catch (const std::exception&) {
-    // It's fine if the file can't be loaded.
-  }
-}
-
-void TrackerConfig::Save() {
-  YAML::Node output;
-  output["ap_server"] = ap_server;
-  output["ap_player"] = ap_player;
-  output["ap_password"] = ap_password;
-
-  std::ofstream filewriter(CONFIG_FILE_NAME);
-  filewriter << output;
-}
-
-TrackerConfig& GetTrackerConfig() {
-  static TrackerConfig* instance = new TrackerConfig();
-  return *instance;
-}
diff --git a/tracker_config.h b/tracker_config.h
deleted file mode 100644
index e2e6cd7..0000000
--- a/tracker_config.h
+++ /dev/null
@@ -1,19 +0,0 @@
-#ifndef TRACKER_CONFIG_H_36CDD648
-#define TRACKER_CONFIG_H_36CDD648
-
-#include <string>
-
-class TrackerConfig {
- public:
-  void Load();
-
-  void Save();
-
-  std::string ap_server;
-  std::string ap_player;
-  std::string ap_password;
-};
-
-TrackerConfig& GetTrackerConfig();
-
-#endif /* end of include guard: TRACKER_CONFIG_H_36CDD648 */
diff --git a/tracker_frame.cpp b/tracker_frame.cpp
deleted file mode 100644
index 2a862a5..0000000
--- a/tracker_frame.cpp
+++ /dev/null
@@ -1,86 +0,0 @@
-#include "tracker_frame.h"
-
-#include "ap_state.h"
-#include "connection_dialog.h"
-#include "tracker_config.h"
-#include "tracker_panel.h"
-
-enum TrackerFrameIds { ID_CONNECT = 1 };
-
-wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent);
-wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
-
-TrackerFrame::TrackerFrame()
-    : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition,
-              wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxFULL_REPAINT_ON_RESIZE) {
-  ::wxInitAllImageHandlers();
-
-  AP_SetTrackerFrame(this);
-
-  SetSize(1280, 728);
-
-  wxMenu *menuFile = new wxMenu();
-  menuFile->Append(ID_CONNECT, "&Connect");
-  menuFile->Append(wxID_EXIT);
-
-  wxMenu *menuHelp = new wxMenu();
-  menuHelp->Append(wxID_ABOUT);
-
-  wxMenuBar *menuBar = new wxMenuBar();
-  menuBar->Append(menuFile, "&File");
-  menuBar->Append(menuHelp, "&Help");
-
-  SetMenuBar(menuBar);
-
-  CreateStatusBar();
-  SetStatusText("Not connected to Archipelago.");
-
-  Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT);
-  Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT);
-  Bind(wxEVT_MENU, &TrackerFrame::OnConnect, this, ID_CONNECT);
-  Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
-  Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);
-
-  tracker_panel_ = new TrackerPanel(this);
-}
-
-void TrackerFrame::SetStatusMessage(std::string message) {
-  wxCommandEvent *event = new wxCommandEvent(STATUS_CHANGED);
-  event->SetString(message.c_str());
-
-  QueueEvent(event);
-}
-
-void TrackerFrame::UpdateIndicators() {
-  QueueEvent(new wxCommandEvent(STATE_CHANGED));
-}
-
-void TrackerFrame::OnAbout(wxCommandEvent &event) {
-  wxMessageBox("Lingo Archipelago Tracker by hatkirby",
-               "About lingo-ap-tracker", wxOK | wxICON_INFORMATION);
-}
-
-void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); }
-
-void TrackerFrame::OnConnect(wxCommandEvent &event) {
-  ConnectionDialog dlg;
-
-  if (dlg.ShowModal() == wxID_OK) {
-    GetTrackerConfig().ap_server = dlg.GetServerValue();
-    GetTrackerConfig().ap_player = dlg.GetPlayerValue();
-    GetTrackerConfig().ap_password = dlg.GetPasswordValue();
-    GetTrackerConfig().Save();
-
-    AP_Connect(dlg.GetServerValue(), dlg.GetPlayerValue(),
-               dlg.GetPasswordValue());
-  }
-}
-
-void TrackerFrame::OnStateChanged(wxCommandEvent &event) {
-  tracker_panel_->UpdateIndicators();
-  Refresh();
-}
-
-void TrackerFrame::OnStatusChanged(wxCommandEvent &event) {
-  SetStatusText(event.GetString());
-}
diff --git a/tracker_frame.h b/tracker_frame.h
deleted file mode 100644
index 6f4e4fc..0000000
--- a/tracker_frame.h
+++ /dev/null
@@ -1,33 +0,0 @@
-#ifndef TRACKER_FRAME_H_86BD8DFB
-#define TRACKER_FRAME_H_86BD8DFB
-
-#include <wx/wxprec.h>
-
-#ifndef WX_PRECOMP
-#include <wx/wx.h>
-#endif
-
-class TrackerPanel;
-
-wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent);
-wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
-
-class TrackerFrame : public wxFrame {
- public:
-  TrackerFrame();
-
-  void SetStatusMessage(std::string message);
-
-  void UpdateIndicators();
-
- private:
-  void OnExit(wxCommandEvent &event);
-  void OnAbout(wxCommandEvent &event);
-  void OnConnect(wxCommandEvent &event);
-  void OnStateChanged(wxCommandEvent &event);
-  void OnStatusChanged(wxCommandEvent &event);
-
-  TrackerPanel *tracker_panel_;
-};
-
-#endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */
diff --git a/tracker_panel.cpp b/tracker_panel.cpp
deleted file mode 100644
index 0e0569b..0000000
--- a/tracker_panel.cpp
+++ /dev/null
@@ -1,149 +0,0 @@
-#include "tracker_panel.h"
-
-#include "ap_state.h"
-#include "area_popup.h"
-#include "game_data.h"
-#include "tracker_state.h"
-
-constexpr int AREA_ACTUAL_SIZE = 64;
-constexpr int AREA_BORDER_SIZE = 5;
-constexpr int AREA_EFFECTIVE_SIZE = AREA_ACTUAL_SIZE + AREA_BORDER_SIZE * 2;
-
-TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
-  map_image_ = wxImage("assets/lingo_map.png", wxBITMAP_TYPE_PNG);
-  if (!map_image_.IsOk()) {
-    return;
-  }
-
-  for (const MapArea &map_area : GetGameData().GetMapAreas()) {
-    AreaIndicator area;
-    area.area_id = map_area.id;
-
-    area.popup = new AreaPopup(this, map_area.id);
-    area.popup->SetPosition({0, 0});
-
-    areas_.push_back(area);
-  }
-
-  Redraw();
-
-  Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this);
-  Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this);
-}
-
-void TrackerPanel::UpdateIndicators() {
-  Redraw();
-
-  for (AreaIndicator &area : areas_) {
-    area.popup->UpdateIndicators();
-  }
-}
-
-void TrackerPanel::OnPaint(wxPaintEvent &event) {
-  if (GetSize() != rendered_.GetSize()) {
-    Redraw();
-  }
-
-  wxPaintDC dc(this);
-  dc.DrawBitmap(rendered_, 0, 0);
-
-  event.Skip();
-}
-
-void TrackerPanel::OnMouseMove(wxMouseEvent &event) {
-  for (AreaIndicator &area : areas_) {
-    if (area.real_x1 <= event.GetX() && event.GetX() < area.real_x2 &&
-        area.real_y1 <= event.GetY() && event.GetY() < area.real_y2) {
-      area.popup->Show();
-    } else {
-      area.popup->Hide();
-    }
-  }
-
-  event.Skip();
-}
-
-void TrackerPanel::Redraw() {
-  wxSize panel_size = GetSize();
-  wxSize image_size = map_image_.GetSize();
-
-  int final_x = 0;
-  int final_y = 0;
-  int final_width = panel_size.GetWidth();
-  int final_height = panel_size.GetHeight();
-
-  if (image_size.GetWidth() * panel_size.GetHeight() >
-      panel_size.GetWidth() * image_size.GetHeight()) {
-    final_height = (panel_size.GetWidth() * image_size.GetHeight()) /
-                   image_size.GetWidth();
-    final_y = (panel_size.GetHeight() - final_height) / 2;
-  } else {
-    final_width = (image_size.GetWidth() * panel_size.GetHeight()) /
-                  image_size.GetHeight();
-    final_x = (panel_size.GetWidth() - final_width) / 2;
-  }
-
-  rendered_ = wxBitmap(
-      map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL)
-          .Size(panel_size, {final_x, final_y}, 0, 0, 0));
-
-  wxMemoryDC dc;
-  dc.SelectObject(rendered_);
-
-  for (AreaIndicator &area : areas_) {
-    const wxBrush *brush_color = wxGREY_BRUSH;
-
-    const MapArea &map_area = GetGameData().GetMapArea(area.area_id);
-    bool has_reachable_unchecked = false;
-    bool has_unreachable_unchecked = false;
-    for (int section_id = 0; section_id < map_area.locations.size();
-         section_id++) {
-      if (!AP_HasCheckedGameLocation(area.area_id, section_id)) {
-        if (GetTrackerState().IsLocationReachable(area.area_id, section_id)) {
-          has_reachable_unchecked = true;
-        } else {
-          has_unreachable_unchecked = true;
-        }
-      }
-    }
-
-    if (has_reachable_unchecked && has_unreachable_unchecked) {
-      brush_color = wxYELLOW_BRUSH;
-    } else if (has_reachable_unchecked) {
-      brush_color = wxGREEN_BRUSH;
-    } else if (has_unreachable_unchecked) {
-      brush_color = wxRED_BRUSH;
-    }
-
-    int real_area_size =
-        final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
-    int actual_border_size =
-        real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE;
-    int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) *
-                                    final_width / image_size.GetWidth();
-    int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
-                                    final_width / image_size.GetWidth();
-
-    dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
-    dc.SetBrush(*brush_color);
-    dc.DrawRectangle({real_area_x, real_area_y},
-                     {real_area_size, real_area_size});
-
-    area.real_x1 = real_area_x;
-    area.real_x2 = real_area_x + real_area_size;
-    area.real_y1 = real_area_y;
-    area.real_y2 = real_area_y + real_area_size;
-
-    int popup_x =
-        final_x + map_area.map_x * final_width / image_size.GetWidth();
-    int popup_y =
-        final_y + map_area.map_y * final_width / image_size.GetWidth();
-    if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
-      popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
-    }
-    if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
-      popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
-    }
-    area.popup->SetPosition({popup_x, popup_y});
-  }
-}
diff --git a/tracker_panel.h b/tracker_panel.h
deleted file mode 100644
index a871f90..0000000
--- a/tracker_panel.h
+++ /dev/null
@@ -1,39 +0,0 @@
-#ifndef TRACKER_PANEL_H_D675A54D
-#define TRACKER_PANEL_H_D675A54D
-
-#include <wx/wxprec.h>
-
-#ifndef WX_PRECOMP
-#include <wx/wx.h>
-#endif
-
-class AreaPopup;
-
-class TrackerPanel : public wxPanel {
- public:
-  TrackerPanel(wxWindow *parent);
-
-  void UpdateIndicators();
-
- private:
-  struct AreaIndicator {
-    int area_id = -1;
-    AreaPopup *popup = nullptr;
-    int real_x1 = 0;
-    int real_y1 = 0;
-    int real_x2 = 0;
-    int real_y2 = 0;
-  };
- 
-  void OnPaint(wxPaintEvent &event);
-  void OnMouseMove(wxMouseEvent &event);
-
-  void Redraw();
-
-  wxImage map_image_;
-  wxBitmap rendered_;
-
-  std::vector<AreaIndicator> areas_;
-};
-
-#endif /* end of include guard: TRACKER_PANEL_H_D675A54D */
diff --git a/tracker_state.cpp b/tracker_state.cpp
deleted file mode 100644
index 858ec3e..0000000
--- a/tracker_state.cpp
+++ /dev/null
@@ -1,181 +0,0 @@
-#include "tracker_state.h"
-
-#include <list>
-#include <set>
-
-#include "ap_state.h"
-#include "game_data.h"
-
-bool IsDoorReachable_Helper(int door_id, const std::set<int>& reachable_rooms);
-
-bool IsPanelReachable_Helper(int panel_id,
-                             const std::set<int>& reachable_rooms) {
-  const Panel& panel_obj = GetGameData().GetPanel(panel_id);
-
-  if (!reachable_rooms.count(panel_obj.room)) {
-    return false;
-  }
-
-  if (panel_obj.name == "THE MASTER") {
-    int achievements_accessible = 0;
-
-    for (int achieve_id : GetGameData().GetAchievementPanels()) {
-      if (IsPanelReachable_Helper(achieve_id, reachable_rooms)) {
-        achievements_accessible++;
-
-        if (achievements_accessible >= AP_GetMasteryRequirement()) {
-          break;
-        }
-      }
-    }
-
-    return (achievements_accessible >= AP_GetMasteryRequirement());
-  }
-
-  for (int room_id : panel_obj.required_rooms) {
-    if (!reachable_rooms.count(room_id)) {
-      return false;
-    }
-  }
-
-  for (int door_id : panel_obj.required_doors) {
-    if (!IsDoorReachable_Helper(door_id, reachable_rooms)) {
-      return false;
-    }
-  }
-
-  if (AP_IsColorShuffle()) {
-    for (LingoColor color : panel_obj.colors) {
-      if (!AP_HasColorItem(color)) {
-        return false;
-      }
-    }
-  }
-
-  return true;
-}
-
-bool IsDoorReachable_Helper(int door_id, const std::set<int>& reachable_rooms) {
-  const Door& door_obj = GetGameData().GetDoor(door_id);
-
-  if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) {
-    if (!reachable_rooms.count(door_obj.room)) {
-      return false;
-    }
-
-    for (int panel_id : door_obj.panels) {
-      if (!IsPanelReachable_Helper(panel_id, reachable_rooms)) {
-        return false;
-      }
-    }
-
-    return true;
-  } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS &&
-             !door_obj.group_name.empty()) {
-    return AP_HasItem(door_obj.group_name);
-  } else {
-    bool has_item = AP_HasItem(door_obj.item_name);
-
-    if (!has_item) {
-      for (const ProgressiveRequirement& prog_req : door_obj.progressives) {
-        if (AP_HasItem(prog_req.item_name, prog_req.quantity)) {
-          has_item = true;
-          break;
-        }
-      }
-    }
-
-    return has_item;
-  }
-}
-
-void TrackerState::CalculateState() {
-  reachability_.clear();
-
-  std::set<int> reachable_rooms;
-
-  std::list<Exit> flood_boundary;
-  flood_boundary.push_back(
-      {.destination_room = GetGameData().GetRoomByName("Menu")});
-
-  bool reachable_changed = true;
-  while (reachable_changed) {
-    reachable_changed = false;
-
-    std::list<Exit> new_boundary;
-    for (const Exit& room_exit : flood_boundary) {
-      if (reachable_rooms.count(room_exit.destination_room)) {
-        continue;
-      }
-
-      bool valid_transition = false;
-      if (room_exit.door.has_value()) {
-        if (IsDoorReachable_Helper(*room_exit.door, reachable_rooms)) {
-          valid_transition = true;
-        } else if (AP_GetDoorShuffleMode() == kNO_DOORS) {
-          new_boundary.push_back(room_exit);
-        }
-      } else {
-        valid_transition = true;
-      }
-
-      if (valid_transition) {
-        reachable_rooms.insert(room_exit.destination_room);
-        reachable_changed = true;
-
-        const Room& room_obj =
-            GetGameData().GetRoom(room_exit.destination_room);
-        for (const Exit& out_edge : room_obj.exits) {
-          if (!out_edge.painting || !AP_IsPaintingShuffle()) {
-            new_boundary.push_back(out_edge);
-          }
-        }
-
-        if (AP_IsPaintingShuffle()) {
-          for (const PaintingExit& out_edge : room_obj.paintings) {
-            if (AP_GetPaintingMapping().count(out_edge.id)) {
-              Exit painting_exit;
-              painting_exit.destination_room = GetGameData().GetRoomForPainting(
-                  AP_GetPaintingMapping().at(out_edge.id));
-              painting_exit.door = out_edge.door;
-
-              new_boundary.push_back(painting_exit);
-            }
-          }
-        }
-      }
-    }
-
-    flood_boundary = new_boundary;
-  }
-
-  for (const MapArea& map_area : GetGameData().GetMapAreas()) {
-    for (int section_id = 0; section_id < map_area.locations.size();
-         section_id++) {
-      const Location& location_section = map_area.locations.at(section_id);
-      bool reachable = reachable_rooms.count(location_section.room);
-      if (reachable) {
-        for (int panel_id : location_section.panels) {
-          reachable &= IsPanelReachable_Helper(panel_id, reachable_rooms);
-        }
-      }
-
-      reachability_[{map_area.id, section_id}] = reachable;
-    }
-  }
-}
-
-bool TrackerState::IsLocationReachable(int area_id, int section_id) {
-  std::tuple<int, int> key = {area_id, section_id};
-
-  if (reachability_.count(key)) {
-    return reachability_.at(key);
-  } else {
-    return false;
-  }
-}
-
-TrackerState& GetTrackerState() {
-  static TrackerState* instance = new TrackerState();
-  return *instance;
-}
diff --git a/tracker_state.h b/tracker_state.h
deleted file mode 100644
index 879e6f2..0000000
--- a/tracker_state.h
+++ /dev/null
@@ -1,19 +0,0 @@
-#ifndef TRACKER_STATE_H_8639BC90
-#define TRACKER_STATE_H_8639BC90
-
-#include <map>
-#include <tuple>
-
-class TrackerState {
- public:
-  void CalculateState();
-
-  bool IsLocationReachable(int area_id, int section_id);
-
- private:
-  std::map<std::tuple<int, int>, bool> reachability_;
-};
-
-TrackerState& GetTrackerState();
-
-#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */
-- 
cgit 1.4.1