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 }