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 //}