1 /// Query the nwn2 server 2 module nwn.nwnserver; 3 4 import nwnlibd.parseutils; 5 6 import std.stdio; 7 import std.datetime; 8 import std.socket; 9 import std.exception: enforce; 10 import std.typecons: tuple; 11 import std.conv; 12 import std.bitmanip: littleEndianToNative; 13 import std.stdint; 14 15 /// 16 class NWNServer{ 17 /// 18 this(in string host, in ushort port = 5121){ 19 sock = new UdpSocket(); 20 sock.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(5)); 21 target = new InternetAddress(host, port); 22 sock.connect(target); 23 } 24 ~this(){ 25 sock.close(); 26 } 27 28 /// Send ping request to the server and measure latency in msecs 29 double ping(){ 30 import std.datetime.stopwatch: StopWatch, to, TickDuration; 31 StopWatch sw; 32 sw.start(); 33 queryBNLM(0, 0); 34 return (cast(TickDuration)sw.peek()).to!("msecs", double); 35 } 36 37 /// 38 struct BNERU { 39 uint16_t port; 40 ubyte __padding; 41 string serverName; 42 } 43 /// Query server name info 44 BNERU queryBNES(){ 45 sock.send("BNES" ~ localPort ~ '\0'); 46 47 ubyte[] buff; 48 buff.length = 128; 49 const len = sock.receive(buff); 50 enforce(len > 0, "Server did not answer"); 51 enforce(len > 5 && buff[0..5] == "BNERU", "Wrong answer received"); 52 auto cr = ChunkReader(buff[5 .. len]); 53 54 return BNERU( 55 cr.read!(ubyte[2]).littleEndianToNative!(uint16_t, 2), 56 cr.read!ubyte, 57 cr.readArray!char(cr.read!ubyte).to!string, 58 ); 59 } 60 61 /// 62 static struct BNDR { 63 uint16_t port; 64 string serverDesc; 65 string moduleDesc; 66 string gameVersion; 67 enum GameType: uint16_t { 68 Action = 0, 69 Story = 1, 70 Story_lite = 2, 71 Role_Play = 3, 72 Team = 4, 73 Melee = 5, 74 Arena = 6, 75 Social = 7, 76 Alternative = 8, 77 PW_Action = 9, 78 PW_Story = 10, 79 Solo = 11, 80 Tech_Support = 12, 81 } 82 GameType gameType; 83 string webUrl; 84 string filesUrl; 85 } 86 87 /// Query server game / module info 88 BNDR queryBNDS(){ 89 sock.send("BNDS" ~ localPort); 90 91 ubyte[] buff; 92 buff.length = 2^^14; 93 const len = sock.receive(buff); 94 enforce(len > 0, "Server did not answer"); 95 enforce(len > 4 && buff[0..4] == "BNDR", "Wrong answer received"); 96 auto cr = ChunkReader(buff[4 .. len]); 97 98 return BNDR( 99 cr.read!(ubyte[2]).littleEndianToNative!(uint16_t, 2), 100 cr.readArray!char(cr.read!(ubyte[4]).littleEndianToNative!(uint32_t, 4)).to!string, 101 cr.readArray!char(cr.read!(ubyte[4]).littleEndianToNative!(uint32_t, 4)).to!string, 102 cr.readArray!char(cr.read!(ubyte[4]).littleEndianToNative!(uint32_t, 4)).to!string, 103 cr.read!(BNDR.GameType), 104 cr.readArray!char(cr.read!(ubyte[4]).littleEndianToNative!(uint32_t, 4)).to!string, 105 cr.readArray!char(cr.read!(ubyte[4]).littleEndianToNative!(uint32_t, 4)).to!string, 106 ); 107 } 108 109 /// 110 static struct BNXR { 111 uint16_t port; 112 ubyte bnxiVersion; 113 bool hasPassword; 114 ubyte minLevel; 115 ubyte maxLevel; 116 ubyte currentPlayers; 117 ubyte maxPlayers; 118 enum VaultType: ubyte { 119 server = 0, 120 local = 1, 121 } 122 VaultType vaultType; 123 enum PvpType: ubyte { 124 none = 0, 125 party = 1, 126 full = 2, 127 } 128 PvpType pvp; 129 bool playerPause; 130 bool oneParty; 131 bool enforceLegalChars; 132 bool itemLvlRestriction; 133 bool xp; 134 string modName; 135 string gameVersion; 136 } 137 138 /// Query server config 139 BNXR queryBNXI(){ 140 sock.send("BNXI" ~ localPort); 141 142 ubyte[] buff; 143 buff.length = 256; 144 const len = sock.receive(buff); 145 enforce(len > 0, "Server did not answer"); 146 enforce(len > 4 && buff[0..4] == "BNXR", "Wrong answer received"); 147 auto cr = ChunkReader(buff[4 .. len]); 148 149 return BNXR( 150 cr.read!(ubyte[2]).littleEndianToNative!(uint16_t, 2), 151 cr.read!ubyte, 152 cr.read!bool, 153 cr.read!ubyte, 154 cr.read!ubyte, 155 cr.read!ubyte, 156 cr.read!ubyte, 157 cr.read!(BNXR.VaultType), 158 cr.read!(BNXR.PvpType), 159 cr.read!bool, 160 cr.read!bool, 161 cr.read!bool, 162 cr.read!bool, 163 cr.read!bool, 164 cr.readArray!char(cr.read!ubyte).to!string, 165 cr.readArray!char(cr.read!ubyte).to!string, 166 ); 167 } 168 169 /// 170 static struct BNLR { 171 uint16_t port; 172 ubyte messageNo; 173 ubyte sessionID; 174 ubyte[3] unknown; 175 } 176 /// 177 BNLR queryBNLM(ubyte messageNo = 0, ubyte sessionID = 0){ 178 sock.send("BNLM" ~ localPort ~ messageNo ~ sessionID ~ hexString!"00 00 00"); 179 180 ubyte[] buff; 181 buff.length = 128; 182 const len = sock.receive(buff); 183 enforce(len > 0, "Server did not answer"); 184 enforce(buff.length > 4 && buff[0..4] == "BNLR", "Wrong answer received"); 185 auto cr = ChunkReader(buff[4 .. len]); 186 187 return cr.readPackedStruct!BNLR; 188 } 189 190 191 private: 192 Address target; 193 UdpSocket sock; 194 195 char[2] localPort(){ 196 short port = (cast(InternetAddress)sock.localAddress).port; 197 return (cast(char*)&port)[0..2]; 198 } 199 } 200 201 202 // Travis seems to block UDP requests 203 //unittest{ 204 // auto gs = new NWNServer("lcda-nwn2.fr", 5121); 205 206 // gs.ping(); 207 // assert(gs.queryBNDS().webUrl == "https://lcda-nwn2.fr"); 208 // assert(gs.queryBNES().serverName == "FR]La Colere d'Aurile"); 209 // assert(gs.queryBNXI().modName == "Lcda"); 210 //}