about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2023-05-05 15:46:58 -0400
committerStar Rauchenberger <fefferburbia@gmail.com>2023-05-05 15:46:58 -0400
commit149e7c0836927e14a926a952bd1a7f0d1b49e779 (patch)
tree2c7ac89387eb890d3d345217b79929e9a23f4ecf /src
parent0dace7831673170bd31eefa6bbe6e705211d3061 (diff)
downloadlingo-ap-tracker-149e7c0836927e14a926a952bd1a7f0d1b49e779.tar.gz
lingo-ap-tracker-149e7c0836927e14a926a952bd1a7f0d1b49e779.tar.bz2
lingo-ap-tracker-149e7c0836927e14a926a952bd1a7f0d1b49e779.zip
Organised repo
Diffstat (limited to 'src')
-rw-r--r--src/ap_state.cpp330
-rw-r--r--src/ap_state.h33
-rw-r--r--src/area_popup.cpp54
-rw-r--r--src/area_popup.h25
-rw-r--r--src/connection_dialog.cpp40
-rw-r--r--src/connection_dialog.h30
-rw-r--r--src/eye_indicator.cpp49
-rw-r--r--src/eye_indicator.h30
-rw-r--r--src/game_data.cpp406
-rw-r--r--src/game_data.h135
-rw-r--r--src/main.cpp21
-rw-r--r--src/tracker_config.cpp33
-rw-r--r--src/tracker_config.h19
-rw-r--r--src/tracker_frame.cpp86
-rw-r--r--src/tracker_frame.h33
-rw-r--r--src/tracker_panel.cpp149
-rw-r--r--src/tracker_panel.h39
-rw-r--r--src/tracker_state.cpp181
-rw-r--r--src/tracker_state.h19
19 files changed, 1712 insertions, 0 deletions
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 @@
1#include "ap_state.h"
2
3#define HAS_STD_FILESYSTEM
4#define _WEBSOCKETPP_CPP11_STRICT_
5#pragma comment(lib, "crypt32")
6
7#include <hkutil/string.h>
8
9#include <apclient.hpp>
10#include <apuuid.hpp>
11#include <chrono>
12#include <exception>
13#include <list>
14#include <memory>
15#include <mutex>
16#include <set>
17#include <thread>
18#include <tuple>
19
20#include "game_data.h"
21#include "tracker_frame.h"
22#include "tracker_state.h"
23
24constexpr int AP_MAJOR = 0;
25constexpr int AP_MINOR = 4;
26constexpr int AP_REVISION = 0;
27
28constexpr int ITEM_HANDLING = 7; // <- all
29
30namespace {
31
32APClient* apclient = nullptr;
33
34bool initialized = false;
35
36TrackerFrame* tracker_frame;
37
38bool client_active = false;
39std::mutex client_mutex;
40
41bool connected = false;
42bool has_connection_result = false;
43
44std::map<int64_t, int> inventory;
45std::set<int64_t> checked_locations;
46
47std::map<std::tuple<int, int>, int64_t> ap_id_by_location_id;
48std::map<std::string, int64_t> ap_id_by_item_name;
49std::map<LingoColor, int64_t> ap_id_by_color;
50std::map<int64_t, std::string> progressive_item_by_ap_id;
51
52DoorShuffleMode door_shuffle_mode = kNO_DOORS;
53bool color_shuffle = false;
54bool painting_shuffle = false;
55int mastery_requirement = 21;
56
57std::map<std::string, std::string> painting_mapping;
58
59void RefreshTracker() {
60 GetTrackerState().CalculateState();
61 tracker_frame->UpdateIndicators();
62}
63
64int64_t GetItemId(const std::string& item_name) {
65 int64_t ap_id = apclient->get_item_id(item_name);
66 if (ap_id == APClient::INVALID_NAME_ID) {
67 std::cout << "Could not find AP item ID for " << item_name << std::endl;
68 }
69
70 return ap_id;
71}
72
73void DestroyClient() {
74 client_active = false;
75 apclient->reset();
76 delete apclient;
77 apclient = nullptr;
78}
79
80} // namespace
81
82void AP_SetTrackerFrame(TrackerFrame* arg) { tracker_frame = arg; }
83
84void AP_Connect(std::string server, std::string player, std::string password) {
85 if (!initialized) {
86 std::thread([]() {
87 for (;;) {
88 {
89 std::lock_guard client_guard(client_mutex);
90 if (apclient) {
91 apclient->poll();
92 }
93 }
94
95 std::this_thread::sleep_for(std::chrono::milliseconds(100));
96 }
97 }).detach();
98
99 initialized = true;
100 }
101
102 tracker_frame->SetStatusMessage("Connecting to Archipelago server....");
103
104 {
105 std::lock_guard client_guard(client_mutex);
106
107 if (apclient) {
108 DestroyClient();
109 }
110
111 apclient = new APClient(ap_get_uuid(""), "Lingo", server);
112 }
113
114 inventory.clear();
115 checked_locations.clear();
116 door_shuffle_mode = kNO_DOORS;
117 color_shuffle = false;
118 painting_shuffle = false;
119 painting_mapping.clear();
120 mastery_requirement = 21;
121
122 connected = false;
123 has_connection_result = false;
124
125 apclient->set_room_info_handler([player, password]() {
126 inventory.clear();
127
128 tracker_frame->SetStatusMessage(
129 "Connected to Archipelago server. Authenticating...");
130
131 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
132 {AP_MAJOR, AP_MINOR, AP_REVISION});
133 });
134
135 apclient->set_location_checked_handler(
136 [](const std::list<int64_t>& locations) {
137 for (const int64_t location_id : locations) {
138 checked_locations.insert(location_id);
139 std::cout << "Location: " << location_id << std::endl;
140 }
141
142 RefreshTracker();
143 });
144
145 apclient->set_slot_disconnected_handler([]() {
146 tracker_frame->SetStatusMessage(
147 "Disconnected from Archipelago. Attempting to reconnect...");
148 });
149
150 apclient->set_socket_disconnected_handler([]() {
151 tracker_frame->SetStatusMessage(
152 "Disconnected from Archipelago. Attempting to reconnect...");
153 });
154
155 apclient->set_items_received_handler(
156 [](const std::list<APClient::NetworkItem>& items) {
157 for (const APClient::NetworkItem& item : items) {
158 inventory[item.item]++;
159 std::cout << "Item: " << item.item << std::endl;
160 }
161
162 RefreshTracker();
163 });
164
165 apclient->set_slot_connected_handler([](const nlohmann::json& slot_data) {
166 tracker_frame->SetStatusMessage("Connected to Archipelago!");
167
168 door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
169 color_shuffle = slot_data["shuffle_colors"].get<bool>();
170 painting_shuffle = slot_data["shuffle_paintings"].get<bool>();
171 mastery_requirement = slot_data["mastery_achievements"].get<int>();
172
173 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) {
174 painting_mapping.clear();
175
176 for (const auto& mapping_it :
177 slot_data["painting_entrance_to_exit"].items()) {
178 painting_mapping[mapping_it.key()] = mapping_it.value();
179 }
180 }
181
182 connected = true;
183 has_connection_result = true;
184
185 RefreshTracker();
186 });
187
188 apclient->set_slot_refused_handler([](const std::list<std::string>& errors) {
189 connected = false;
190 has_connection_result = true;
191
192 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
193
194 std::vector<std::string> error_messages;
195 error_messages.push_back("Could not connect to Archipelago.");
196
197 for (const std::string& error : errors) {
198 if (error == "InvalidSlot") {
199 error_messages.push_back("Invalid player name.");
200 } else if (error == "InvalidGame") {
201 error_messages.push_back("The specified player is not playing Lingo.");
202 } else if (error == "IncompatibleVersion") {
203 error_messages.push_back(
204 "The Archipelago server is not the correct version for this "
205 "client.");
206 } else if (error == "InvalidPassword") {
207 error_messages.push_back("Incorrect password.");
208 } else if (error == "InvalidItemsHandling") {
209 error_messages.push_back(
210 "Invalid item handling flag. This is a bug with the tracker. "
211 "Please report it to the lingo-ap-tracker GitHub.");
212 } else {
213 error_messages.push_back("Unknown error.");
214 }
215 }
216
217 std::string full_message = hatkirby::implode(error_messages, " ");
218
219 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
220 });
221
222 client_active = true;
223
224 int timeout = 5000; // 5 seconds
225 int interval = 100;
226 int remaining_loops = timeout / interval;
227 while (!has_connection_result) {
228 if (interval == 0) {
229 connected = false;
230 has_connection_result = true;
231
232 DestroyClient();
233
234 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
235
236 wxMessageBox("Timeout while connecting to Archipelago server.",
237 "Connection failed", wxOK | wxICON_ERROR);
238 }
239
240 std::this_thread::sleep_for(std::chrono::milliseconds(100));
241
242 interval--;
243 }
244
245 if (connected) {
246 for (const MapArea& map_area : GetGameData().GetMapAreas()) {
247 for (int section_id = 0; section_id < map_area.locations.size();
248 section_id++) {
249 const Location& location = map_area.locations.at(section_id);
250
251 int64_t ap_id = apclient->get_location_id(location.ap_location_name);
252 if (ap_id == APClient::INVALID_NAME_ID) {
253 std::cout << "Could not find AP location ID for "
254 << location.ap_location_name << std::endl;
255 } else {
256 ap_id_by_location_id[{map_area.id, section_id}] = ap_id;
257 }
258 }
259 }
260
261 for (const Door& door : GetGameData().GetDoors()) {
262 if (!door.skip_item) {
263 ap_id_by_item_name[door.item_name] = GetItemId(door.item_name);
264
265 if (!door.group_name.empty() &&
266 !ap_id_by_item_name.count(door.group_name)) {
267 ap_id_by_item_name[door.group_name] = GetItemId(door.group_name);
268 }
269
270 for (const ProgressiveRequirement& prog_req : door.progressives) {
271 ap_id_by_item_name[prog_req.item_name] =
272 GetItemId(prog_req.item_name);
273 }
274 }
275 }
276
277 ap_id_by_color[LingoColor::kBlack] = GetItemId("Black");
278 ap_id_by_color[LingoColor::kRed] = GetItemId("Red");
279 ap_id_by_color[LingoColor::kBlue] = GetItemId("Blue");
280 ap_id_by_color[LingoColor::kYellow] = GetItemId("Yellow");
281 ap_id_by_color[LingoColor::kPurple] = GetItemId("Purple");
282 ap_id_by_color[LingoColor::kOrange] = GetItemId("Orange");
283 ap_id_by_color[LingoColor::kGreen] = GetItemId("Green");
284 ap_id_by_color[LingoColor::kBrown] = GetItemId("Brown");
285 ap_id_by_color[LingoColor::kGray] = GetItemId("Gray");
286
287 RefreshTracker();
288 } else {
289 client_active = false;
290 }
291}
292
293bool AP_HasCheckedGameLocation(int area_id, int section_id) {
294 std::tuple<int, int> location_key = {area_id, section_id};
295
296 if (ap_id_by_location_id.count(location_key)) {
297 return checked_locations.count(ap_id_by_location_id.at(location_key));
298 } else {
299 return false;
300 }
301}
302
303bool AP_HasColorItem(LingoColor color) {
304 if (ap_id_by_color.count(color)) {
305 return inventory.count(ap_id_by_color.at(color));
306 } else {
307 return false;
308 }
309}
310
311bool AP_HasItem(const std::string& item, int quantity) {
312 if (ap_id_by_item_name.count(item)) {
313 int64_t ap_id = ap_id_by_item_name.at(item);
314 return inventory.count(ap_id) && inventory.at(ap_id) >= quantity;
315 } else {
316 return false;
317 }
318}
319
320DoorShuffleMode AP_GetDoorShuffleMode() { return door_shuffle_mode; }
321
322bool AP_IsColorShuffle() { return color_shuffle; }
323
324bool AP_IsPaintingShuffle() { return painting_shuffle; }
325
326const std::map<std::string, std::string> AP_GetPaintingMapping() {
327 return painting_mapping;
328}
329
330int 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 @@
1#ifndef AP_STATE_H_664A4180
2#define AP_STATE_H_664A4180
3
4#include <map>
5#include <string>
6
7#include "game_data.h"
8
9class TrackerFrame;
10
11enum DoorShuffleMode { kNO_DOORS = 0, kSIMPLE_DOORS = 1, kCOMPLEX_DOORS = 2 };
12
13void AP_SetTrackerFrame(TrackerFrame* tracker_frame);
14
15void AP_Connect(std::string server, std::string player, std::string password);
16
17bool AP_HasCheckedGameLocation(int area_id, int section_id);
18
19bool AP_HasColorItem(LingoColor color);
20
21bool AP_HasItem(const std::string& item, int quantity = 1);
22
23DoorShuffleMode AP_GetDoorShuffleMode();
24
25bool AP_IsColorShuffle();
26
27bool AP_IsPaintingShuffle();
28
29const std::map<std::string, std::string> AP_GetPaintingMapping();
30
31int AP_GetMasteryRequirement();
32
33#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 @@
1#include "area_popup.h"
2
3#include "ap_state.h"
4#include "game_data.h"
5#include "tracker_state.h"
6
7AreaPopup::AreaPopup(wxWindow* parent, int area_id)
8 : wxPanel(parent, wxID_ANY), area_id_(area_id) {
9 const MapArea& map_area = GetGameData().GetMapArea(area_id);
10
11 wxFlexGridSizer* section_sizer = new wxFlexGridSizer(2, 10, 10);
12
13 for (const Location& location : map_area.locations) {
14 EyeIndicator* eye_indicator = new EyeIndicator(this);
15 section_sizer->Add(eye_indicator, wxSizerFlags().Expand());
16 eye_indicators_.push_back(eye_indicator);
17
18 wxStaticText* section_label = new wxStaticText(this, -1, location.name);
19 section_label->SetForegroundColour(*wxWHITE);
20 section_sizer->Add(
21 section_label,
22 wxSizerFlags().Align(wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL));
23 section_labels_.push_back(section_label);
24 }
25
26 wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
27
28 wxStaticText* top_label = new wxStaticText(this, -1, map_area.name);
29 top_label->SetForegroundColour(*wxWHITE);
30 top_label->SetFont(top_label->GetFont().Bold());
31 top_sizer->Add(top_label,
32 wxSizerFlags().Center().DoubleBorder(wxUP | wxLEFT | wxRIGHT));
33
34 top_sizer->Add(section_sizer, wxSizerFlags().DoubleBorder(wxALL).Expand());
35
36 SetSizerAndFit(top_sizer);
37
38 SetBackgroundColour(*wxBLACK);
39 Hide();
40}
41
42void AreaPopup::UpdateIndicators() {
43 const MapArea& map_area = GetGameData().GetMapArea(area_id_);
44 for (int section_id = 0; section_id < map_area.locations.size();
45 section_id++) {
46 bool checked = AP_HasCheckedGameLocation(area_id_, section_id);
47 bool reachable =
48 GetTrackerState().IsLocationReachable(area_id_, section_id);
49 const wxColour* text_color = reachable ? wxWHITE : wxRED;
50
51 section_labels_[section_id]->SetForegroundColour(*text_color);
52 eye_indicators_[section_id]->SetChecked(checked);
53 }
54}
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 @@
1#ifndef AREA_POPUP_H_03FAC988
2#define AREA_POPUP_H_03FAC988
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include "eye_indicator.h"
11
12class AreaPopup : public wxPanel {
13 public:
14 AreaPopup(wxWindow* parent, int area_id);
15
16 void UpdateIndicators();
17
18 private:
19 int area_id_;
20
21 std::vector<wxStaticText*> section_labels_;
22 std::vector<EyeIndicator*> eye_indicators_;
23};
24
25#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 @@
1#include "connection_dialog.h"
2
3#include "tracker_config.h"
4
5ConnectionDialog::ConnectionDialog()
6 : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") {
7 server_box_ = new wxTextCtrl(this, -1, GetTrackerConfig().ap_server, wxDefaultPosition,
8 {300, -1});
9 player_box_ = new wxTextCtrl(this, -1, GetTrackerConfig().ap_player, wxDefaultPosition,
10 {300, -1});
11 password_box_ = new wxTextCtrl(this, -1, GetTrackerConfig().ap_password,
12 wxDefaultPosition, {300, -1});
13
14 wxFlexGridSizer* form_sizer = new wxFlexGridSizer(2, 10, 10);
15
16 form_sizer->Add(
17 new wxStaticText(this, -1, "Server:"),
18 wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
19 form_sizer->Add(server_box_, wxSizerFlags().Expand());
20 form_sizer->Add(
21 new wxStaticText(this, -1, "Player:"),
22 wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
23 form_sizer->Add(player_box_, wxSizerFlags().Expand());
24 form_sizer->Add(
25 new wxStaticText(this, -1, "Password:"),
26 wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
27 form_sizer->Add(password_box_, wxSizerFlags().Expand());
28
29 wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
30 top_sizer->Add(new wxStaticText(
31 this, -1, "Enter the details to connect to Archipelago."),
32 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder());
33 top_sizer->Add(form_sizer, wxSizerFlags().DoubleBorder().Expand());
34 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Center());
35
36 SetSizerAndFit(top_sizer);
37
38 Center();
39 server_box_->SetFocus();
40}
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 @@
1#ifndef CONNECTION_DIALOG_H_E526D0E7
2#define CONNECTION_DIALOG_H_E526D0E7
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <string>
11
12class ConnectionDialog : public wxDialog {
13 public:
14 ConnectionDialog();
15
16 std::string GetServerValue() { return server_box_->GetValue().ToStdString(); }
17
18 std::string GetPlayerValue() { return player_box_->GetValue().ToStdString(); }
19
20 std::string GetPasswordValue() {
21 return password_box_->GetValue().ToStdString();
22 }
23
24 private:
25 wxTextCtrl* server_box_;
26 wxTextCtrl* player_box_;
27 wxTextCtrl* password_box_;
28};
29
30#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 @@
1#include "eye_indicator.h"
2
3EyeIndicator::EyeIndicator(wxWindow* parent) : wxWindow(parent, wxID_ANY) {
4 SetMinSize({32, 32});
5
6 Redraw();
7
8 Bind(wxEVT_PAINT, &EyeIndicator::OnPaint, this);
9}
10
11void EyeIndicator::SetChecked(bool checked) {
12 if (intended_checked_ != checked) {
13 intended_checked_ = checked;
14
15 Redraw();
16 }
17}
18
19const wxImage& EyeIndicator::GetUncheckedImage() {
20 static wxImage* unchecked_image =
21 new wxImage("assets/unchecked.png", wxBITMAP_TYPE_PNG);
22 return *unchecked_image;
23}
24
25const wxImage& EyeIndicator::GetCheckedImage() {
26 static wxImage* checked_image =
27 new wxImage("assets/checked.png", wxBITMAP_TYPE_PNG);
28 return *checked_image;
29}
30
31void EyeIndicator::OnPaint(wxPaintEvent& event) {
32 if (GetSize() != rendered_.GetSize() ||
33 intended_checked_ != rendered_checked_) {
34 Redraw();
35 }
36
37 wxPaintDC dc(this);
38 dc.DrawBitmap(rendered_, 0, 0);
39
40 event.Skip();
41}
42
43void EyeIndicator::Redraw() {
44 rendered_ =
45 wxBitmap((intended_checked_ ? GetCheckedImage() : GetUncheckedImage())
46 .Scale(GetSize().GetWidth(), GetSize().GetHeight(),
47 wxIMAGE_QUALITY_NORMAL));
48 rendered_checked_ = intended_checked_;
49}
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 @@
1#ifndef EYE_INDICATOR_H_778150F2
2#define EYE_INDICATOR_H_778150F2
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10class EyeIndicator : public wxWindow {
11 public:
12 EyeIndicator(wxWindow* parent);
13
14 void SetChecked(bool checked);
15
16 private:
17 static const wxImage& GetUncheckedImage();
18 static const wxImage& GetCheckedImage();
19
20 void OnPaint(wxPaintEvent& event);
21
22 void Redraw();
23
24 bool intended_checked_ = false;
25
26 wxBitmap rendered_;
27 bool rendered_checked_ = false;
28};
29
30#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 @@
1#include "game_data.h"
2
3#include <hkutil/string.h>
4#include <yaml-cpp/yaml.h>
5
6#include <iostream>
7
8LingoColor GetColorForString(const std::string &str) {
9 if (str == "black") {
10 return LingoColor::kBlack;
11 } else if (str == "red") {
12 return LingoColor::kRed;
13 } else if (str == "blue") {
14 return LingoColor::kBlue;
15 } else if (str == "yellow") {
16 return LingoColor::kYellow;
17 } else if (str == "orange") {
18 return LingoColor::kOrange;
19 } else if (str == "green") {
20 return LingoColor::kGreen;
21 } else if (str == "gray") {
22 return LingoColor::kGray;
23 } else if (str == "brown") {
24 return LingoColor::kBrown;
25 } else if (str == "purple") {
26 return LingoColor::kPurple;
27 } else {
28 std::cout << "Invalid color: " << str << std::endl;
29 return LingoColor::kNone;
30 }
31}
32
33GameData::GameData() {
34 YAML::Node lingo_config = YAML::LoadFile("assets/LL1.yaml");
35 YAML::Node areas_config = YAML::LoadFile("assets/areas.yaml");
36
37 rooms_.reserve(lingo_config.size() * 2);
38
39 for (const auto &room_it : lingo_config) {
40 int room_id = AddOrGetRoom(room_it.first.as<std::string>());
41 Room &room_obj = rooms_[room_id];
42
43 for (const auto &entrance_it : room_it.second["entrances"]) {
44 int from_room_id = AddOrGetRoom(entrance_it.first.as<std::string>());
45 Room &from_room_obj = rooms_[from_room_id];
46
47 switch (entrance_it.second.Type()) {
48 case YAML::NodeType::Scalar: {
49 // This is just "true".
50 from_room_obj.exits.push_back({.destination_room = room_id});
51 break;
52 }
53 case YAML::NodeType::Map: {
54 Exit exit_obj;
55 exit_obj.destination_room = room_id;
56
57 if (entrance_it.second["door"]) {
58 std::string door_room = room_obj.name;
59 if (entrance_it.second["room"]) {
60 door_room = entrance_it.second["room"].as<std::string>();
61 }
62 exit_obj.door = AddOrGetDoor(
63 door_room, entrance_it.second["door"].as<std::string>());
64 }
65
66 if (entrance_it.second["painting"]) {
67 exit_obj.painting = entrance_it.second["painting"].as<bool>();
68 }
69
70 from_room_obj.exits.push_back(exit_obj);
71 break;
72 }
73 case YAML::NodeType::Sequence: {
74 for (const auto &option : entrance_it.second) {
75 Exit exit_obj;
76 exit_obj.destination_room = room_id;
77
78 std::string door_room = room_obj.name;
79 if (option["room"]) {
80 door_room = option["room"].as<std::string>();
81 }
82 exit_obj.door =
83 AddOrGetDoor(door_room, option["door"].as<std::string>());
84
85 if (option["painting"]) {
86 exit_obj.painting = option["painting"].as<bool>();
87 }
88
89 from_room_obj.exits.push_back(exit_obj);
90 }
91
92 break;
93 }
94 default: {
95 // This shouldn't happen.
96 std::cout << "Error reading game data: " << entrance_it << std::endl;
97 break;
98 }
99 }
100 }
101
102 if (room_it.second["panels"]) {
103 for (const auto &panel_it : room_it.second["panels"]) {
104 int panel_id =
105 AddOrGetPanel(room_obj.name, panel_it.first.as<std::string>());
106 Panel &panel_obj = panels_[panel_id];
107
108 if (panel_it.second["colors"]) {
109 if (panel_it.second["colors"].IsScalar()) {
110 panel_obj.colors.push_back(
111 GetColorForString(panel_it.second["colors"].as<std::string>()));
112 } else {
113 for (const auto &color_node : panel_it.second["colors"]) {
114 panel_obj.colors.push_back(
115 GetColorForString(color_node.as<std::string>()));
116 }
117 }
118 }
119
120 if (panel_it.second["required_room"]) {
121 if (panel_it.second["required_room"].IsScalar()) {
122 panel_obj.required_rooms.push_back(AddOrGetRoom(
123 panel_it.second["required_room"].as<std::string>()));
124 } else {
125 for (const auto &rr_node : panel_it.second["required_room"]) {
126 panel_obj.required_rooms.push_back(
127 AddOrGetRoom(rr_node.as<std::string>()));
128 }
129 }
130 }
131
132 if (panel_it.second["required_door"]) {
133 if (panel_it.second["required_door"].IsMap()) {
134 std::string rd_room = room_obj.name;
135 if (panel_it.second["required_door"]["room"]) {
136 rd_room =
137 panel_it.second["required_door"]["room"].as<std::string>();
138 }
139
140 panel_obj.required_doors.push_back(AddOrGetDoor(
141 rd_room,
142 panel_it.second["required_door"]["door"].as<std::string>()));
143 } else {
144 for (const auto &rr_node : panel_it.second["required_door"]) {
145 std::string rd_room = room_obj.name;
146 if (rr_node["room"]) {
147 rd_room = rr_node["room"].as<std::string>();
148 }
149
150 panel_obj.required_doors.push_back(
151 AddOrGetDoor(rd_room, rr_node["door"].as<std::string>()));
152 }
153 }
154 }
155
156 if (panel_it.second["check"]) {
157 panel_obj.check = panel_it.second["check"].as<bool>();
158 }
159
160 if (panel_it.second["achievement"]) {
161 panel_obj.achievement = panel_it.second["achievement"].as<bool>();
162
163 if (panel_obj.achievement) {
164 achievement_panels_.push_back(panel_id);
165 }
166 }
167
168 if (panel_it.second["exclude_reduce"]) {
169 panel_obj.exclude_reduce =
170 panel_it.second["exclude_reduce"].as<bool>();
171 }
172 }
173 }
174
175 if (room_it.second["doors"]) {
176 for (const auto &door_it : room_it.second["doors"]) {
177 int door_id =
178 AddOrGetDoor(room_obj.name, door_it.first.as<std::string>());
179 Door &door_obj = doors_[door_id];
180
181 bool has_external_panels = false;
182 std::vector<std::string> panel_names;
183
184 for (const auto &panel_node : door_it.second["panels"]) {
185 if (panel_node.IsScalar()) {
186 panel_names.push_back(panel_node.as<std::string>());
187 door_obj.panels.push_back(
188 AddOrGetPanel(room_obj.name, panel_node.as<std::string>()));
189 } else {
190 has_external_panels = true;
191 panel_names.push_back(panel_node["panel"].as<std::string>());
192 door_obj.panels.push_back(
193 AddOrGetPanel(panel_node["room"].as<std::string>(),
194 panel_node["panel"].as<std::string>()));
195 }
196 }
197
198 if (door_it.second["skip_location"]) {
199 door_obj.skip_location = door_it.second["skip_location"].as<bool>();
200 }
201
202 if (door_it.second["skip_item"]) {
203 door_obj.skip_item = door_it.second["skip_item"].as<bool>();
204 }
205
206 if (door_it.second["event"]) {
207 door_obj.skip_location = door_it.second["event"].as<bool>();
208 door_obj.skip_item = door_it.second["event"].as<bool>();
209 }
210
211 if (door_it.second["item_name"]) {
212 door_obj.item_name = door_it.second["item_name"].as<std::string>();
213 } else if (!door_it.second["skip_item"] && !door_it.second["event"]) {
214 door_obj.item_name = room_obj.name + " - " + door_obj.name;
215 }
216
217 if (door_it.second["group"]) {
218 door_obj.group_name = door_it.second["group"].as<std::string>();
219 }
220
221 if (door_it.second["location_name"]) {
222 door_obj.location_name =
223 door_it.second["location_name"].as<std::string>();
224 } else if (!door_it.second["skip_location"] &&
225 !door_it.second["event"]) {
226 if (has_external_panels) {
227 std::cout
228 << room_obj.name << " - " << door_obj.name
229 << " has panels from other rooms but does not have an explicit "
230 "location name and is not marked skip_location or event"
231 << std::endl;
232 }
233
234 door_obj.location_name =
235 room_obj.name + " - " + hatkirby::implode(panel_names, ", ");
236 }
237
238 if (door_it.second["include_reduce"]) {
239 door_obj.exclude_reduce =
240 !door_it.second["include_reduce"].as<bool>();
241 }
242 }
243 }
244
245 if (room_it.second["paintings"]) {
246 for (const auto &painting : room_it.second["paintings"]) {
247 std::string painting_id = painting["id"].as<std::string>();
248 room_by_painting_[painting_id] = room_id;
249
250 if (!painting["exit_only"] || !painting["exit_only"].as<bool>()) {
251 PaintingExit painting_exit;
252 painting_exit.id = painting_id;
253
254 if (painting["required_door"]) {
255 std::string rd_room = room_obj.name;
256 if (painting["required_door"]["room"]) {
257 rd_room = painting["required_door"]["room"].as<std::string>();
258 }
259
260 painting_exit.door = AddOrGetDoor(
261 rd_room, painting["required_door"]["door"].as<std::string>());
262 }
263
264 room_obj.paintings.push_back(painting_exit);
265 }
266 }
267 }
268
269 if (room_it.second["progression"]) {
270 for (const auto &progression_it : room_it.second["progression"]) {
271 std::string progressive_item_name =
272 progression_it.first.as<std::string>();
273
274 int index = 1;
275 for (const auto &stage : progression_it.second) {
276 int door_id = -1;
277
278 if (stage.IsScalar()) {
279 door_id = AddOrGetDoor(room_obj.name, stage.as<std::string>());
280 } else {
281 door_id = AddOrGetDoor(stage["room"].as<std::string>(),
282 stage["door"].as<std::string>());
283 }
284
285 doors_[door_id].progressives.push_back(
286 {.item_name = progressive_item_name, .quantity = index});
287 index++;
288 }
289 }
290 }
291 }
292
293 map_areas_.reserve(areas_config.size());
294
295 std::map<std::string, int> fold_areas;
296 for (const auto &area_it : areas_config) {
297 if (area_it.second["map"]) {
298 int area_id = AddOrGetArea(area_it.first.as<std::string>());
299 MapArea &area_obj = map_areas_[area_id];
300 area_obj.map_x = area_it.second["map"][0].as<int>();
301 area_obj.map_y = area_it.second["map"][1].as<int>();
302 } else if (area_it.second["fold_into"]) {
303 fold_areas[area_it.first.as<std::string>()] =
304 AddOrGetArea(area_it.second["fold_into"].as<std::string>());
305 }
306 }
307
308 for (const Panel &panel : panels_) {
309 if (panel.check) {
310 int room_id = panel.room;
311 std::string room_name = rooms_[room_id].name;
312
313 std::string area_name = room_name;
314 if (fold_areas.count(room_name)) {
315 int fold_area_id = fold_areas[room_name];
316 area_name = map_areas_[fold_area_id].name;
317 }
318
319 int area_id = AddOrGetArea(area_name);
320 MapArea &map_area = map_areas_[area_id];
321 // room field should be the original room ID
322 map_area.locations.push_back(
323 {.name = panel.name,
324 .ap_location_name = room_name + " - " + panel.name,
325 .room = panel.room,
326 .panels = {panel.id}});
327 }
328 }
329
330 for (const Door &door : doors_) {
331 if (!door.skip_location) {
332 int room_id = door.room;
333 std::string area_name = rooms_[room_id].name;
334 std::string section_name;
335
336 size_t divider_pos = door.location_name.find(" - ");
337 if (divider_pos == std::string::npos) {
338 section_name = door.location_name;
339 } else {
340 area_name = door.location_name.substr(0, divider_pos);
341 section_name = door.location_name.substr(divider_pos + 3);
342 }
343
344 if (fold_areas.count(area_name)) {
345 int fold_area_id = fold_areas[area_name];
346 area_name = map_areas_[fold_area_id].name;
347 }
348
349 int area_id = AddOrGetArea(area_name);
350 MapArea &map_area = map_areas_[area_id];
351 // room field should be the original room ID
352 map_area.locations.push_back({.name = section_name,
353 .ap_location_name = door.location_name,
354 .room = door.room,
355 .panels = door.panels});
356 }
357 }
358}
359
360int GameData::AddOrGetRoom(std::string room) {
361 if (!room_by_id_.count(room)) {
362 room_by_id_[room] = rooms_.size();
363 rooms_.push_back({.name = room});
364 }
365
366 return room_by_id_[room];
367}
368
369int GameData::AddOrGetDoor(std::string room, std::string door) {
370 std::string full_name = room + " - " + door;
371
372 if (!door_by_id_.count(full_name)) {
373 door_by_id_[full_name] = doors_.size();
374 doors_.push_back({.room = AddOrGetRoom(room), .name = door});
375 }
376
377 return door_by_id_[full_name];
378}
379
380int GameData::AddOrGetPanel(std::string room, std::string panel) {
381 std::string full_name = room + " - " + panel;
382
383 if (!panel_by_id_.count(full_name)) {
384 int panel_id = panels_.size();
385 panel_by_id_[full_name] = panel_id;
386 panels_.push_back(
387 {.id = panel_id, .room = AddOrGetRoom(room), .name = panel});
388 }
389
390 return panel_by_id_[full_name];
391}
392
393int GameData::AddOrGetArea(std::string area) {
394 if (!area_by_id_.count(area)) {
395 int area_id = map_areas_.size();
396 area_by_id_[area] = area_id;
397 map_areas_.push_back({.id = area_id, .name = area});
398 }
399
400 return area_by_id_[area];
401}
402
403const GameData &GetGameData() {
404 static GameData *instance = new GameData();
405 return *instance;
406}
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 @@
1#ifndef GAME_DATA_H_9C42AC51
2#define GAME_DATA_H_9C42AC51
3
4#include <map>
5#include <optional>
6#include <string>
7#include <vector>
8
9enum class LingoColor {
10 kNone,
11 kBlack,
12 kRed,
13 kBlue,
14 kYellow,
15 kGreen,
16 kOrange,
17 kPurple,
18 kBrown,
19 kGray
20};
21
22struct Panel {
23 int id;
24 int room;
25 std::string name;
26 std::vector<LingoColor> colors;
27 std::vector<int> required_rooms;
28 std::vector<int> required_doors;
29 bool check = false;
30 bool exclude_reduce = false;
31 bool achievement = false;
32};
33
34struct ProgressiveRequirement {
35 std::string item_name;
36 int quantity = 0;
37};
38
39struct Door {
40 int room;
41 std::string name;
42 std::string location_name;
43 std::string item_name;
44 std::string group_name;
45 bool skip_location = false;
46 bool skip_item = false;
47 std::vector<int> panels;
48 bool exclude_reduce = true;
49 std::vector<ProgressiveRequirement> progressives;
50};
51
52struct Exit {
53 int destination_room;
54 std::optional<int> door;
55 bool painting = false;
56};
57
58struct PaintingExit {
59 std::string id;
60 std::optional<int> door;
61};
62
63struct Room {
64 std::string name;
65 std::vector<Exit> exits;
66 std::vector<PaintingExit> paintings;
67};
68
69struct Location {
70 std::string name;
71 std::string ap_location_name;
72 int room;
73 std::vector<int> panels;
74};
75
76struct MapArea {
77 int id;
78 std::string name;
79 std::vector<Location> locations;
80 int map_x;
81 int map_y;
82};
83
84class GameData {
85 public:
86 GameData();
87
88 const std::vector<MapArea>& GetMapAreas() const { return map_areas_; }
89
90 const MapArea& GetMapArea(int id) const { return map_areas_.at(id); }
91
92 int GetRoomByName(const std::string& name) const {
93 return room_by_id_.at(name);
94 }
95
96 const Room& GetRoom(int room_id) const { return rooms_.at(room_id); }
97
98 const std::vector<Door>& GetDoors() const { return doors_; }
99
100 const Door& GetDoor(int door_id) const { return doors_.at(door_id); }
101
102 const Panel& GetPanel(int panel_id) const { return panels_.at(panel_id); }
103
104 int GetRoomForPainting(const std::string& painting_id) const {
105 return room_by_painting_.at(painting_id);
106 }
107
108 const std::vector<int>& GetAchievementPanels() const {
109 return achievement_panels_;
110 }
111
112 private:
113 int AddOrGetRoom(std::string room);
114 int AddOrGetDoor(std::string room, std::string door);
115 int AddOrGetPanel(std::string room, std::string panel);
116 int AddOrGetArea(std::string area);
117
118 std::vector<Room> rooms_;
119 std::vector<Door> doors_;
120 std::vector<Panel> panels_;
121 std::vector<MapArea> map_areas_;
122
123 std::map<std::string, int> room_by_id_;
124 std::map<std::string, int> door_by_id_;
125 std::map<std::string, int> panel_by_id_;
126 std::map<std::string, int> area_by_id_;
127
128 std::map<std::string, int> room_by_painting_;
129
130 std::vector<int> achievement_panels_;
131};
132
133const GameData& GetGameData();
134
135#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 @@
1#include <wx/wxprec.h>
2
3#ifndef WX_PRECOMP
4#include <wx/wx.h>
5#endif
6
7#include "tracker_config.h"
8#include "tracker_frame.h"
9
10class TrackerApp : public wxApp {
11 public:
12 virtual bool OnInit() {
13 GetTrackerConfig().Load();
14
15 TrackerFrame *frame = new TrackerFrame();
16 frame->Show(true);
17 return true;
18 }
19};
20
21wxIMPLEMENT_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 @@
1#include "tracker_config.h"
2
3#include <fstream>
4#include <yaml-cpp/yaml.h>
5
6constexpr const char* CONFIG_FILE_NAME = "config.yaml";
7
8void TrackerConfig::Load() {
9 try {
10 YAML::Node file = YAML::LoadFile(CONFIG_FILE_NAME);
11
12 ap_server = file["ap_server"].as<std::string>();
13 ap_player = file["ap_player"].as<std::string>();
14 ap_password = file["ap_password"].as<std::string>();
15 } catch (const std::exception&) {
16 // It's fine if the file can't be loaded.
17 }
18}
19
20void TrackerConfig::Save() {
21 YAML::Node output;
22 output["ap_server"] = ap_server;
23 output["ap_player"] = ap_player;
24 output["ap_password"] = ap_password;
25
26 std::ofstream filewriter(CONFIG_FILE_NAME);
27 filewriter << output;
28}
29
30TrackerConfig& GetTrackerConfig() {
31 static TrackerConfig* instance = new TrackerConfig();
32 return *instance;
33}
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 @@
1#ifndef TRACKER_CONFIG_H_36CDD648
2#define TRACKER_CONFIG_H_36CDD648
3
4#include <string>
5
6class TrackerConfig {
7 public:
8 void Load();
9
10 void Save();
11
12 std::string ap_server;
13 std::string ap_player;
14 std::string ap_password;
15};
16
17TrackerConfig& GetTrackerConfig();
18
19#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 @@
1#include "tracker_frame.h"
2
3#include "ap_state.h"
4#include "connection_dialog.h"
5#include "tracker_config.h"
6#include "tracker_panel.h"
7
8enum TrackerFrameIds { ID_CONNECT = 1 };
9
10wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent);
11wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
12
13TrackerFrame::TrackerFrame()
14 : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition,
15 wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxFULL_REPAINT_ON_RESIZE) {
16 ::wxInitAllImageHandlers();
17
18 AP_SetTrackerFrame(this);
19
20 SetSize(1280, 728);
21
22 wxMenu *menuFile = new wxMenu();
23 menuFile->Append(ID_CONNECT, "&Connect");
24 menuFile->Append(wxID_EXIT);
25
26 wxMenu *menuHelp = new wxMenu();
27 menuHelp->Append(wxID_ABOUT);
28
29 wxMenuBar *menuBar = new wxMenuBar();
30 menuBar->Append(menuFile, "&File");
31 menuBar->Append(menuHelp, "&Help");
32
33 SetMenuBar(menuBar);
34
35 CreateStatusBar();
36 SetStatusText("Not connected to Archipelago.");
37
38 Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT);
39 Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT);
40 Bind(wxEVT_MENU, &TrackerFrame::OnConnect, this, ID_CONNECT);
41 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
42 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);
43
44 tracker_panel_ = new TrackerPanel(this);
45}
46
47void TrackerFrame::SetStatusMessage(std::string message) {
48 wxCommandEvent *event = new wxCommandEvent(STATUS_CHANGED);
49 event->SetString(message.c_str());
50
51 QueueEvent(event);
52}
53
54void TrackerFrame::UpdateIndicators() {
55 QueueEvent(new wxCommandEvent(STATE_CHANGED));
56}
57
58void TrackerFrame::OnAbout(wxCommandEvent &event) {
59 wxMessageBox("Lingo Archipelago Tracker by hatkirby",
60 "About lingo-ap-tracker", wxOK | wxICON_INFORMATION);
61}
62
63void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); }
64
65void TrackerFrame::OnConnect(wxCommandEvent &event) {
66 ConnectionDialog dlg;
67
68 if (dlg.ShowModal() == wxID_OK) {
69 GetTrackerConfig().ap_server = dlg.GetServerValue();
70 GetTrackerConfig().ap_player = dlg.GetPlayerValue();
71 GetTrackerConfig().ap_password = dlg.GetPasswordValue();
72 GetTrackerConfig().Save();
73
74 AP_Connect(dlg.GetServerValue(), dlg.GetPlayerValue(),
75 dlg.GetPasswordValue());
76 }
77}
78
79void TrackerFrame::OnStateChanged(wxCommandEvent &event) {
80 tracker_panel_->UpdateIndicators();
81 Refresh();
82}
83
84void TrackerFrame::OnStatusChanged(wxCommandEvent &event) {
85 SetStatusText(event.GetString());
86}
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 @@
1#ifndef TRACKER_FRAME_H_86BD8DFB
2#define TRACKER_FRAME_H_86BD8DFB
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10class TrackerPanel;
11
12wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent);
13wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
14
15class TrackerFrame : public wxFrame {
16 public:
17 TrackerFrame();
18
19 void SetStatusMessage(std::string message);
20
21 void UpdateIndicators();
22
23 private:
24 void OnExit(wxCommandEvent &event);
25 void OnAbout(wxCommandEvent &event);
26 void OnConnect(wxCommandEvent &event);
27 void OnStateChanged(wxCommandEvent &event);
28 void OnStatusChanged(wxCommandEvent &event);
29
30 TrackerPanel *tracker_panel_;
31};
32
33#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 @@
1#include "tracker_panel.h"
2
3#include "ap_state.h"
4#include "area_popup.h"
5#include "game_data.h"
6#include "tracker_state.h"
7
8constexpr int AREA_ACTUAL_SIZE = 64;
9constexpr int AREA_BORDER_SIZE = 5;
10constexpr int AREA_EFFECTIVE_SIZE = AREA_ACTUAL_SIZE + AREA_BORDER_SIZE * 2;
11
12TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
13 map_image_ = wxImage("assets/lingo_map.png", wxBITMAP_TYPE_PNG);
14 if (!map_image_.IsOk()) {
15 return;
16 }
17
18 for (const MapArea &map_area : GetGameData().GetMapAreas()) {
19 AreaIndicator area;
20 area.area_id = map_area.id;
21
22 area.popup = new AreaPopup(this, map_area.id);
23 area.popup->SetPosition({0, 0});
24
25 areas_.push_back(area);
26 }
27
28 Redraw();
29
30 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this);
31 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this);
32}
33
34void TrackerPanel::UpdateIndicators() {
35 Redraw();
36
37 for (AreaIndicator &area : areas_) {
38 area.popup->UpdateIndicators();
39 }
40}
41
42void TrackerPanel::OnPaint(wxPaintEvent &event) {
43 if (GetSize() != rendered_.GetSize()) {
44 Redraw();
45 }
46
47 wxPaintDC dc(this);
48 dc.DrawBitmap(rendered_, 0, 0);
49
50 event.Skip();
51}
52
53void TrackerPanel::OnMouseMove(wxMouseEvent &event) {
54 for (AreaIndicator &area : areas_) {
55 if (area.real_x1 <= event.GetX() && event.GetX() < area.real_x2 &&
56 area.real_y1 <= event.GetY() && event.GetY() < area.real_y2) {
57 area.popup->Show();
58 } else {
59 area.popup->Hide();
60 }
61 }
62
63 event.Skip();
64}
65
66void TrackerPanel::Redraw() {
67 wxSize panel_size = GetSize();
68 wxSize image_size = map_image_.GetSize();
69
70 int final_x = 0;
71 int final_y = 0;
72 int final_width = panel_size.GetWidth();
73 int final_height = panel_size.GetHeight();
74
75 if (image_size.GetWidth() * panel_size.GetHeight() >
76 panel_size.GetWidth() * image_size.GetHeight()) {
77 final_height = (panel_size.GetWidth() * image_size.GetHeight()) /
78 image_size.GetWidth();
79 final_y = (panel_size.GetHeight() - final_height) / 2;
80 } else {
81 final_width = (image_size.GetWidth() * panel_size.GetHeight()) /
82 image_size.GetHeight();
83 final_x = (panel_size.GetWidth() - final_width) / 2;
84 }
85
86 rendered_ = wxBitmap(
87 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL)
88 .Size(panel_size, {final_x, final_y}, 0, 0, 0));
89
90 wxMemoryDC dc;
91 dc.SelectObject(rendered_);
92
93 for (AreaIndicator &area : areas_) {
94 const wxBrush *brush_color = wxGREY_BRUSH;
95
96 const MapArea &map_area = GetGameData().GetMapArea(area.area_id);
97 bool has_reachable_unchecked = false;
98 bool has_unreachable_unchecked = false;
99 for (int section_id = 0; section_id < map_area.locations.size();
100 section_id++) {
101 if (!AP_HasCheckedGameLocation(area.area_id, section_id)) {
102 if (GetTrackerState().IsLocationReachable(area.area_id, section_id)) {
103 has_reachable_unchecked = true;
104 } else {
105 has_unreachable_unchecked = true;
106 }
107 }
108 }
109
110 if (has_reachable_unchecked && has_unreachable_unchecked) {
111 brush_color = wxYELLOW_BRUSH;
112 } else if (has_reachable_unchecked) {
113 brush_color = wxGREEN_BRUSH;
114 } else if (has_unreachable_unchecked) {
115 brush_color = wxRED_BRUSH;
116 }
117
118 int real_area_size =
119 final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
120 int actual_border_size =
121 real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE;
122 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) *
123 final_width / image_size.GetWidth();
124 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
125 final_width / image_size.GetWidth();
126
127 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
128 dc.SetBrush(*brush_color);
129 dc.DrawRectangle({real_area_x, real_area_y},
130 {real_area_size, real_area_size});
131
132 area.real_x1 = real_area_x;
133 area.real_x2 = real_area_x + real_area_size;
134 area.real_y1 = real_area_y;
135 area.real_y2 = real_area_y + real_area_size;
136
137 int popup_x =
138 final_x + map_area.map_x * final_width / image_size.GetWidth();
139 int popup_y =
140 final_y + map_area.map_y * final_width / image_size.GetWidth();
141 if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
142 popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
143 }
144 if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
145 popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
146 }
147 area.popup->SetPosition({popup_x, popup_y});
148 }
149}
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 @@
1#ifndef TRACKER_PANEL_H_D675A54D
2#define TRACKER_PANEL_H_D675A54D
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10class AreaPopup;
11
12class TrackerPanel : public wxPanel {
13 public:
14 TrackerPanel(wxWindow *parent);
15
16 void UpdateIndicators();
17
18 private:
19 struct AreaIndicator {
20 int area_id = -1;
21 AreaPopup *popup = nullptr;
22 int real_x1 = 0;
23 int real_y1 = 0;
24 int real_x2 = 0;
25 int real_y2 = 0;
26 };
27
28 void OnPaint(wxPaintEvent &event);
29 void OnMouseMove(wxMouseEvent &event);
30
31 void Redraw();
32
33 wxImage map_image_;
34 wxBitmap rendered_;
35
36 std::vector<AreaIndicator> areas_;
37};
38
39#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 @@
1#include "tracker_state.h"
2
3#include <list>
4#include <set>
5
6#include "ap_state.h"
7#include "game_data.h"
8
9bool IsDoorReachable_Helper(int door_id, const std::set<int>& reachable_rooms);
10
11bool IsPanelReachable_Helper(int panel_id,
12 const std::set<int>& reachable_rooms) {
13 const Panel& panel_obj = GetGameData().GetPanel(panel_id);
14
15 if (!reachable_rooms.count(panel_obj.room)) {
16 return false;
17 }
18
19 if (panel_obj.name == "THE MASTER") {
20 int achievements_accessible = 0;
21
22 for (int achieve_id : GetGameData().GetAchievementPanels()) {
23 if (IsPanelReachable_Helper(achieve_id, reachable_rooms)) {
24 achievements_accessible++;
25
26 if (achievements_accessible >= AP_GetMasteryRequirement()) {
27 break;
28 }
29 }
30 }
31
32 return (achievements_accessible >= AP_GetMasteryRequirement());
33 }
34
35 for (int room_id : panel_obj.required_rooms) {
36 if (!reachable_rooms.count(room_id)) {
37 return false;
38 }
39 }
40
41 for (int door_id : panel_obj.required_doors) {
42 if (!IsDoorReachable_Helper(door_id, reachable_rooms)) {
43 return false;
44 }
45 }
46
47 if (AP_IsColorShuffle()) {
48 for (LingoColor color : panel_obj.colors) {
49 if (!AP_HasColorItem(color)) {
50 return false;
51 }
52 }
53 }
54
55 return true;
56}
57
58bool IsDoorReachable_Helper(int door_id, const std::set<int>& reachable_rooms) {
59 const Door& door_obj = GetGameData().GetDoor(door_id);
60
61 if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) {
62 if (!reachable_rooms.count(door_obj.room)) {
63 return false;
64 }
65
66 for (int panel_id : door_obj.panels) {
67 if (!IsPanelReachable_Helper(panel_id, reachable_rooms)) {
68 return false;
69 }
70 }
71
72 return true;
73 } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS &&
74 !door_obj.group_name.empty()) {
75 return AP_HasItem(door_obj.group_name);
76 } else {
77 bool has_item = AP_HasItem(door_obj.item_name);
78
79 if (!has_item) {
80 for (const ProgressiveRequirement& prog_req : door_obj.progressives) {
81 if (AP_HasItem(prog_req.item_name, prog_req.quantity)) {
82 has_item = true;
83 break;
84 }
85 }
86 }
87
88 return has_item;
89 }
90}
91
92void TrackerState::CalculateState() {
93 reachability_.clear();
94
95 std::set<int> reachable_rooms;
96
97 std::list<Exit> flood_boundary;
98 flood_boundary.push_back(
99 {.destination_room = GetGameData().GetRoomByName("Menu")});
100
101 bool reachable_changed = true;
102 while (reachable_changed) {
103 reachable_changed = false;
104
105 std::list<Exit> new_boundary;
106 for (const Exit& room_exit : flood_boundary) {
107 if (reachable_rooms.count(room_exit.destination_room)) {
108 continue;
109 }
110
111 bool valid_transition = false;
112 if (room_exit.door.has_value()) {
113 if (IsDoorReachable_Helper(*room_exit.door, reachable_rooms)) {
114 valid_transition = true;
115 } else if (AP_GetDoorShuffleMode() == kNO_DOORS) {
116 new_boundary.push_back(room_exit);
117 }
118 } else {
119 valid_transition = true;
120 }
121
122 if (valid_transition) {
123 reachable_rooms.insert(room_exit.destination_room);
124 reachable_changed = true;
125
126 const Room& room_obj =
127 GetGameData().GetRoom(room_exit.destination_room);
128 for (const Exit& out_edge : room_obj.exits) {
129 if (!out_edge.painting || !AP_IsPaintingShuffle()) {
130 new_boundary.push_back(out_edge);
131 }
132 }
133
134 if (AP_IsPaintingShuffle()) {
135 for (const PaintingExit& out_edge : room_obj.paintings) {
136 if (AP_GetPaintingMapping().count(out_edge.id)) {
137 Exit painting_exit;
138 painting_exit.destination_room = GetGameData().GetRoomForPainting(
139 AP_GetPaintingMapping().at(out_edge.id));
140 painting_exit.door = out_edge.door;
141
142 new_boundary.push_back(painting_exit);
143 }
144 }
145 }
146 }
147 }
148
149 flood_boundary = new_boundary;
150 }
151
152 for (const MapArea& map_area : GetGameData().GetMapAreas()) {
153 for (int section_id = 0; section_id < map_area.locations.size();
154 section_id++) {
155 const Location& location_section = map_area.locations.at(section_id);
156 bool reachable = reachable_rooms.count(location_section.room);
157 if (reachable) {
158 for (int panel_id : location_section.panels) {
159 reachable &= IsPanelReachable_Helper(panel_id, reachable_rooms);
160 }
161 }
162
163 reachability_[{map_area.id, section_id}] = reachable;
164 }
165 }
166}
167
168bool TrackerState::IsLocationReachable(int area_id, int section_id) {
169 std::tuple<int, int> key = {area_id, section_id};
170
171 if (reachability_.count(key)) {
172 return reachability_.at(key);
173 } else {
174 return false;
175 }
176}
177
178TrackerState& GetTrackerState() {
179 static TrackerState* instance = new TrackerState();
180 return *instance;
181}
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 @@
1#ifndef TRACKER_STATE_H_8639BC90
2#define TRACKER_STATE_H_8639BC90
3
4#include <map>
5#include <tuple>
6
7class TrackerState {
8 public:
9 void CalculateState();
10
11 bool IsLocationReachable(int area_id, int section_id);
12
13 private:
14 std::map<std::tuple<int, int>, bool> reachability_;
15};
16
17TrackerState& GetTrackerState();
18
19#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */