about summary refs log blame commit diff stats
path: root/src/ipc_state.cpp
blob: a99fa895740a786b3c596399410a466ab651890b (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11









                                  
              



                     
                     



                          



                                        


                                 







                                                      
                                      
 


                                 








                                                                                
 




                               







                                                             


                                                                                 

                                                                               





                                             


                               












                                                           
                                           



                                             

                 








                                                                                 
                                                 
 
                                                    










                                       
         
                              
                                                  


                             
                                                                                












                                                                                
             



                                                                        
           

                                                   
 

                                     
 



                                                        




                                                                               
                                
                                                   
 





                                       
                                                          
                                


                                                                       
           








                                                                             
       









                                                                              


                                                                    
 






                                                   

     
                                            
                                        
                                                                          


                                                         
                                                                                



                   









                                               
















                                               

                              



                                             



                                                                




                                                                                
                                                                            





                                               
                                                                            



                                       
                             



                                                          
                                             
             
                           
                                                            
     
   
                             
                                                         
                         

















                                             



                                                                      












                                                               
 
                                             
                                      
#include "ipc_state.h"

#define _WEBSOCKETPP_CPP11_STRICT_

#include <fmt/core.h>

#include <chrono>
#include <memory>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <set>
#include <string>
#include <thread>
#include <tuple>
#include <wswrap.hpp>

#include "ap_state.h"
#include "logger.h"
#include "tracker_frame.h"

namespace {

struct IPCState {
  std::mutex state_mutex;
  TrackerFrame* tracker_frame = nullptr;

  // Protected state
  bool initialized = false;
  std::string address;
  bool should_disconnect = false;

  std::optional<std::string> status_message;

  bool slot_matches = false;
  std::string tracker_ap_server;
  std::string tracker_ap_user;
  std::string game_ap_server;
  std::string game_ap_user;

  std::optional<std::tuple<int, int>> player_position;
  std::set<std::string> solved_panels;

  // Thread state
  std::unique_ptr<wswrap::WS> ws;
  bool connected = false;

  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();

      initialized = true;
    } else if (address != a) {
      should_disconnect = true;
    }

    address = a;
  }

  std::optional<std::string> GetStatusMessage() {
    std::lock_guard state_guard(state_mutex);

    return status_message;
  }

  void SetTrackerSlot(std::string server, std::string user) {
    // This 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);
    tracker_ap_user = std::move(user);

    CheckIfSlotMatches();

    if (!slot_matches) {
      should_disconnect = true;
      address.clear();
    }
  }

  bool IsConnected() {
    std::lock_guard state_guard(state_mutex);

    return slot_matches;
  }

  std::optional<std::tuple<int, int>> GetPlayerPosition() {
    std::lock_guard state_guard(state_mutex);

    return player_position;
  }

  std::set<std::string> GetSolvedPanels() {
    std::lock_guard state_guard(state_mutex);

    return solved_panels;
  }

 private:
  void Thread() {
    for (;;) {
      // 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);

        SetStatusMessage("Disconnected from game.");

        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;

        SetStatusMessage("Connecting to game...");
      }

      int backoff_amount = 0;

      TrackerLog(fmt::format("Looking for game over IPC ({})...", ipc_address));

      while (!connected) {
        if (TryConnect(ipc_address)) {
          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++;
        } else {
          std::lock_guard state_guard(state_mutex);

          if (!should_disconnect) {
            should_disconnect = true;
            address.clear();

            SetStatusMessage("Disconnected from game.");
          }

          break;
        }

        // 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.
        bool show_error = false;
        {
          std::lock_guard state_guard(state_mutex);

          if (should_disconnect) {
            break;
          } else if (!connected) {
            if (backoff_amount >= 10) {
              should_disconnect = true;
              address.clear();

              SetStatusMessage("Disconnected from game.");

              show_error = true;
            } else {
              TrackerLog(fmt::format("Retrying IPC in {} second(s)...",
                                     backoff_amount + 1));
            }
          }
        }

        // We do this after giving up the mutex because otherwise we could
        // deadlock with the main thread.
        if (show_error) {
          TrackerLog("Giving up on IPC.");

          wxMessageBox("Connection to Lingo timed out.", "Connection failed",
                       wxOK | wxICON_ERROR);
          break;
        }
      }

      // 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));

        {
          std::lock_guard state_guard(state_mutex);
          if (should_disconnect) {
            ws.reset();
            break;
          }
        }
      }
    }
  }

  bool TryConnect(std::string ipc_address) {
    try {
      ws = std::make_unique<wswrap::WS>(
          ipc_address, [this]() { OnConnect(); }, [this]() { OnClose(); },
          [this](const std::string& s) { OnMessage(s); },
          [this](const std::string& s) { OnError(s); });
      return true;
    } catch (const std::exception& ex) {
      TrackerLog(fmt::format("Error connecting to Lingo: {}", ex.what()));
      wxMessageBox(ex.what(), "Error connecting to Lingo", wxOK | wxICON_ERROR);
      ws.reset();
      return false;
    }
  }

  void OnConnect() {
    connected = true;

    {
      std::lock_guard state_guard(state_mutex);

      slot_matches = false;
      player_position = std::nullopt;
      solved_panels.clear();
    }
  }

  void OnClose() {
    connected = false;

    {
      std::lock_guard state_guard(state_mutex);

      slot_matches = false;
    }
  }

  void OnMessage(const std::string& s) {
    TrackerLog(s);

    auto msg = nlohmann::json::parse(s);

    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);

      player_position =
          std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]);

      tracker_frame->UpdateIndicators(StateUpdate{.player_position = true});
    } else if (msg["cmd"] == "SolvePanels") {
      std::lock_guard state_guard(state_mutex);

      for (std::string panel : msg["panels"]) {
        solved_panels.insert(std::move(panel));
      }

      tracker_frame->UpdateIndicators(StateUpdate{.open_panels_tab = true});
    }
  }

  void OnError(const std::string& s) {}

  // Assumes mutex is locked.
  void CheckIfSlotMatches() {
    slot_matches = (tracker_ap_server == game_ap_server &&
                    tracker_ap_user == game_ap_user);

    if (slot_matches) {
      SetStatusMessage("Connected to game.");

      Sync();
    } else if (connected) {
      SetStatusMessage("Local game doesn't match AP slot.");
    }
  }

  // Assumes mutex is locked.
  void SetStatusMessage(std::optional<std::string> msg) {
    status_message = msg;

    tracker_frame->UpdateStatusMessage();
  }

  void Sync() {
    nlohmann::json msg;
    msg["cmd"] = "Sync";

    ws->send_text(msg.dump());
  }
};

IPCState& GetState() {
  static IPCState* instance = new IPCState();
  return *instance;
}

}  // namespace

void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) {
  GetState().SetTrackerFrame(tracker_frame);
}

void IPC_Connect(std::string address) { GetState().Connect(address); }

std::optional<std::string> IPC_GetStatusMessage() {
  return GetState().GetStatusMessage();
}

void IPC_SetTrackerSlot(std::string server, std::string user) {
  GetState().SetTrackerSlot(server, user);
}

bool IPC_IsConnected() { return GetState().IsConnected(); }

std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() {
  return GetState().GetPlayerPosition();
}

std::set<std::string> IPC_GetSolvedPanels() {
  return GetState().GetSolvedPanels();
}