1 /*
2  * Copyright (c) 2017-2018 sel-project
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in all
12  * copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20  * SOFTWARE.
21  *
22  */
23 /**
24  * Copyright: 2017-2018 sel-project
25  * License: MIT
26  * Authors: Kripth
27  * Source: $(HTTP github.com/sel-project/selery/source/selery/hub/player.d, selery/hub/player.d)
28  */
29 module selery.hub.player;
30 
31 import core.atomic : atomicOp;
32 
33 import std.algorithm : sort;
34 import std.base64 : Base64;
35 import std.conv : to;
36 import std.json : JSONValue;
37 import std.socket : Address;
38 import std.string : toLower;
39 import std.uuid : UUID;
40 
41 import sel.server.client : InputMode, Client;
42 
43 import selery.about;
44 import selery.hub.handler.hncom : AbstractNode;
45 import selery.hub.server : HubServer;
46 
47 import HncomPlayer = sel.hncom.player;
48 
49 class World {
50 	
51 	public immutable uint id;
52 	public immutable string name;
53 	public immutable ubyte dimension;
54 	
55 	public World parent;
56 	
57 	public shared this(uint id, string name, ubyte dimension) {
58 		this.id = id;
59 		this.name = name;
60 		this.dimension = dimension;
61 	}
62 	
63 }
64 
65 /**
66  * Session for players.
67  */
68 class PlayerSession {
69 
70 	private shared HubServer server;
71 	private shared Client client;
72 	
73 	private shared AbstractNode _node; // shouldn't be null
74 	private shared uint last_node = -1;
75 	
76 	private shared uint expected;
77 	private shared ubyte[][size_t] unordered_payloads;
78 	
79 	protected immutable string lower_username;
80 	protected shared string _display_name;
81 
82 	protected shared ubyte _permission_level;
83 
84 	protected shared World _world;
85 	protected shared ubyte _dimension;
86 	protected shared ubyte _view_distance;
87 
88 	protected shared string _language;
89 	protected shared Skin _skin = null;
90 	
91 	public shared this(shared HubServer server, shared Client client) {
92 		this.server = server;
93 		this.client = client;
94 		this.lower_username = this._display_name = username;
95 		this._language = client.language.length ? client.language : server.config.lang.language;
96 		if(client.skinName.length) this._skin = cast(shared)new Skin(client.skinName, client.skinData, client.skinGeometryName, client.skinGeometryData, client.skinCape);
97 	}
98 
99 	public final shared nothrow @property @safe @nogc uint id() {
100 		return this.client.id;
101 	}
102 	
103 	/**
104 	 * Gets the game type as an unsigned byte identifier.
105 	 * The types are indicated in module sel.server.client, in the
106 	 * Client's class.
107 	 * Example:
108 	 * ---
109 	 * import sel.hncom.about;
110 	 * 
111 	 * if(player.type == Client.JAVA) {
112 	 *    log(player.username, " is on Java Edition");
113 	 * }
114 	 * ---
115 	 */
116 	public final shared nothrow @property @safe @nogc ubyte type() {
117 		return this.client.type;
118 	}
119 
120 	/**
121 	 * Gets the protocol number used by the client.
122 	 * It may be 0 if the packet with the protocol number didn't
123 	 * come yet.
124 	 */
125 	public final shared nothrow @property @safe @nogc uint protocol() {
126 		return this.client.protocol;
127 	}
128 
129 	/**
130 	 * Gets the client's game name.
131 	 * Examples:
132 	 * "Minecraft"
133 	 * "Minecraft: Java Edition"
134 	 * "Minecraft: Education Edition"
135 	 */
136 	public final shared nothrow @property @safe @nogc string gameName() {
137 		return this.client.gameName;
138 	}
139 
140 	/**
141 	 * Gets the client's game version, which could either be calculated
142 	 * from the protocol number or given by the client.
143 	 * Example:
144 	 * ---
145 	 * if(player.type == Client.JAVA)
146 	 *    assert(supportedMinecraftProtocols[player.protocol].canFind(player.gameVersion));
147 	 * ---
148 	 */
149 	public final shared nothrow @property @safe @nogc string gameVersion() {
150 		return this.client.gameVersion;
151 	}
152 	
153 	/**
154 	 * Gets the game type and version as a human-readable string.
155 	 * Examples:
156 	 * "Minecraft 1.2.0"
157 	 * "Minecraft: Java Edition 1.12"
158 	 * "Minecraft: Education Edition 1.1.5"
159 	 */
160 	public final shared nothrow @property @safe string game() {
161 		return this.client.game;
162 	}
163 	
164 	/**
165 	 * Gets the UUID. It's unique when online-mode is se to true
166 	 * and can be used to identify a player.
167 	 * When online-mode is set to false it is randomly generated
168 	 * and its uses are very limited.
169 	 */
170 	public final shared nothrow @property @safe @nogc UUID uuid() {
171 		return this.client.uuid;
172 	}
173 	
174 	/**
175 	 * Gets the SEL UUID, a unique identifier for the player in
176 	 * the session. It's composed by 17 bytes (type and UUID)
177 	 * and it will never change for authenticated players.
178 	 * Example:
179 	 * ---
180 	 * assert(session.type == session.suuid[0]);
181 	 * ---
182 	 */
183 	public final shared nothrow @property @safe const(suuid_t) suuid() {
184 		immutable(ubyte)[17] data = this.type ~ this.uuid.data;
185 		return data;
186 	}
187 	
188 	/**
189 	 * Gets the player's username, mantaining the case given
190 	 * by the login packet.
191 	 * It doesn't change during the life of the session.
192 	 */
193 	public final shared nothrow @property @safe @nogc string username() {
194 		return this.client.username;
195 	}
196 	
197 	/**
198 	 * Gets the player's lowercase username.
199 	 * Example:
200 	 * ---
201 	 * assert(session.username.toLower == session.iusername);
202 	 * ---
203 	 */
204 	public final shared @property @safe string iusername() {
205 		return this.lower_username;
206 	}
207 	
208 	/**
209 	 * Gets and sets the player's display name.
210 	 * It can contain formatting codes and it could change during
211 	 * the session's lifetime (if modified by a node).
212 	 * It's usually displayed in the nametag and in the players list.
213 	 */
214 	public final shared nothrow @property @safe @nogc string displayName() {
215 		return this._display_name;
216 	}
217 
218 	/// ditto
219 	public final shared @property string displayName(string displayName) {
220 		this._node.sendDisplayNameUpdate(this, displayName);
221 		return this._display_name = displayName;
222 	}
223 
224 	public final shared nothrow @property @safe @nogc ubyte permissionLevel() {
225 		return this._permission_level;
226 	}
227 
228 	public final shared @property ubyte permissionLevel(ubyte permissionLevel) {
229 		this._node.sendPermissionLevelUpdate(this, permissionLevel);
230 		return this._permission_level = permissionLevel;
231 	}
232 
233 	/**
234 	 * Gets the player's world, which is updated by the node every
235 	 * time the client changes dimension.
236 	 */
237 	public final shared nothrow @property @safe @nogc shared(World) world() {
238 		return this._world;
239 	}
240 	
241 	public final shared nothrow @property @safe @nogc shared(World) world(shared World world) {
242 		this._dimension = world.dimension;
243 		return this._world = world;
244 	}
245 
246 	/// ditto
247 	public final shared nothrow @property @safe @nogc byte dimension() {
248 		return this._dimension;
249 	}
250 
251 	public final shared nothrow @property @safe @nogc ubyte viewDistance() {
252 		return this._view_distance;
253 	}
254 
255 	public final shared @property ubyte viewDistance(ubyte viewDistance) {
256 		this._node.sendViewDistanceUpdate(this, viewDistance);
257 		return this._view_distance = viewDistance;
258 	}
259 
260 	/**
261 	 * Gets the player's input mode.
262 	 */
263 	public final shared nothrow @property @safe @nogc InputMode inputMode() {
264 		return this.client.inputMode;
265 	}
266 
267 	/**
268 	 * Gets the player's remote address. it's usually an ipv4, ipv6
269 	 * or an ipv4-mapped ipv6.
270 	 * Example:
271 	 * ---
272 	 * if(session.address.addressFamily == AddressFamily.INET6) {
273 	 *    log(session, " is connected through IPv6");
274 	 * }
275 	 * ---
276 	 */
277 	public final shared nothrow @property @trusted @nogc Address address() {
278 		return this.client.address;
279 	}
280 
281 	/**
282 	 * IP used by the client to connect to the server.
283 	 * It's a string and can either be a numerical ip or a full url.
284 	 */
285 	public final shared nothrow @property @safe @nogc string serverIp() {
286 		return this.client.serverIp;
287 	}
288 
289 	/**
290 	 * Port used by the client to connect to the server.
291 	 */
292 	public final shared nothrow @property @safe @nogc ushort serverPort() {
293 		return this.client.serverPort;
294 	}
295 	
296 	/**
297 	 * Gets the player's latency.
298 	 * Not being calculated using an ICMP protocol the value may not be
299 	 * completely accurate.
300 	 */
301 	public shared nothrow @property @safe @nogc uint latency() {
302 		return 0; //TODO
303 	}
304 	
305 	/**
306 	 * Gets the player's latency.
307 	 * It returns a floating point value between 0 and 100 where
308 	 * 0 is no packet loss and 100 is every packet lost since the
309 	 * last check.
310 	 * If the client uses a stream-oriented connection the value
311 	 * will always be 0.
312 	 */
313 	public shared nothrow @property @safe @nogc float packetLoss() {
314 		return 0f; //TODO
315 	}
316 	
317 	/**
318 	 * Gets/sets the player's language, indicated as code_COUNTRY.
319 	 */
320 	public final shared nothrow @property @safe @nogc string language() {
321 		return this._language;
322 	}
323 
324 	public final shared @property string language(string language) {
325 		this._node.sendLanguageUpdate(this, language);
326 		return this._language = language;
327 	}
328 	
329 	/**
330 	 * Gets the player's skin as a Skin object.
331 	 * If the player has no skin the object will be null.
332 	 */
333 	public final shared nothrow @property @trusted @nogc Skin skin() {
334 		return cast()this._skin;
335 	}
336 
337 	public shared JSONValue hncomAddData() {
338 		return this.client.gameData;
339 	}
340 	
341 	/**
342 	 * Tries to connect the player to a node.
343 	 * This function does not notify the old node of the change,
344 	 * as the old node should have called the function.
345 	 */
346 	public shared bool connect(ubyte reason, int nodeId=-1, ubyte[] message=[], ubyte onFail=HncomPlayer.Transfer.DISCONNECT) {
347 		shared AbstractNode[] nodes;
348 		if(nodeId < 0) {
349 			nodes = this.server.mainNodes;
350 		} else {
351 			auto node = this.server.nodeById(nodeId);
352 			if(node !is null) nodes = [node];
353 		}
354 		foreach(node ; nodes) {
355 			if(node.accepts(this.type, this.protocol)) {
356 				this._node = node;
357 				this.last_node = node.id;
358 				this.expected = 0;
359 				this.unordered_payloads.clear();
360 				node.addPlayer(this, reason, message);
361 				return true;
362 			}
363 		}
364 		if(onFail == HncomPlayer.Transfer.AUTO) {
365 			return this.connect(reason);
366 		} else if(onFail == HncomPlayer.Transfer.RECONNECT && this.last_node != -1) {
367 			return this.connect(reason, this.last_node);
368 		} else {
369 			this.endOfStream();
370 			return false;
371 		}
372 	}
373 	
374 	/**
375 	 * Calls the connect function using 'first join' as reason
376 	 * and, if it successfully connects to a node, add the player
377 	 * to the server.
378 	 */
379 	public shared bool firstConnect() {
380 		return this.connect(HncomPlayer.Add.FIRST_JOIN);
381 	}
382 	
383 	/**
384 	 * Function called when the player is manually
385 	 * transferred by the hub to a node.
386 	 */
387 	public shared void transfer(uint node) {
388 		this._node.onPlayerTransferred(this);
389 		this.connect(HncomPlayer.Add.TRANSFERRED, node);
390 	}
391 	
392 	/**
393 	 * Sends the latency to the connected node.
394 	 */
395 	protected shared void sendLatency() {
396 		this._node.sendLatencyUpdate(this);
397 	}
398 	
399 	/**
400 	 * Sends the packet loss to the connected node.
401 	 */
402 	protected shared void sendPacketLoss() {
403 		this._node.sendPacketLossUpdate(this);
404 	}
405 
406 	/**
407 	 *  Forwards a game packet to the node.
408 	 */
409 	public shared void sendToNode(ubyte[] payload) {
410 		this._node.sendTo(this, payload);
411 	}
412 	
413 	/**
414 	 * Sends a packet from the node and mantains the order.
415 	 */
416 	public final shared void sendOrderedFromNode(uint order, ubyte[] payload) {
417 		if(order == this.expected) {
418 			this.sendFromNode(payload);
419 			atomicOp!"+="(this.expected, 1);
420 			if(this.unordered_payloads.length) {
421 				size_t[] keys = this.unordered_payloads.keys;
422 				sort(keys);
423 				while(keys.length && keys[0] == this.expected) {
424 					this.sendFromNode(cast(ubyte[])this.unordered_payloads[keys[0]]);
425 					this.unordered_payloads.remove(keys[0]);
426 					keys = keys[1..$];
427 					atomicOp!"+="(this.expected, 1);
428 				}
429 			}
430 		} else {
431 			this.unordered_payloads[order] = cast(shared ubyte[])payload;
432 		}
433 	}
434 	
435 	/**
436 	 * Sends an encoded packet to client that has been created
437 	 * and encoded by the node.
438 	 */
439 	public shared void sendFromNode(ubyte[] payload) {
440 		this.client.directSend(payload);
441 	}
442 	
443 	/**
444 	 * Function called when the player tries to connect to
445 	 * a node that doesn't exist, either because the name is
446 	 * wrong or because there aren't available ones.
447 	 */
448 	protected shared void endOfStream() {
449 		this.kick("disconnect.endOfStream", true, []);
450 	};
451 	
452 	/**
453 	 * Function called when the player is kicked (by the
454 	 * hub or the node).
455 	 * The function should send a disconnection message
456 	 * and close the session.
457 	 */
458 	public shared void kick(string reason, bool translation, string[] params) {
459 		this.client.disconnect(reason, translation, params);
460 	}
461 	
462 	/**
463 	 * Function called when the client times out.
464 	 */
465 	protected shared void onTimedOut() {
466 		this._node.onPlayerTimedOut(this);
467 	}
468 	
469 	/**
470 	 * Function called when the connection is manually closed
471 	 * by the client clicking the disconnect button on the
472 	 * game's interface.
473 	 */
474 	protected shared void onClosedByClient() {
475 		this._node.onPlayerLeft(this);
476 	}
477 	
478 	/**
479 	 * Function called when the client is kicked from
480 	 * the hub (not from the node).
481 	 */
482 	public shared void onKicked(string reason) {
483 		this._node.onPlayerKicked(this);
484 		this.kick(reason, false, []);
485 	}
486 
487 	public shared void onClosed() {
488 		this._node.onPlayerLeft(this);
489 	}
490 	
491 }
492 
493 class Skin {
494 	
495 	public immutable string name;
496 	public immutable(ubyte)[] data;
497 	public string geometryName;
498 	public immutable(ubyte)[] geometryData;
499 	public immutable(ubyte)[] cape;
500 
501 	public ubyte[192] face;
502 	public string faceBase64;
503 	
504 	public this(string name, immutable(ubyte)[] data, string geometryName, immutable(ubyte)[] geometryData, immutable(ubyte)[] cape) {
505 		this.name = name;
506 		this.data = data;
507 		this.geometryName = geometryName;
508 		this.geometryData = geometryData;
509 		this.cape = cape;
510 		ubyte[192] face;
511 		size_t i = 0;
512 		foreach(y ; 0..8) {
513 			foreach(x ; 0..8) {
514 				size_t layer = ((x + 40) + ((y + 8) * 64)) * 4;
515 				if(data[layer] == 0) {
516 					layer = ((x + 8) + ((y + 8) * 64)) * 4;
517 				}
518 				face[i++] = this.data[layer++];
519 				face[i++] = this.data[layer++];
520 				face[i++] = this.data[layer++];
521 			}
522 		}
523 		this.face = face;
524 		this.faceBase64 = Base64.encode(face);
525 	}
526 	
527 }