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