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