1 module mcrcd; 2 3 import std.string; 4 import std.socket; 5 import std.bitmanip; 6 import core.time; 7 8 /// Response for Rcon packets. 9 struct MCRconResponse 10 { 11 /// 12 this(ubyte[] _data, int _responseID = -1) 13 { 14 data = _data; 15 responseID = _responseID; 16 } 17 18 /// Response ID for packets. `-1` on login error. 19 int responseID; 20 21 /// Automatically removes all § codes from text. 22 @property string unformatted() const 23 { 24 string raw = text.idup; 25 size_t index; 26 while((index = raw.indexOf('§')) != -1) 27 { 28 if(index < raw.length - 2) 29 raw = raw[0 .. index] ~ raw[index + 3 .. $]; 30 else if(index < raw.length - 1) 31 raw = raw[0 .. index] ~ raw[index + 2 .. $]; 32 else 33 break; 34 } 35 return raw; 36 } 37 38 union 39 { 40 ubyte[] data; /// Raw data containing response. 41 char[] text; /// ditto 42 } 43 } 44 45 unittest 46 { 47 MCRconResponse res; 48 res.text = "foo§".dup; 49 assert(res.unformatted == "foo"); 50 res.text = "foo§f".dup; 51 assert(res.unformatted == "foo"); 52 } 53 54 /// 55 enum MCRconPacket : int 56 { 57 Command = 2, /// 58 Login = 3 /// 59 } 60 61 /// Rcon class for connections. 62 class MCRcon 63 { 64 private: 65 Socket _socket; 66 public: 67 /// Connects to `host:port` and creates a new socket. 68 void connect(string host, short port) 69 { 70 assert(!isConnected, "Still connected!"); 71 _socket = new TcpSocket(); 72 _socket.connect(new InternetAddress(host, port)); 73 } 74 75 /// Disconnects from the server. 76 void disconnect() 77 { 78 _socket.close(); 79 _socket = null; 80 } 81 82 /// Returns if still connected to the server. 83 @property bool isConnected() 84 { 85 return _socket !is null && _socket.isAlive(); 86 } 87 88 /// Sends a packet containing `data` with `packetID` as ID and returns the data synchronously. 89 MCRconResponse send(MCRconPacket packetID, string data) 90 { 91 assert(isConnected, "Cannot send data without being connected"); 92 ubyte[] payload = cast(ubyte[])[0, 0, 0, 0] ~ cast(ubyte[])nativeToLittleEndian(cast(int)packetID) ~ cast(ubyte[])data ~ cast(ubyte[])[0, 0]; 93 assert(_socket.send(cast(ubyte[])nativeToLittleEndian(cast(int)payload.length) ~ payload) != Socket.ERROR, "Couldn't send packet! " ~ _socket.getErrorText()); 94 95 MCRconResponse response; 96 97 while(true) 98 { 99 ubyte[4] recv = new ubyte[4]; 100 assert(_socket.receive(recv) > 0, "Couldn't receive packet! " ~ _socket.getErrorText()); 101 int packLength = littleEndianToNative!int(recv); 102 103 ubyte[] packet = new ubyte[packLength]; 104 assert(_socket.receive(packet) > 0, "Couldn't receive packet! " ~ _socket.getErrorText()); 105 106 response.responseID = littleEndianToNative!int(packet[0 .. 4]); 107 int packetType = littleEndianToNative!int(packet[4 .. 8]); 108 response.data ~= packet[8 .. $ - 2]; 109 assert(packet[$ - 2 .. $] == cast(ubyte[])[0, 0], "Invalid padding"); 110 assert(response.responseID != -1, "Login failed"); 111 112 auto sockIn = new SocketSet(1); 113 sockIn.add(_socket); 114 115 if(Socket.select(sockIn, new SocketSet(0), new SocketSet(0), 0.msecs) == 0) 116 return response; 117 } 118 } 119 120 /// Shorthand for `send(MCRconPacket.Command, command)` 121 auto command(string command) 122 { 123 return send(MCRconPacket.Command, command); 124 } 125 126 /// Shorthand for `send(MCRconPacket.Login, password)` 127 auto login(string password) 128 { 129 return send(MCRconPacket.Login, password); 130 } 131 }