/ source / blood / src / chatpipe.cpp
chatpipe.cpp
  1  //-------------------------------------------------------------------------
  2  /*
  3  Copyright (C) 2010-2019 EDuke32 developers and contributors
  4  Copyright (C) 2023, 2023 - Jordon Moss (Aka. StrikerTheHedgefox)
  5  
  6  This file is part of NBlood.
  7  
  8  NBlood is free software; you can redistribute it and/or
  9  modify it under the terms of the GNU General Public License version 2
 10  as published by the Free Software Foundation.
 11  
 12  This program is distributed in the hope that it will be useful,
 13  but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 15  
 16  See the GNU General Public License for more details.
 17  
 18  You should have received a copy of the GNU General Public License
 19  along with this program; if not, write to the Free Software
 20  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 21  */
 22  //-------------------------------------------------------------------------
 23  #include <stdlib.h>
 24  #include <string.h>
 25  #include "compat.h"
 26  #include "build.h"
 27  #include "common.h"
 28  #include "common_game.h"
 29  #include "sjson.h"
 30  
 31  #include "chatpipe.h"
 32  #include "view.h"
 33  
 34  #ifdef NETCODE_DISABLE // non-netcode build
 35  void ChatPipe_Create(void)
 36  {
 37      return;
 38  }
 39  
 40  void ChatPipe_SendMessage(const char* message)
 41  {
 42      UNREFERENCED_PARAMETER(message);
 43  }
 44  
 45  void ChatPipe_Poll(void)
 46  {
 47      return;
 48  }
 49  #else
 50  
 51  #define CHATPIPE_BUFSIZE 4096
 52  #define CHATPIPE_TIMEOUT 5000
 53  
 54  // Global vars
 55  char cpTempBuf[CHATPIPE_BUFSIZE] = "";
 56  char bPipeActive = 0;
 57  
 58  // Platform-agnostic functions.
 59  static void ChatPipe_WriteToPipe(const char* message);
 60  
 61  static bool ChatPipe_ParseJSON(const char* data)
 62  {
 63      sjson_context* jsonCtx = sjson_create_context(0, 0, NULL);
 64      sjson_node* jRoot = sjson_decode(jsonCtx, data);
 65      bool success = false;
 66  
 67      if (jRoot)
 68      {
 69          const char* strEvent = sjson_get_string(jRoot, "event", NULL);
 70          if (!Bstrcasecmp(strEvent, "message"))
 71          {
 72              const char* strUser = sjson_get_string(jRoot, "user", NULL);
 73              const char* strText = sjson_get_string(jRoot, "text", NULL);
 74  
 75              snprintf(cpTempBuf, kMaxMessageTextLength, "\r[LOBBY]\r \r%s\r: %s\n", strUser, strText);
 76              success = true;
 77          }
 78          else if (!Bstrcasecmp(strEvent, "join"))
 79          {
 80              const char* strUser = sjson_get_string(jRoot, "user", NULL);
 81  
 82              snprintf(cpTempBuf, kMaxMessageTextLength, "\r[LOBBY]\r \r%s\r has joined the lobby.\n", strUser);
 83              success = true;
 84          }
 85          else if (!Bstrcasecmp(strEvent, "leave"))
 86          {
 87              const char* strUser = sjson_get_string(jRoot, "user", NULL);
 88  
 89              snprintf(cpTempBuf, kMaxMessageTextLength, "\r[LOBBY]\r \r%s\r has left the lobby.\n", strUser);
 90              success = true;
 91          }
 92  
 93          if (success)
 94              viewSetMessageColor(cpTempBuf, 0, MESSAGE_PRIORITY_NORMAL, gColorMsg ? 9 : 0, gColorMsg ? kFlagBluePal : 0);
 95          sjson_delete_node(jsonCtx, jRoot);
 96      }
 97  
 98  #ifdef DEBUGGINGAIDS
 99      if (success)
100          LOG_F(INFO, "%s: JSON parse succeded!", __func__);
101      else
102          LOG_F(INFO, "%s: JSON parse failed, or unknown message!", __func__);
103  #endif
104  
105      sjson_destroy_context(jsonCtx);
106      return success;
107  }
108  
109  void ChatPipe_SendMessage(const char* message)
110  {
111      if (!bPipeActive)
112          return;
113  
114      sjson_context* jsonCtx = sjson_create_context(0, 0, NULL);
115      sjson_node* jRoot = sjson_mkobject(jsonCtx);
116  
117      sjson_put_string(jsonCtx, jRoot, "event", "message");
118      //sjson_put_string(jsonCtx, jRoot, "user", g_player[myconnectindex].user_name);
119      OSD_StripColors(cpTempBuf, message);
120      sjson_put_string(jsonCtx, jRoot, "text", cpTempBuf);
121  
122      char* jEncoded = sjson_stringify(jsonCtx, jRoot, NULL);
123      ChatPipe_WriteToPipe(jEncoded);
124  
125      sjson_free_string(jsonCtx, jEncoded);
126      sjson_delete_node(jsonCtx, jRoot);
127      sjson_destroy_context(jsonCtx);
128  }
129  
130  #if defined(_WIN32) // Windows implementation
131  struct {
132      HANDLE handle;
133      OVERLAPPED overlapRead, overlapWrite;
134      DWORD state;
135      DWORD bytesRead;
136      DWORD bytesToWrite;
137      char readBuffer[CHATPIPE_BUFSIZE];
138      char writeBuffer[CHATPIPE_BUFSIZE];
139      int readFlag;
140      int writeFlag;
141  } Pipe;
142  
143  HANDLE waitEvent;
144  
145  enum chatPipeState_t
146  {
147      PIPESTATE_CONNECTING,
148      PIPESTATE_READY,
149  };
150  
151  static bool ChatPipe_Connect(void)
152  {
153      bool pipeConnected = ConnectNamedPipe(Pipe.handle, &Pipe.overlapRead);
154  
155      DWORD error = GetLastError();
156      if (pipeConnected) // Overlapped ConnectNamedPipe should return zero
157      {
158          LOG_F(ERROR, "%s: Chat pipe connection failed with %ld", __func__, error);
159          return false;
160      }
161  
162      if (error == ERROR_IO_PENDING)
163      {
164          LOG_F(INFO, "%s: Chat pipe connection pending...", __func__);
165          return true;
166      }
167      else if (error == ERROR_PIPE_CONNECTED)
168      {
169          //a client has already connected between creating the pipe and
170          //waiting for a client to connect. This is rare but can happen.
171          //When it does, the overlapped wait does not begin, so the overlapped
172          //event would not be signalled. We trigger it manually so that the normal
173          //workflow continues
174          SetEvent(Pipe.overlapRead.hEvent);
175          return false;
176      }
177      else
178      {
179          LOG_F(ERROR, "%s: Chat pipe connection failed with %ld", __func__, error);
180          return false;
181      }
182  
183      return false;
184  }
185  
186  static void ChatPipe_Reconnect()
187  {
188      // Disconnect the pipe instance. 
189  
190      if (!DisconnectNamedPipe(Pipe.handle))
191      {
192          LOG_F(ERROR, "%s: DisconnectNamedPipe failed with %ld.", __func__, GetLastError());
193      }
194  
195      // Call a subroutine to connect to the new client. 
196      Pipe.state = ChatPipe_Connect() ? PIPESTATE_CONNECTING : PIPESTATE_READY;
197      Pipe.readFlag = 0;
198      Pipe.writeFlag = 0;
199  }
200  
201  void ChatPipe_Create(void)
202  {
203      bPipeActive = 0;
204  
205      if (Pipe.handle != NULL)
206      {
207          LOG_F(ERROR, "%s: Already have a chat pipe!", __func__);
208          return;
209      }
210  
211      Pipe.handle = CreateNamedPipe("\\\\.\\pipe\\NotBlood", 
212          PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, 
213          PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 
214          1, 
215          CHATPIPE_BUFSIZE,
216          CHATPIPE_BUFSIZE,
217          CHATPIPE_TIMEOUT,
218          NULL);
219  
220      if ((Pipe.handle == NULL) || (Pipe.handle == INVALID_HANDLE_VALUE))
221      {
222          LOG_F(ERROR, "%s: Chat pipe creation failed", __func__);
223          return;
224      }
225  
226      waitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
227      Pipe.overlapRead.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
228      Pipe.overlapWrite.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
229  
230      Pipe.state = ChatPipe_Connect() ? PIPESTATE_CONNECTING : PIPESTATE_READY;
231      Pipe.readFlag = 0;
232      Pipe.writeFlag = 0;
233  
234      bPipeActive = 1;
235  }
236  
237  static void ChatPipe_WriteToPipe(const char* message)
238  {
239      DWORD byteCnt;
240  
241      snprintf(Pipe.writeBuffer, sizeof(Pipe.writeBuffer), "%s", message);
242      Pipe.bytesToWrite = (strlen(Pipe.writeBuffer)) * sizeof(char);
243  
244      bool success = WriteFile(
245          Pipe.handle,
246          Pipe.writeBuffer,
247          Pipe.bytesToWrite,
248          &byteCnt,
249          &Pipe.overlapWrite);
250  
251      DWORD error_code = GetLastError();
252  
253      if (!success && error_code != ERROR_IO_PENDING)
254      {
255          LOG_F(ERROR, "%s: WriteFile failed, error code: %ld", __func__, error_code);
256          ChatPipe_Reconnect(); // An error occurred; disconnect from the client. 
257          return;
258      }
259  
260      Pipe.writeFlag++;
261  }
262  
263  static void ChatPipe_ReadFromPipe()
264  {
265      bool success = ReadFile(
266          Pipe.handle,
267          Pipe.readBuffer,
268          CHATPIPE_BUFSIZE,
269          &Pipe.bytesRead,
270          &Pipe.overlapRead);
271  
272      DWORD error_code = ::GetLastError();
273      if (!success && error_code != ERROR_IO_PENDING)
274      {
275          LOG_F(ERROR, "%s: ReadFile failed, error code: %ld", __func__, error_code);
276          ChatPipe_Reconnect(); // An error occurred; disconnect from the client. 
277          return;
278      }
279  
280      Pipe.readFlag++;
281  
282      return;
283  }
284  
285  void ChatPipe_OnWriteComplete()
286  {
287      Pipe.writeFlag--;
288  
289  #ifdef DEBUGGINGAIDS
290      LOG_F(INFO, "%s: Write completed. (%ld bytes)", __func__, Pipe.overlapWrite.InternalHigh);
291  #endif
292  }
293  
294  void ChatPipe_OnReadComplete()
295  {
296      Pipe.readFlag--;
297      DWORD byteCnt;
298      BOOL ol_result = ::GetOverlappedResult(Pipe.handle, &Pipe.overlapRead, &byteCnt, FALSE);
299      if (!ol_result) {
300          DWORD code = ::GetLastError();
301          if (code != ERROR_MORE_DATA) {
302              LOG_F(ERROR, "GetOverlappedResult failed, last error: %ld", code);
303              ChatPipe_Reconnect();
304              return;
305          }
306  
307          LOG_F(INFO, "%s: Recieved messaged too large! (%ld > %d bytes)", __func__, byteCnt, CHATPIPE_BUFSIZE);
308          ChatPipe_ReadFromPipe();
309          return;
310      }
311  
312      Pipe.readBuffer[byteCnt] = '\0';
313  
314      if (byteCnt > 0)
315      {
316          char* p = Pipe.readBuffer;
317  
318  #ifdef DEBUGGINGAIDS
319          LOG_F(INFO, "%s: Got messages! (%ld bytes)", __func__, byteCnt);
320  #endif
321  
322          for (DWORD i = 0; i < byteCnt;) // Bullshit hack because for some reason simultaneous messages get concatenated...
323          {
324              strcpy(cpTempBuf, p);
325  
326  #ifdef DEBUGGINGAIDS
327              LOG_F(INFO, "DATA: %s", cpTempBuf);
328  #endif
329  
330              if (!ChatPipe_ParseJSON(cpTempBuf))
331                  break;
332  
333              i += strlen(p) + 1;
334              p += strlen(p) + 1;
335          }
336      }
337  
338      if(Pipe.state == PIPESTATE_CONNECTING)
339      {
340          LOG_F(INFO, "%s: Connection established.", __func__);
341          Pipe.state = PIPESTATE_READY;
342      }
343  
344      ChatPipe_ReadFromPipe();
345  }
346  
347  void ChatPipe_Poll(void) // Called once per game loop
348  {
349      if (!bPipeActive || (Pipe.handle == NULL) || (Pipe.handle == INVALID_HANDLE_VALUE))
350          return;
351  
352      HANDLE handles[3] = { waitEvent, Pipe.overlapRead.hEvent, Pipe.overlapWrite.hEvent };
353  
354      DWORD waitResult;
355  
356      //if (!(waitResult > 0 && waitResult != WAIT_TIMEOUT))
357          //return;
358  
359      do
360      {
361          waitResult = WaitForMultipleObjects(3, handles, FALSE, 0);
362          //LOG_F(INFO, "%s: waitResult: %ld", __func__, waitResult);
363          switch (waitResult)
364          {
365              case WAIT_OBJECT_0:
366                  break;
367              case WAIT_OBJECT_0 + 1:
368                  ChatPipe_OnReadComplete();
369                  break;
370              case WAIT_OBJECT_0 + 2:
371                  ChatPipe_OnWriteComplete();
372                  break;
373              case WAIT_TIMEOUT:
374                  break;
375              default:
376                  LOG_F(ERROR, "WaitForMultipleObjects failed, error code: %ld", GetLastError());
377                  break;
378          }
379      } while ((waitResult > 0) && (waitResult != WAIT_TIMEOUT));
380  
381      return;
382  }
383  #else // Unix/Linux stuff
384  
385  static void ChatPipe_WriteToPipe(const char* message)
386  {
387      UNREFERENCED_PARAMETER(message);
388  }
389  
390  void ChatPipe_Create(void)
391  {
392      return;
393  }
394  
395  void ChatPipe_Poll(void) // Called once per game loop
396  {
397      return;
398  }
399  
400  #endif
401  #endif