diff options
Diffstat (limited to 'src/ipc_state.cpp')
-rw-r--r-- | src/ipc_state.cpp | 367 |
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 | |||
22 | namespace { | ||
23 | |||
24 | struct 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 | |||
342 | IPCState& GetState() { | ||
343 | static IPCState* instance = new IPCState(); | ||
344 | return *instance; | ||
345 | } | ||
346 | |||
347 | } // namespace | ||
348 | |||
349 | void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) { | ||
350 | GetState().SetTrackerFrame(tracker_frame); | ||
351 | } | ||
352 | |||
353 | void IPC_Connect(std::string address) { GetState().Connect(address); } | ||
354 | |||
355 | std::optional<std::string> IPC_GetStatusMessage() { | ||
356 | return GetState().GetStatusMessage(); | ||
357 | } | ||
358 | |||
359 | void IPC_SetTrackerSlot(std::string server, std::string user) { | ||
360 | GetState().SetTrackerSlot(server, user); | ||
361 | } | ||
362 | |||
363 | bool IPC_IsConnected() { return GetState().IsConnected(); } | ||
364 | |||
365 | std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() { | ||
366 | return GetState().GetPlayerPosition(); | ||
367 | } | ||