1 /// Simplified D translation of example_chat.cpp from the original repo.
2 ///
3 /// Copyright: Valve Corporation, all rights reserved
4 module chat;
5 
6 import steam_gns;
7 
8 import std.uni;
9 import std.stdio;
10 import std.range;
11 import std.format;
12 import std..string;
13 import std.algorithm;
14 import std.exception;
15 import std.concurrency;
16 
17 import core.time;
18 import core.atomic;
19 import core.thread;
20 
21 enum ushort defaultServerPort = 27020;
22 
23 shared {
24 
25     bool quit = false;
26     auto outbox = new shared MessageQueue;
27 
28 }
29 
30 /// A simple input message queue to work across threads. Handles local user input.
31 synchronized class MessageQueue {
32 
33     private string[] messages;
34 
35     bool empty() const {
36 
37         return messages.length == 0;
38 
39     }
40 
41     string front() const {
42 
43         return messages[0];
44 
45     }
46 
47     void popFront() {
48 
49         messages = messages[1..$];
50 
51     }
52 
53     void put(string message) {
54 
55         messages ~= message;
56 
57     }
58 
59 }
60 
61 // Non-blocking console user input. Sort of.
62 // Why is this so hard?
63 //
64 // Done from a separate thread.
65 void userInputEntrypoint() {
66 
67     // Repeat
68     while (!quit.atomicLoad) {
69 
70         write("msg: ");
71 
72         // Read a line and send to the parent thread
73         auto line = readln().strip;
74 
75         // Quit
76         if (line is null) {
77 
78             quit.atomicStore = true;
79 
80         }
81 
82         // Or send the message to the owner thread
83         else outbox.put(line);
84 
85     }
86 }
87 
88 /// Our chat server
89 class ChatServer {
90 
91     struct Client {
92 
93         string nick;
94 
95     }
96 
97     private static ChatServer instance;
98 
99     ISteamNetworkingSockets inter;
100     HSteamListenSocket socket;
101     HSteamNetPollGroup pollGroup;
102 
103     Client[HSteamNetConnection] clients;
104 
105     static void SteamNetConnectionStatusChangedCallback(SteamNetConnectionStatusChangedCallback_t* info) {
106 
107         instance.OnSteamNetConnectionStatusChanged(info);
108 
109     }
110 
111     void Run(ushort port) {
112 
113         // Select instance to use. For now we'll always use the default.
114         inter = SteamNetworkingSockets();
115 
116         // Set IP for the connection
117         SteamNetworkingIPAddr serverLocalAddr;
118         serverLocalAddr.Clear();
119         serverLocalAddr.m_port = port;
120 
121         // Set callback for connection status change
122         SteamNetworkingConfigValue_t opt;
123         opt.SetPtr(
124             ESteamNetworkingConfigValue.Callback_ConnectionStatusChanged,
125             &SteamNetConnectionStatusChangedCallback
126         );
127 
128         // Start listening
129         socket = inter.CreateListenSocketIP(serverLocalAddr, 1, &opt);
130         enforce(socket != k_HSteamListenSocket_Invalid, port.format!"Failed to listen on port %s");
131 
132         // Clean the socket up
133         scope (exit) {
134             inter.CloseListenSocket(socket);
135             socket = k_HSteamListenSocket_Invalid;
136         }
137 
138         // Create a poll group
139         pollGroup = inter.CreatePollGroup();
140         enforce(pollGroup != k_HSteamNetPollGroup_Invalid, port.format!"Failed to listen on port %s");
141 
142         // Clean the poll group up
143         scope (exit) {
144             inter.DestroyPollGroup(pollGroup);
145             pollGroup = k_HSteamNetPollGroup_Invalid;
146         }
147 
148         // Output listening status
149         writefln!"Server listening on port %s"(port);
150 
151         // Close connections
152         scope (exit) {
153 
154             foreach (connection; clients.byKey) {
155 
156                 // Send them one more goodbye message.  Note that we also have the
157                 // connection close reason as a place to send final data.  However,
158                 // that's usually best left for more diagnostic/debug text not actual
159                 // protocol strings.
160                 SendStringToClient(connection, "Server is shutting down. Goodbye.");
161 
162                 // Close the connection.  We use "linger mode" to ask SteamNetworkingSockets
163                 // to flush this out and close gracefully.
164                 inter.CloseConnection(connection, 0, "Server shutdown", true);
165 
166             }
167 
168             clients.clear();
169 
170         }
171 
172         // Listen to connections
173         while (!quit.atomicLoad) {
174 
175             PollIncomingMessages();
176             PollConnectionStateChanges();
177             PollLocalUserInput();
178 
179             Thread.sleep(10.msecs);
180 
181         }
182 
183         // Close all the connections
184         writefln!"Closing connections... Press <Enter> to continue.";
185 
186     }
187 
188     void SendStringToClient(HSteamNetConnection conn, string str) {
189 
190         inter.SendMessageToConnection(conn, str.ptr, cast(uint) str.length, k_nSteamNetworkingSend_Reliable,
191             null);
192 
193     }
194 
195     void SendStringToAllClients(string str, HSteamNetConnection except = k_HSteamNetConnection_Invalid) {
196 
197         foreach (connection; clients.byKey) {
198 
199             if (connection == except) continue;
200             SendStringToClient(connection, str);
201 
202         }
203 
204     }
205 
206     void PollIncomingMessages() {
207 
208         while (!quit.atomicLoad) {
209 
210             SteamNetworkingMessage_t* incomingMsg;
211 
212             int numMsgs = inter.ReceiveMessagesOnPollGroup(pollGroup, &incomingMsg, 1);
213 
214             // Stop when out of messages
215             if (numMsgs == 0) break;
216 
217             enforce(numMsgs > 0, "Error checking for messages");
218             enforce(incomingMsg);
219 
220             // Clean the message up
221             scope (exit) incomingMsg.Release();
222 
223             auto connection = incomingMsg.m_conn;
224             auto client = clients[connection];
225 
226             // '\0'-terminate it to make it easier to parse
227             auto cmd = cast(string) incomingMsg.m_pData[0 .. incomingMsg.m_cbSize];
228 
229             // Check for known commands.  None of this example code is secure or robust.
230             // Don't write a real server like this, please.
231             if (cmd.startsWith("/nick")) {
232 
233                 string nick = cmd[5..$];
234 
235                 // Skip space
236                 while (nick.front.isWhite) nick.popFront;
237 
238                 // Let everybody else know they changed their name
239                 SendStringToAllClients(
240                     format!"%s shall henceforth be known as %s"(client.nick, nick),
241                     connection
242                 );
243 
244                 // Respond to client
245                 SendStringToClient(
246                     connection,
247                     format!"Ye shall henceforth be known as %s"(nick)
248                 );
249 
250                 // Actually change their name
251                 SetClientNick(connection, nick.dup);
252 
253                 continue;
254 
255             }
256 
257             // Assume it's just a ordinary chat message, dispatch to everybody else
258             SendStringToAllClients(format!"%s: %s"(client.nick, cmd), connection);
259 
260         }
261 
262     }
263 
264     void PollLocalUserInput() {
265 
266         while (!quit.atomicLoad && !outbox.empty) {
267 
268             auto cmd = outbox.front;
269             outbox.popFront;
270 
271             if (cmd.startsWith("/quit")) {
272 
273                 quit.atomicStore = true;
274                 writefln!"Shutting down the server";
275                 break;
276 
277             }
278 
279             // That's the only command we support
280             writefln!"The server only knows one command: '/quit'";
281 
282         }
283 
284     }
285 
286     void SetClientNick(HSteamNetConnection hConn, string nick) {
287 
288         // Remember their nick
289         clients[hConn].nick = nick;
290 
291         // Set the connection name, too, which is useful for debugging
292         inter.SetConnectionName(hConn, nick.toStringz);
293 
294     }
295 
296     void OnSteamNetConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* info) {
297 
298         // What's the state of the connection?
299         switch (info.m_info.m_eState) {
300 
301             case ESteamNetworkingConnectionState.None:
302 
303                 // NOTE: We will get callbacks here when we destroy connections. You can ignore these.
304                 break;
305 
306             case ESteamNetworkingConnectionState.ClosedByPeer:
307             case ESteamNetworkingConnectionState.ProblemDetectedLocally:
308 
309                 scope (exit) {
310 
311                     // Clean up the connection.  This is important!
312                     // The connection is "closed" in the network sense, but it has not been destroyed.  We must close it
313                     // on our end, too to finish up. The reason information does not matter in this case, and we cannot
314                     // linger because it's already closed on the other end, so we just pass 0's.
315                     inter.CloseConnection(info.m_hConn, 0, null, false);
316 
317                 }
318 
319                 // Ignore if they were not previously connected.  (If they disconnected before we accepted the
320                 // connection)
321                 if (info.m_eOldState == ESteamNetworkingConnectionState.Connected) {
322 
323                     // Locate the client. Note that it should have been found, because this is the only codepath where
324                     // we remove clients (except on shutdown), and connection change callbacks are dispatched in queue
325                     // order.
326                     auto connection = info.m_hConn;
327                     auto client = clients[connection];
328 
329                     // Select appropriate log messages
330                     string logMessage;
331 
332                     if (info.m_info.m_eState == ESteamNetworkingConnectionState.ProblemDetectedLocally) {
333 
334                         logMessage = "problem detected locally";
335 
336                         // Send a message so everybody else knows what happened
337                         SendStringToAllClients(
338                             format!"Alas, %s hath fallen into shadow. (%s)"(client.nick, info.m_info.m_szEndDebug)
339                         );
340 
341                     }
342 
343                     else {
344 
345                         // Note that here we could check the reason code to see if
346                         // it was a "usual" connection or an "unusual" one.
347                         logMessage = "closed by peer";
348 
349                         SendStringToAllClients(
350                             format!"%s hath departed"(client.nick)
351                         );
352 
353                     }
354 
355                     // Spew something to our own log. Note that because we put their nick as the connection description,
356                     // it will show up, along with their transport-specific data (e.g. their IP address)
357                     writefln!"Connection %s %s, reason %s: %s\n"(
358                         info.m_info.m_szConnectionDescription,
359                         logMessage,
360                         info.m_info.m_eEndReason,
361                         info.m_info.m_szEndDebug
362                     );
363 
364                     clients.remove(connection);
365 
366                 }
367 
368                 else assert(info.m_eOldState == ESteamNetworkingConnectionState.Connecting);
369 
370                 break;
371 
372             case ESteamNetworkingConnectionState.Connecting:
373 
374                 // This must be a new connection
375                 assert(info.m_hConn !in clients);
376 
377                 writefln!"Connection request from %s"(info.m_info.m_szConnectionDescription);
378 
379                 // A client is attempting to connect
380                 // Try to accept the connection.
381                 if (inter.AcceptConnection(info.m_hConn) != EResult.OK) {
382 
383                     // This could fail. If the remote host tried to connect, but then disconnected, the connection may
384                     // already be half closed. Just destroy whatever we have on our side.
385                     inter.CloseConnection(info.m_hConn, 0, null, false);
386 
387                     writefln!"Can't accept connection. (It was already closed?)";
388                     break;
389 
390                 }
391 
392                 // Assign the poll group
393                 if (!inter.SetConnectionPollGroup(info.m_hConn, pollGroup)) {
394 
395                     inter.CloseConnection(info.m_hConn, 0, null, false);
396 
397                     writefln!"Failed to set poll group?";
398                     break;
399                 }
400 
401                 import std.random;
402 
403                 // Generate a random nick. A random temporary nick is really dumb and not how you would write a real
404                 // chat server.  You would want them to have some sort of signon message, and you would keep their
405                 // client in a state of limbo (connected, but not logged on) until them.  I'm trying to keep this
406                 // example code really simple.
407                 auto nick = format!"BraveWarrior%s"(10_000 + uniform(0, 100_000));
408 
409                 // Send them a welcome message
410                 SendStringToClient(info.m_hConn,
411                     format!("Welcome, stranger.  Thou art known to us for now as '%s'; upon thine command '/nick' we "
412                         ~ "shall know thee otherwise.")(nick)
413                 );
414 
415                 // Also send them a list of everybody who is already connected
416                 if (clients.empty) {
417                     SendStringToClient(info.m_hConn, "Thou art utterly alone.");
418                 }
419 
420                 else {
421 
422                     SendStringToClient(info.m_hConn, format!"%s companions greet you:"(clients.length));
423 
424                     foreach (client; clients) {
425 
426                         SendStringToClient(info.m_hConn, client.nick);
427 
428                     }
429 
430                 }
431 
432                 // Let everybody else know who they are for now
433                 SendStringToAllClients(
434                     format!"Hark!  A stranger hath joined this merry host.  For now we shall call them '%s'"(nick),
435                     info.m_hConn
436                 );
437 
438                 // Add them to the client list, using std::map wacky syntax
439                 clients[info.m_hConn] = Client(nick);
440                 break;
441 
442             case ESteamNetworkingConnectionState.Connected:
443 
444                 // We will get a callback immediately after accepting the connection.
445                 // Since we are the server, we can ignore this, it's not news to us.
446                 break;
447 
448             default: break;
449 
450         }
451 
452     }
453 
454     void PollConnectionStateChanges() {
455 
456         instance = this;
457         inter.RunCallbacks();
458 
459     }
460 
461 }
462 
463 /// Our chat client
464 class ChatClient {
465 
466     static ChatClient instance;
467 
468     HSteamNetConnection connection;
469     ISteamNetworkingSockets inter;
470 
471     static void SteamNetConnectionStatusChangedCallback(SteamNetConnectionStatusChangedCallback_t* info) {
472 
473         instance.OnSteamNetConnectionStatusChanged(info);
474 
475     }
476 
477     void Run(const ref SteamNetworkingIPAddr serverAddr) {
478 
479         // Select instance to use.  For now we'll always use the default.
480         inter = SteamNetworkingSockets();
481 
482         // Start connecting
483         char[SteamNetworkingIPAddr.k_cchMaxString] szAddr;
484         serverAddr.ToString(szAddr.ptr, szAddr.length, true);
485 
486         writefln!"Connecting to chat server at %s"(szAddr);
487         SteamNetworkingConfigValue_t opt;
488 
489         opt.SetPtr(
490             ESteamNetworkingConfigValue.Callback_ConnectionStatusChanged,
491             &SteamNetConnectionStatusChangedCallback
492         );
493 
494         connection = inter.ConnectByIPAddress(serverAddr, 1, &opt);
495 
496         enforce(connection != k_HSteamNetConnection_Invalid, "Failed to create connection");
497 
498         while (!quit.atomicLoad) {
499 
500             PollIncomingMessages();
501             PollConnectionStateChanges();
502             PollLocalUserInput();
503 
504             Thread.sleep(10.msecs);
505 
506         }
507     }
508 
509     void PollIncomingMessages() {
510 
511         while (!quit.atomicLoad) {
512 
513             SteamNetworkingMessage_t* incomingMsg;
514 
515             int numMsgs = inter.ReceiveMessagesOnConnection(connection, &incomingMsg, 1);
516 
517             if (numMsgs == 0) break;
518 
519             assert(numMsgs > 0, "Error checking for messages");
520 
521             // Clean up
522             scope (exit) incomingMsg.Release();
523 
524             // Just echo anything we get from the server
525             writeln(cast(string) incomingMsg.m_pData[0..incomingMsg.m_cbSize]);
526 
527         }
528     }
529 
530     void PollLocalUserInput() {
531 
532         while (!quit.atomicLoad && !outbox.empty) {
533 
534             auto cmd = outbox.front;
535             outbox.popFront;
536 
537             // Check for known commands
538             if (cmd.startsWith("/quit")) {
539 
540                 quit.atomicStore = true;
541                 writefln!"Disconnecting from chat server. Press <Enter> to continue.";
542 
543                 // Close the connection gracefully.
544                 // We use linger mode to ask for any remaining reliable data to be flushed out. But remember this is an
545                 // application protocol on UDP. See ShutdownSteamDatagramConnectionSockets
546                 inter.CloseConnection(connection, 0, "Goodbye", true);
547 
548                 break;
549 
550             }
551 
552             // Anything else, just send it to the server and let them parse it
553             inter.SendMessageToConnection(connection, cmd.ptr, cast(uint) cmd.length, k_nSteamNetworkingSend_Reliable,
554                 null);
555 
556         }
557 
558     }
559 
560     void OnSteamNetConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* info) {
561 
562         assert(info.m_hConn == connection || connection == k_HSteamNetConnection_Invalid);
563 
564         // What's the state of the connection?
565         switch (info.m_info.m_eState) {
566 
567             case ESteamNetworkingConnectionState.None:
568                 // NOTE: We will get callbacks here when we destroy connections.  You can ignore these.
569                 break;
570 
571             case ESteamNetworkingConnectionState.ClosedByPeer:
572             case ESteamNetworkingConnectionState.ProblemDetectedLocally:
573 
574                 quit.atomicStore = true;
575 
576                 scope (exit) {
577 
578                     // Clean up the connection.  This is important!
579                     // The connection is "closed" in the network sense, but it has not been destroyed. We must close it on
580                     // our end, too to finish up. The reason information do not matter in this case, and we cannot linger
581                     // because it's already closed on the other end, so we just pass 0's.
582                     inter.CloseConnection(info.m_hConn, 0, null, false);
583                     connection = k_HSteamNetConnection_Invalid;
584 
585                 }
586 
587                 // Print an appropriate message
588                 if (info.m_eOldState == ESteamNetworkingConnectionState.Connecting) {
589 
590                     // Note: we could distinguish between a timeout, a rejected connection,
591                     // or some other transport problem.
592                     writefln!"We sought the remote host, yet our efforts were met with defeat. (%s)"(
593                         info.m_info.m_szEndDebug);
594 
595                 }
596 
597                 else if (info.m_info.m_eState == ESteamNetworkingConnectionState.ProblemDetectedLocally) {
598 
599                     writefln!"Alas, troubles beset us; we have lost contact with the host. (%s)"(
600                         info.m_info.m_szEndDebug);
601 
602                 }
603 
604                 // NOTE: We could check the reason code for a normal disconnection
605                 else writefln!"The host hath bidden us farewell. (%s)"(info.m_info.m_szEndDebug);
606 
607                 break;
608 
609             case ESteamNetworkingConnectionState.Connecting:
610                 // We will get this callback when we start connecting.
611                 // We can ignore this.
612                 break;
613 
614             case ESteamNetworkingConnectionState.Connected:
615                 writefln!"Connected to server OK";
616                 break;
617 
618             default: break;
619 
620         }
621 
622     }
623 
624     void PollConnectionStateChanges() {
625 
626         instance = this;
627         inter.RunCallbacks();
628 
629     }
630 
631 }
632 
633 void main(string[] args) {
634 
635     import std.getopt;
636 
637     auto server = false;
638     auto port = defaultServerPort;
639 
640     SteamNetworkingIPAddr addrServer;
641     addrServer.Clear();
642 
643     auto help = args.getopt(
644         "port|p", "Port to use, if running as a server", &port,
645         "server|s", "Run as a server", &server,
646     );
647 
648     // User requested help
649     if (help.helpWanted) {
650 
651         help:
652 
653         defaultGetoptPrinter("An example chat program", help.options);
654         return;
655 
656     }
657 
658     // Unknown argument
659     if (args.length > 1) {
660 
661         // Must be the server address to connect to!
662         if (!server && addrServer.IsIPv6AllZeros) {
663 
664             enforce(addrServer.ParseString(args[1].toStringz));
665 
666             // No port set
667             if (addrServer.m_port == 0) {
668 
669                 addrServer.m_port = defaultServerPort;
670 
671             }
672 
673         }
674 
675         // Nope.
676         else goto help;
677 
678     }
679 
680     // No address set as client.
681     if (!server && addrServer.IsIPv6AllZeros()) goto help;
682 
683     // Create client and server sockets
684     //InitSteamDatagramConnectionSockets();
685 
686     SteamNetworkingErrMsg errMsg;
687     enforce(GameNetworkingSockets_Init(null, errMsg), errMsg.format!"GameNetworkingSockets_Init failed. %s");
688     scope (exit) GameNetworkingSockets_Kill();
689 
690     // Start input
691     auto inputTid = spawn(&userInputEntrypoint);
692 
693     if (!server) {
694 
695         auto prog = new ChatClient;
696         prog.Run(addrServer);
697 
698     }
699 
700     else {
701 
702         auto prog = new ChatServer;
703         prog.Run(cast(ushort) port);
704 
705     }
706 
707     //ShutdownSteamDatagramConnectionSockets();
708 
709 }