1 /* 2 * Copyright (c) 2017-2019 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: Copyright (c) 2017-2019 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.hncom : AbstractNode; 45 import selery.hub.server : HubServer; 46 47 import HncomPlayer = selery.hncom.player; 48 49 class World { 50 51 public immutable uint id; 52 public immutable uint groupId; 53 public immutable string name; 54 public immutable ubyte dimension; 55 56 public shared this(uint id, uint groupId, string name, ubyte dimension) { 57 this.id = id; 58 this.groupId = groupId; 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 }