about summary refs log tree commit diff stats
path: root/src/ipc_state.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ipc_state.cpp')
-rw-r--r--src/ipc_state.cpp367
1 files changed, 367 insertions, 0 deletions
diff --git a/src/ipc_state.cpp b/src/ipc_state.cpp new file mode 100644 index 0000000..6e2a440 --- /dev/null +++ b/src/ipc_state.cpp
@@ -0,0 +1,367 @@
1#include "ipc_state.h"
2
3#define _WEBSOCKETPP_CPP11_STRICT_
4
5#include <fmt/core.h>
6
7#include <chrono>
8#include <memory>
9#include <mutex>
10#include <nlohmann/json.hpp>
11#include <optional>
12#include <set>
13#include <string>
14#include <thread>
15#include <tuple>
16#include <wswrap.hpp>
17
18#include "ap_state.h"
19#include "logger.h"
20#include "tracker_frame.h"
21
22namespace {
23
24struct IPCState {
25 std::mutex state_mutex;
26 TrackerFrame* tracker_frame = nullptr;
27
28 // Protected state
29 bool initialized = false;
30 std::string address;
31 bool should_disconnect = false;
32
33 std::optional<std::string> status_message;
34
35 bool slot_matches = false;
36 std::string tracker_ap_server;
37 std::string tracker_ap_user;
38 std::string game_ap_server;
39 std::string game_ap_user;
40
41 std::optional<std::tuple<int, int>> player_position;
42
43 // Thread state
44 std::unique_ptr<wswrap::WS> ws;
45 bool connected = false;
46
47 void SetTrackerFrame(TrackerFrame* frame) { tracker_frame = frame; }
48
49 void Connect(std::string a) {
50 // This is the main concurrency concern, as it mutates protected state in an
51 // important way. Thread() is documented with how it interacts with this
52 // function.
53 std::lock_guard state_guard(state_mutex);
54
55 if (!initialized) {
56 std::thread([this]() { Thread(); }).detach();
57
58 initialized = true;
59 } else if (address != a) {
60 should_disconnect = true;
61 }
62
63 address = a;
64 }
65
66 std::optional<std::string> GetStatusMessage() {
67 std::lock_guard state_guard(state_mutex);
68
69 return status_message;
70 }
71
72 void SetTrackerSlot(std::string server, std::string user) {
73 // This function is called from the APState thread, not the main thread, and
74 // it mutates protected state. It only really competes with OnMessage(), when
75 // a "Connect" message is received. If this is called right before, and the
76 // tracker slot does not match the old game slot, it will initiate a
77 // disconnect, and then the OnMessage() handler will see should_disconnect
78 // and stop processing the "Connect" message. If this is called right after
79 // and the slot does not match, IPC will disconnect, which is tolerable.
80 std::lock_guard state_guard(state_mutex);
81
82 tracker_ap_server = std::move(server);
83 tracker_ap_user = std::move(user);
84
85 CheckIfSlotMatches();
86
87 if (!slot_matches) {
88 should_disconnect = true;
89 address.clear();
90 }
91 }
92
93 bool IsConnected() {
94 std::lock_guard state_guard(state_mutex);
95
96 return slot_matches;
97 }
98
99 std::optional<std::tuple<int, int>> GetPlayerPosition() {
100 std::lock_guard state_guard(state_mutex);
101
102 return player_position;
103 }
104
105 private:
106 void Thread() {
107 for (;;) {
108 // initialized is definitely true because it is set to true when the thread
109 // is created and only set to false within this block, when the thread is
110 // killed. Thus, a call to Connect would always at most set
111 // should_disconnect and address. If this happens before this block, it is
112 // as if we are starting from a new thread anyway because should_disconnect
113 // is immediately reset. If a call to Connect happens after this block,
114 // then a connection attempt will be made to the wrong address, but the
115 // thread will grab the mutex right after this and back out the wrong
116 // connection.
117 std::string ipc_address;
118 {
119 std::lock_guard state_guard(state_mutex);
120
121 SetStatusMessage("Disconnected from game.");
122
123 should_disconnect = false;
124
125 slot_matches = false;
126 game_ap_server.clear();
127 game_ap_user.clear();
128
129 player_position = std::nullopt;
130
131 if (address.empty()) {
132 initialized = false;
133 return;
134 }
135
136 ipc_address = address;
137
138 SetStatusMessage("Connecting to game...");
139 }
140
141 int backoff_amount = 0;
142
143 TrackerLog(fmt::format("Looking for game over IPC ({})...", ipc_address));
144
145 while (!connected) {
146 if (TryConnect(ipc_address)) {
147 int backoff_limit = (backoff_amount + 1) * 10;
148
149 for (int i = 0; i < backoff_limit && !connected; i++) {
150 // If Connect is called right before this block, we will see and
151 // handle should_disconnect. If it is called right after, we will do
152 // one bad poll, one sleep, and then grab the mutex again right
153 // after.
154 {
155 std::lock_guard state_guard(state_mutex);
156 if (should_disconnect) {
157 break;
158 }
159 }
160
161 ws->poll();
162
163 // Back off
164 std::this_thread::sleep_for(std::chrono::milliseconds(100));
165 }
166
167 backoff_amount++;
168 } else {
169 std::lock_guard state_guard(state_mutex);
170
171 if (!should_disconnect) {
172 should_disconnect = true;
173 address.clear();
174
175 SetStatusMessage("Disconnected from game.");
176 }
177
178 break;
179 }
180
181 // If Connect is called right before this block, we will see and handle
182 // should_disconnect. If it is called right after, and the connection
183 // was unsuccessful, we will grab the mutex after one bad connection
184 // attempt. If the connection was successful, we grab the mutex right
185 // after exiting the loop.
186 bool show_error = false;
187 {
188 std::lock_guard state_guard(state_mutex);
189
190 if (should_disconnect) {
191 break;
192 } else if (!connected) {
193 if (backoff_amount >= 10) {
194 should_disconnect = true;
195 address.clear();
196
197 SetStatusMessage("Disconnected from game.");
198
199 show_error = true;
200 } else {
201 TrackerLog(fmt::format("Retrying IPC in {} second(s)...",
202 backoff_amount + 1));
203 }
204 }
205 }
206
207 // We do this after giving up the mutex because otherwise we could
208 // deadlock with the main thread.
209 if (show_error) {
210 TrackerLog("Giving up on IPC.");
211
212 wxMessageBox("Connection to Lingo timed out.", "Connection failed",
213 wxOK | wxICON_ERROR);
214 break;
215 }
216 }
217
218 // Pretty much every lock guard in the thread is the same. We check for
219 // should_disconnect, and if it gets set directly after the block, we do
220 // minimal bad work before checking for it again.
221 {
222 std::lock_guard state_guard(state_mutex);
223 if (should_disconnect) {
224 ws.reset();
225 continue;
226 }
227 }
228
229 while (connected) {
230 ws->poll();
231
232 std::this_thread::sleep_for(std::chrono::milliseconds(100));
233
234 {
235 std::lock_guard state_guard(state_mutex);
236 if (should_disconnect) {
237 ws.reset();
238 break;
239 }
240 }
241 }
242 }
243 }
244
245 bool TryConnect(std::string ipc_address) {
246 try {
247 ws = std::make_unique<wswrap::WS>(
248 ipc_address, [this]() { OnConnect(); }, [this]() { OnClose(); },
249 [this](const std::string& s) { OnMessage(s); },
250 [this](const std::string& s) { OnError(s); });
251 return true;
252 } catch (const std::exception& ex) {
253 TrackerLog(fmt::format("Error connecting to Lingo: {}", ex.what()));
254 wxMessageBox(ex.what(), "Error connecting to Lingo", wxOK | wxICON_ERROR);
255 ws.reset();
256 return false;
257 }
258 }
259
260 void OnConnect() {
261 connected = true;
262
263 {
264 std::lock_guard state_guard(state_mutex);
265
266 slot_matches = false;
267 player_position = std::nullopt;
268 }
269 }
270
271 void OnClose() {
272 connected = false;
273
274 {
275 std::lock_guard state_guard(state_mutex);
276
277 slot_matches = false;
278 }
279 }
280
281 void OnMessage(const std::string& s) {
282 TrackerLog(s);
283
284 auto msg = nlohmann::json::parse(s);
285
286 if (msg["cmd"] == "Connect") {
287 std::lock_guard state_guard(state_mutex);
288 if (should_disconnect) {
289 return;
290 }
291
292 game_ap_server = msg["slot"]["server"];
293 game_ap_user = msg["slot"]["player"];
294
295 CheckIfSlotMatches();
296
297 if (!slot_matches) {
298 tracker_frame->ConnectToAp(game_ap_server, game_ap_user,
299 msg["slot"]["password"]);
300 }
301 } else if (msg["cmd"] == "UpdatePosition") {
302 std::lock_guard state_guard(state_mutex);
303
304 player_position =
305 std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]);
306
307 tracker_frame->UpdateIndicators(StateUpdate{.player_position = true});
308 }
309 }
310
311 void OnError(const std::string& s) {}
312
313 // Assumes mutex is locked.
314 void CheckIfSlotMatches() {
315 slot_matches = (tracker_ap_server == game_ap_server &&
316 tracker_ap_user == game_ap_user);
317
318 if (slot_matches) {
319 SetStatusMessage("Connected to game.");
320
321 Sync();
322 } else if (connected) {
323 SetStatusMessage("Local game doesn't match AP slot.");
324 }
325 }
326
327 // Assumes mutex is locked.
328 void SetStatusMessage(std::optional<std::string> msg) {
329 status_message = msg;
330
331 tracker_frame->UpdateStatusMessage();
332 }
333
334 void Sync() {
335 nlohmann::json msg;
336 msg["cmd"] = "Sync";
337
338 ws->send_text(msg.dump());
339 }
340};
341
342IPCState& GetState() {
343 static IPCState* instance = new IPCState();
344 return *instance;
345}
346
347} // namespace
348
349void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) {
350 GetState().SetTrackerFrame(tracker_frame);
351}
352
353void IPC_Connect(std::string address) { GetState().Connect(address); }
354
355std::optional<std::string> IPC_GetStatusMessage() {
356 return GetState().GetStatusMessage();
357}
358
359void IPC_SetTrackerSlot(std::string server, std::string user) {
360 GetState().SetTrackerSlot(server, user);
361}
362
363bool IPC_IsConnected() { return GetState().IsConnected(); }
364
365std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() {
366 return GetState().GetPlayerPosition();
367}