From 149e7c0836927e14a926a952bd1a7f0d1b49e779 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 5 May 2023 15:46:58 -0400 Subject: Organised repo --- 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 +++ 19 files changed, 1712 insertions(+) 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 (limited to 'src') 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 inventory; +std::set checked_locations; + +std::map, int64_t> ap_id_by_location_id; +std::map ap_id_by_item_name; +std::map ap_id_by_color; +std::map 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 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& 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& 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(); + color_shuffle = slot_data["shuffle_colors"].get(); + painting_shuffle = slot_data["shuffle_paintings"].get(); + mastery_requirement = slot_data["mastery_achievements"].get(); + + 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& errors) { + connected = false; + has_connection_result = true; + + tracker_frame->SetStatusMessage("Disconnected from Archipelago."); + + std::vector 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 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 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 +#include + +#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 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 + +#ifndef WX_PRECOMP +#include +#endif + +#include "eye_indicator.h" + +class AreaPopup : public wxPanel { + public: + AreaPopup(wxWindow* parent, int area_id); + + void UpdateIndicators(); + + private: + int area_id_; + + std::vector section_labels_; + std::vector 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 + +#ifndef WX_PRECOMP +#include +#endif + +#include + +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 + +#ifndef WX_PRECOMP +#include +#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 +#include + +#include + +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()); + Room &room_obj = rooms_[room_id]; + + for (const auto &entrance_it : room_it.second["entrances"]) { + int from_room_id = AddOrGetRoom(entrance_it.first.as()); + 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(); + } + exit_obj.door = AddOrGetDoor( + door_room, entrance_it.second["door"].as()); + } + + if (entrance_it.second["painting"]) { + exit_obj.painting = entrance_it.second["painting"].as(); + } + + 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(); + } + exit_obj.door = + AddOrGetDoor(door_room, option["door"].as()); + + if (option["painting"]) { + exit_obj.painting = option["painting"].as(); + } + + 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()); + 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())); + } else { + for (const auto &color_node : panel_it.second["colors"]) { + panel_obj.colors.push_back( + GetColorForString(color_node.as())); + } + } + } + + 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())); + } else { + for (const auto &rr_node : panel_it.second["required_room"]) { + panel_obj.required_rooms.push_back( + AddOrGetRoom(rr_node.as())); + } + } + } + + 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(); + } + + panel_obj.required_doors.push_back(AddOrGetDoor( + rd_room, + panel_it.second["required_door"]["door"].as())); + } 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(); + } + + panel_obj.required_doors.push_back( + AddOrGetDoor(rd_room, rr_node["door"].as())); + } + } + } + + if (panel_it.second["check"]) { + panel_obj.check = panel_it.second["check"].as(); + } + + if (panel_it.second["achievement"]) { + panel_obj.achievement = panel_it.second["achievement"].as(); + + 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(); + } + } + } + + 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()); + Door &door_obj = doors_[door_id]; + + bool has_external_panels = false; + std::vector panel_names; + + for (const auto &panel_node : door_it.second["panels"]) { + if (panel_node.IsScalar()) { + panel_names.push_back(panel_node.as()); + door_obj.panels.push_back( + AddOrGetPanel(room_obj.name, panel_node.as())); + } else { + has_external_panels = true; + panel_names.push_back(panel_node["panel"].as()); + door_obj.panels.push_back( + AddOrGetPanel(panel_node["room"].as(), + panel_node["panel"].as())); + } + } + + if (door_it.second["skip_location"]) { + door_obj.skip_location = door_it.second["skip_location"].as(); + } + + if (door_it.second["skip_item"]) { + door_obj.skip_item = door_it.second["skip_item"].as(); + } + + if (door_it.second["event"]) { + door_obj.skip_location = door_it.second["event"].as(); + door_obj.skip_item = door_it.second["event"].as(); + } + + if (door_it.second["item_name"]) { + door_obj.item_name = door_it.second["item_name"].as(); + } 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(); + } + + if (door_it.second["location_name"]) { + door_obj.location_name = + door_it.second["location_name"].as(); + } 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(); + } + } + } + + if (room_it.second["paintings"]) { + for (const auto &painting : room_it.second["paintings"]) { + std::string painting_id = painting["id"].as(); + room_by_painting_[painting_id] = room_id; + + if (!painting["exit_only"] || !painting["exit_only"].as()) { + 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(); + } + + painting_exit.door = AddOrGetDoor( + rd_room, painting["required_door"]["door"].as()); + } + + 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(); + + 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()); + } else { + door_id = AddOrGetDoor(stage["room"].as(), + stage["door"].as()); + } + + doors_[door_id].progressives.push_back( + {.item_name = progressive_item_name, .quantity = index}); + index++; + } + } + } + } + + map_areas_.reserve(areas_config.size()); + + std::map fold_areas; + for (const auto &area_it : areas_config) { + if (area_it.second["map"]) { + int area_id = AddOrGetArea(area_it.first.as()); + MapArea &area_obj = map_areas_[area_id]; + area_obj.map_x = area_it.second["map"][0].as(); + area_obj.map_y = area_it.second["map"][1].as(); + } else if (area_it.second["fold_into"]) { + fold_areas[area_it.first.as()] = + AddOrGetArea(area_it.second["fold_into"].as()); + } + } + + 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 +#include +#include +#include + +enum class LingoColor { + kNone, + kBlack, + kRed, + kBlue, + kYellow, + kGreen, + kOrange, + kPurple, + kBrown, + kGray +}; + +struct Panel { + int id; + int room; + std::string name; + std::vector colors; + std::vector required_rooms; + std::vector 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 panels; + bool exclude_reduce = true; + std::vector progressives; +}; + +struct Exit { + int destination_room; + std::optional door; + bool painting = false; +}; + +struct PaintingExit { + std::string id; + std::optional door; +}; + +struct Room { + std::string name; + std::vector exits; + std::vector paintings; +}; + +struct Location { + std::string name; + std::string ap_location_name; + int room; + std::vector panels; +}; + +struct MapArea { + int id; + std::string name; + std::vector locations; + int map_x; + int map_y; +}; + +class GameData { + public: + GameData(); + + const std::vector& 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& 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& 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 rooms_; + std::vector doors_; + std::vector panels_; + std::vector map_areas_; + + std::map room_by_id_; + std::map door_by_id_; + std::map panel_by_id_; + std::map area_by_id_; + + std::map room_by_painting_; + + std::vector 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 + +#ifndef WX_PRECOMP +#include +#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 +#include + +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(); + ap_player = file["ap_player"].as(); + ap_password = file["ap_password"].as(); + } 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 + +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 + +#ifndef WX_PRECOMP +#include +#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 + +#ifndef WX_PRECOMP +#include +#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 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 +#include + +#include "ap_state.h" +#include "game_data.h" + +bool IsDoorReachable_Helper(int door_id, const std::set& reachable_rooms); + +bool IsPanelReachable_Helper(int panel_id, + const std::set& 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& 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 reachable_rooms; + + std::list flood_boundary; + flood_boundary.push_back( + {.destination_room = GetGameData().GetRoomByName("Menu")}); + + bool reachable_changed = true; + while (reachable_changed) { + reachable_changed = false; + + std::list 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 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 +#include + +class TrackerState { + public: + void CalculateState(); + + bool IsLocationReachable(int area_id, int section_id); + + private: + std::map, bool> reachability_; +}; + +TrackerState& GetTrackerState(); + +#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */ -- cgit 1.4.1