From 2844eecb65501f7dafa4de15d7377bfb810e1158 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 18 Dec 2024 13:36:52 -0500 Subject: Make IPC opt-in and configurable --- CMakeLists.txt | 1 + src/ipc_dialog.cpp | 53 ++++++++++++++++ src/ipc_dialog.h | 24 ++++++++ src/ipc_state.cpp | 164 ++++++++++++++++++++++++++++++++++++++++--------- src/ipc_state.h | 4 +- src/tracker_config.cpp | 4 ++ src/tracker_config.h | 1 + src/tracker_frame.cpp | 38 ++++++++++-- src/tracker_frame.h | 29 ++++++++- 9 files changed, 281 insertions(+), 37 deletions(-) create mode 100644 src/ipc_dialog.cpp create mode 100644 src/ipc_dialog.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fd943b3..f46c158 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ add_executable(lingo_ap_tracker "src/logger.cpp" "src/godot_variant.cpp" "src/ipc_state.cpp" + "src/ipc_dialog.cpp" "vendor/whereami/whereami.c" ) set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) diff --git a/src/ipc_dialog.cpp b/src/ipc_dialog.cpp new file mode 100644 index 0000000..f17c2d8 --- /dev/null +++ b/src/ipc_dialog.cpp @@ -0,0 +1,53 @@ +#include "ipc_dialog.h" + +#include "tracker_config.h" + +constexpr const char* kDefaultIpcAddress = "ws://127.0.0.1:41253"; + +IpcDialog::IpcDialog() : wxDialog(nullptr, wxID_ANY, "Connect to game") { + std::string address_value; + if (GetTrackerConfig().ipc_address.empty()) { + address_value = kDefaultIpcAddress; + } else { + address_value = GetTrackerConfig().ipc_address; + } + + address_box_ = + new wxTextCtrl(this, -1, address_value, wxDefaultPosition, {300, -1}); + + wxButton* reset_button = new wxButton(this, -1, "Use Default"); + reset_button->Bind(wxEVT_BUTTON, &IpcDialog::OnResetClicked, this); + + wxFlexGridSizer* form_sizer = new wxFlexGridSizer(3, 10, 10); + form_sizer->Add( + new wxStaticText(this, -1, "Address:"), + wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); + form_sizer->Add(address_box_, wxSizerFlags().Expand()); + form_sizer->Add(reset_button); + + wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL); + wxStaticText* top_text = new wxStaticText(this, -1, ""); + top_sizer->Add(top_text, wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder().Expand()); + top_sizer->Add(form_sizer, wxSizerFlags().DoubleBorder().Expand()); + top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), + wxSizerFlags().Border().Center()); + + SetSizer(top_sizer); + Layout(); + Fit(); + + int width = top_text->GetClientSize().GetWidth(); + top_text->SetLabel( + "This allows you to connect to a running Lingo game and track non-multiworld " + "state, such as the player's position and what panels are solved. Unless " + "you are doing something weird, the default value for the address is " + "probably correct."); + top_text->Wrap(width); + + Fit(); + Center(); +} + +void IpcDialog::OnResetClicked(wxCommandEvent& event) { + address_box_->SetValue(kDefaultIpcAddress); +} diff --git a/src/ipc_dialog.h b/src/ipc_dialog.h new file mode 100644 index 0000000..1caed01 --- /dev/null +++ b/src/ipc_dialog.h @@ -0,0 +1,24 @@ +#ifndef IPC_DIALOG_H_F4C5680C +#define IPC_DIALOG_H_F4C5680C + +#include + +#ifndef WX_PRECOMP +#include +#endif + +#include + +class IpcDialog : public wxDialog { + public: + IpcDialog(); + + std::string GetIpcAddress() { return address_box_->GetValue().ToStdString(); } + + private: + void OnResetClicked(wxCommandEvent& event); + + wxTextCtrl* address_box_; +}; + +#endif /* end of include guard: IPC_DIALOG_H_F4C5680C */ diff --git a/src/ipc_state.cpp b/src/ipc_state.cpp index c0bdc9b..24d0115 100644 --- a/src/ipc_state.cpp +++ b/src/ipc_state.cpp @@ -15,18 +15,21 @@ #include #include +#include "ap_state.h" #include "logger.h" #include "tracker_frame.h" namespace { -constexpr const char* kIpcAddress = "ws://127.0.0.1:41253"; - struct IPCState { std::mutex state_mutex; TrackerFrame* tracker_frame = nullptr; // Protected state + bool initialized = false; + std::string address; + bool should_disconnect = false; + std::optional status_message; bool slot_matches = false; @@ -38,16 +41,27 @@ struct IPCState { std::optional> player_position; std::set solved_panels; - int backoff_amount = 0; - // Thread state std::unique_ptr ws; bool connected = false; - void Start(TrackerFrame* frame) { - tracker_frame = frame; + void SetTrackerFrame(TrackerFrame* frame) { tracker_frame = frame; } + + void Connect(std::string a) { + // This is the main concurrency concern, as it mutates protected state in an + // important way. Thread() is documented with how it interacts with this + // function. + std::lock_guard state_guard(state_mutex); + + if (!initialized) { + std::thread([this]() { Thread(); }).detach(); - std::thread([this]() { Thread(); }).detach(); + initialized = true; + } else if (address != a) { + should_disconnect = true; + } + + address = a; } std::optional GetStatusMessage() { @@ -57,6 +71,13 @@ struct IPCState { } void SetTrackerSlot(std::string server, std::string user) { + // This is function is called from the APState thread, not the main thread, + // and it mutates protected state. It only really competes with OnMessage(), + // when a "Connect" message is received. If this is called right before, + // and the tracker slot does not match the old game slot, it will initiate a + // disconnect, and then the OnMessage() handler will see should_disconnect + // and stop processing the "Connect" message. If this is called right after + // and the slot does not match, IPC will disconnect, which is tolerable. std::lock_guard state_guard(state_mutex); tracker_ap_server = std::move(server); @@ -64,7 +85,10 @@ struct IPCState { CheckIfSlotMatches(); - backoff_amount = 0; + if (!slot_matches) { + should_disconnect = true; + address.clear(); + } } bool IsConnected() { @@ -88,54 +112,126 @@ struct IPCState { private: void Thread() { for (;;) { - player_position = std::nullopt; - - TrackerLog("Looking for game over IPC..."); + SetStatusMessage("Disconnected from game."); + // initialized is definitely true because it is set to true when the thread + // is created and only set to false within this block, when the thread is + // killed. Thus, a call to Connect would always at most set + // should_disconnect and address. If this happens before this block, it is + // as if we are starting from a new thread anyway because should_disconnect + // is immediately reset. If a call to Connect happens after this block, + // then a connection attempt will be made to the wrong address, but the + // thread will grab the mutex right after this and back out the wrong + // connection. + std::string ipc_address; { std::lock_guard state_guard(state_mutex); - backoff_amount = 0; - } - while (!TryConnect() || !connected) { - int backoff_limit; - { - std::lock_guard state_guard(state_mutex); - backoff_limit = (backoff_amount + 1) * 10; + should_disconnect = false; + + slot_matches = false; + game_ap_server.clear(); + game_ap_user.clear(); + + player_position = std::nullopt; + solved_panels.clear(); + + if (address.empty()) { + initialized = false; + return; } + ipc_address = address; + } + + int backoff_amount = 0; + + SetStatusMessage("Connecting to game..."); + TrackerLog(fmt::format("Looking for game over IPC ({})...", ipc_address)); + + while (!TryConnect(ipc_address) || !connected) { + int backoff_limit = (backoff_amount + 1) * 10; + for (int i = 0; i < backoff_limit && !connected; i++) { + // If Connect is called right before this block, we will see and handle + // should_disconnect. If it is called right after, we will do one bad + // poll, one sleep, and then grab the mutex again right after. + { + std::lock_guard state_guard(state_mutex); + if (should_disconnect) { + break; + } + } + ws->poll(); // Back off std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + backoff_amount++; + + // If Connect is called right before this block, we will see and handle + // should_disconnect. If it is called right after, and the connection + // was unsuccessful, we will grab the mutex after one bad connection + // attempt. If the connection was successful, we grab the mutex right + // after exiting the loop. { std::lock_guard state_guard(state_mutex); - backoff_amount = std::min(9, backoff_amount + 1); - if (!connected) { - TrackerLog(fmt::format("Retrying IPC in {} second(s)...", - backoff_amount + 1)); + if (should_disconnect) { + break; + } else if (!connected) { + if (backoff_amount >= 10) { + should_disconnect = true; + address.clear(); + + TrackerLog("Giving up on IPC."); + SetStatusMessage("Disconnected from game."); + + wxMessageBox("Connection to Lingo timed out.", + "Connection failed", wxOK | wxICON_ERROR); + + break; + } else { + TrackerLog(fmt::format("Retrying IPC in {} second(s)...", + backoff_amount + 1)); + } } } } + // Pretty much every lock guard in the thread is the same. We check for + // should_disconnect, and if it gets set directly after the block, we do + // minimal bad work before checking for it again. + { + std::lock_guard state_guard(state_mutex); + if (should_disconnect) { + ws.reset(); + continue; + } + } + while (connected) { ws->poll(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - SetStatusMessage("Disconnected from game."); + { + std::lock_guard state_guard(state_mutex); + if (should_disconnect) { + ws.reset(); + break; + } + } + } } } - bool TryConnect() { + bool TryConnect(std::string ipc_address) { try { ws = std::make_unique( - kIpcAddress, [this]() { OnConnect(); }, [this]() { OnClose(); }, + ipc_address, [this]() { OnConnect(); }, [this]() { OnClose(); }, [this](const std::string& s) { OnMessage(s); }, [this](const std::string& s) { OnError(s); }); return true; @@ -174,11 +270,19 @@ struct IPCState { if (msg["cmd"] == "Connect") { std::lock_guard state_guard(state_mutex); + if (should_disconnect) { + return; + } game_ap_server = msg["slot"]["server"]; game_ap_user = msg["slot"]["player"]; CheckIfSlotMatches(); + + if (!slot_matches) { + tracker_frame->ConnectToAp(game_ap_server, game_ap_user, + msg["slot"]["password"]); + } } else if (msg["cmd"] == "UpdatePosition") { std::lock_guard state_guard(state_mutex); @@ -207,8 +311,6 @@ struct IPCState { status_message = "Connected to game."; Sync(); - } else if (tracker_ap_server.empty()) { - status_message = std::nullopt; } else if (connected) { status_message = "Local game doesn't match AP slot."; } @@ -240,7 +342,11 @@ IPCState& GetState() { } // namespace -void IPC_Start(TrackerFrame* tracker_frame) { GetState().Start(tracker_frame); } +void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) { + GetState().SetTrackerFrame(tracker_frame); +} + +void IPC_Connect(std::string address) { GetState().Connect(address); } std::optional IPC_GetStatusMessage() { return GetState().GetStatusMessage(); diff --git a/src/ipc_state.h b/src/ipc_state.h index a6bdc36..84f3d29 100644 --- a/src/ipc_state.h +++ b/src/ipc_state.h @@ -8,7 +8,9 @@ class TrackerFrame; -void IPC_Start(TrackerFrame* tracker_frame); +void IPC_SetTrackerFrame(TrackerFrame* tracker_frame); + +void IPC_Connect(std::string address); std::optional IPC_GetStatusMessage(); diff --git a/src/tracker_config.cpp b/src/tracker_config.cpp index 85164d5..129dbbc 100644 --- a/src/tracker_config.cpp +++ b/src/tracker_config.cpp @@ -27,6 +27,8 @@ void TrackerConfig::Load() { }); } } + + ipc_address = file["ipc_address"].as(); } catch (const std::exception&) { // It's fine if the file can't be loaded. } @@ -52,6 +54,8 @@ void TrackerConfig::Save() { output["connection_history"].push_back(connection); } + output["ipc_address"] = ipc_address; + std::ofstream filewriter(filename_); filewriter << output; } diff --git a/src/tracker_config.h b/src/tracker_config.h index a1a6c1d..9244b74 100644 --- a/src/tracker_config.h +++ b/src/tracker_config.h @@ -29,6 +29,7 @@ class TrackerConfig { bool hybrid_areas = false; bool show_hunt_panels = false; std::deque connection_history; + std::string ipc_address; private: std::string filename_; diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index d0fd5a6..587d87b 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp @@ -14,6 +14,7 @@ #include "achievements_pane.h" #include "ap_state.h" #include "connection_dialog.h" +#include "ipc_dialog.h" #include "ipc_state.h" #include "settings_dialog.h" #include "subway_map.h" @@ -38,18 +39,20 @@ std::string GetStatusMessage() { } // namespace enum TrackerFrameIds { - ID_CONNECT = 1, + ID_AP_CONNECT = 1, ID_CHECK_FOR_UPDATES = 2, ID_SETTINGS = 3, ID_ZOOM_IN = 4, ID_ZOOM_OUT = 5, ID_OPEN_SAVE_FILE = 6, + ID_IPC_CONNECT = 7, }; wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); wxDEFINE_EVENT(REDRAW_POSITION, wxCommandEvent); +wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent); TrackerFrame::TrackerFrame() : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition, @@ -57,9 +60,11 @@ TrackerFrame::TrackerFrame() ::wxInitAllImageHandlers(); AP_SetTrackerFrame(this); + IPC_SetTrackerFrame(this); wxMenu *menuFile = new wxMenu(); - menuFile->Append(ID_CONNECT, "&Connect"); + menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago"); + menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo"); menuFile->Append(ID_OPEN_SAVE_FILE, "&Open Save Data\tCtrl-O"); menuFile->Append(ID_SETTINGS, "&Settings"); menuFile->Append(wxID_EXIT); @@ -86,7 +91,8 @@ TrackerFrame::TrackerFrame() 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(wxEVT_MENU, &TrackerFrame::OnApConnect, this, ID_AP_CONNECT); + Bind(wxEVT_MENU, &TrackerFrame::OnIpcConnect, this, ID_IPC_CONNECT); Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, ID_CHECK_FOR_UPDATES); @@ -98,6 +104,7 @@ TrackerFrame::TrackerFrame() Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); Bind(REDRAW_POSITION, &TrackerFrame::OnRedrawPosition, this); + Bind(CONNECT_TO_AP, &TrackerFrame::OnConnectToAp, this); wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY); achievements_pane_ = new AchievementsPane(choicebook); @@ -134,11 +141,15 @@ TrackerFrame::TrackerFrame() CheckForUpdates(/*manual=*/false); } - IPC_Start(this); - SetStatusText(GetStatusMessage()); } +void TrackerFrame::ConnectToAp(std::string server, std::string user, + std::string pass) { + QueueEvent(new ApConnectEvent(CONNECT_TO_AP, GetId(), std::move(server), + std::move(user), std::move(pass))); +} + void TrackerFrame::UpdateStatusMessage() { QueueEvent(new wxCommandEvent(STATUS_CHANGED)); } @@ -172,7 +183,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) { void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } -void TrackerFrame::OnConnect(wxCommandEvent &event) { +void TrackerFrame::OnApConnect(wxCommandEvent &event) { ConnectionDialog dlg; if (dlg.ShowModal() == wxID_OK) { @@ -202,6 +213,17 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) { } } +void TrackerFrame::OnIpcConnect(wxCommandEvent &event) { + IpcDialog dlg; + + if (dlg.ShowModal() == wxID_OK) { + GetTrackerConfig().ipc_address = dlg.GetIpcAddress(); + GetTrackerConfig().Save(); + + IPC_Connect(dlg.GetIpcAddress()); + } +} + void TrackerFrame::OnSettings(wxCommandEvent &event) { SettingsDialog dlg; @@ -306,6 +328,10 @@ void TrackerFrame::OnRedrawPosition(wxCommandEvent &event) { } } +void TrackerFrame::OnConnectToAp(ApConnectEvent &event) { + AP_Connect(event.GetServer(), event.GetUser(), event.GetPass()); +} + void TrackerFrame::CheckForUpdates(bool manual) { wxWebRequest request = wxWebSession::GetDefault().CreateRequest( this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION"); diff --git a/src/tracker_frame.h b/src/tracker_frame.h index 9d18dc7..e9fec17 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h @@ -13,10 +13,34 @@ class TrackerPanel; class wxBookCtrlEvent; class wxNotebook; +class ApConnectEvent : public wxEvent { + public: + ApConnectEvent(wxEventType eventType, int winid, std::string server, + std::string user, std::string pass) + : wxEvent(winid, eventType), + ap_server_(std::move(server)), + ap_user_(std::move(user)), + ap_pass_(std::move(pass)) {} + + const std::string &GetServer() const { return ap_server_; } + + const std::string &GetUser() const { return ap_user_; } + + const std::string &GetPass() const { return ap_pass_; } + + virtual wxEvent *Clone() const { return new ApConnectEvent(*this); } + + private: + std::string ap_server_; + std::string ap_user_; + std::string ap_pass_; +}; + wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); wxDECLARE_EVENT(REDRAW_POSITION, wxCommandEvent); +wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent); enum UpdateIndicatorsMode { kUPDATE_ALL_INDICATORS = 0, @@ -27,6 +51,7 @@ class TrackerFrame : public wxFrame { public: TrackerFrame(); + void ConnectToAp(std::string server, std::string user, std::string pass); void UpdateStatusMessage(); void ResetIndicators(); @@ -36,7 +61,8 @@ class TrackerFrame : public wxFrame { private: void OnExit(wxCommandEvent &event); void OnAbout(wxCommandEvent &event); - void OnConnect(wxCommandEvent &event); + void OnApConnect(wxCommandEvent &event); + void OnIpcConnect(wxCommandEvent &event); void OnSettings(wxCommandEvent &event); void OnCheckForUpdates(wxCommandEvent &event); void OnZoomIn(wxCommandEvent &event); @@ -48,6 +74,7 @@ class TrackerFrame : public wxFrame { void OnStateChanged(wxCommandEvent &event); void OnStatusChanged(wxCommandEvent &event); void OnRedrawPosition(wxCommandEvent &event); + void OnConnectToAp(ApConnectEvent &event); void CheckForUpdates(bool manual); -- cgit 1.4.1