001/* 002 * Trident - A Multithreaded Server Alternative 003 * Copyright 2014 The TridentSDK Team 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package net.tridentsdk.server.player; 019 020import com.google.gson.JsonArray; 021import com.google.gson.JsonElement; 022import com.google.gson.JsonObject; 023import com.google.gson.JsonParser; 024import net.tridentsdk.Trident; 025import net.tridentsdk.bar.BarType; 026import net.tridentsdk.base.BoundingBox; 027import net.tridentsdk.base.Position; 028import net.tridentsdk.docs.InternalUseOnly; 029import net.tridentsdk.effect.sound.SoundEffect; 030import net.tridentsdk.effect.sound.SoundEffectType; 031import net.tridentsdk.entity.Entity; 032import net.tridentsdk.entity.living.Player; 033import net.tridentsdk.entity.types.EntityType; 034import net.tridentsdk.event.player.PlayerDisconnectEvent; 035import net.tridentsdk.event.player.PlayerJoinEvent; 036import net.tridentsdk.event.player.PlayerMoveEvent; 037import net.tridentsdk.inventory.Item; 038import net.tridentsdk.meta.ChatColor; 039import net.tridentsdk.meta.MessageBuilder; 040import net.tridentsdk.meta.block.Tile; 041import net.tridentsdk.meta.nbt.CompoundTag; 042import net.tridentsdk.server.TridentServer; 043import net.tridentsdk.server.chunk.ChunkLocationSet; 044import net.tridentsdk.server.concurrent.ThreadsHandler; 045import net.tridentsdk.server.data.MetadataType; 046import net.tridentsdk.server.data.ProtocolMetadata; 047import net.tridentsdk.server.entity.TridentDroppedItem; 048import net.tridentsdk.server.event.EventProcessor; 049import net.tridentsdk.server.netty.ClientConnection; 050import net.tridentsdk.server.netty.packet.Packet; 051import net.tridentsdk.server.packets.play.in.PacketPlayInPlayerClickWindow.ClickAction; 052import net.tridentsdk.server.packets.play.out.*; 053import net.tridentsdk.server.packets.play.out.PacketPlayOutChat.ChatPosition; 054import net.tridentsdk.server.packets.play.out.PacketPlayOutPlayerListItem.PlayerListDataBuilder; 055import net.tridentsdk.server.world.TridentWorld; 056import net.tridentsdk.title.TitleTransition; 057import net.tridentsdk.util.TridentLogger; 058import net.tridentsdk.util.Vector; 059import net.tridentsdk.world.settings.GameMode; 060import net.tridentsdk.world.settings.LevelType; 061 062import javax.annotation.concurrent.ThreadSafe; 063import java.io.BufferedReader; 064import java.io.InputStreamReader; 065import java.net.URL; 066import java.net.URLConnection; 067import java.util.*; 068import java.util.concurrent.ConcurrentHashMap; 069import java.util.function.Predicate; 070import java.util.stream.Stream; 071 072@ThreadSafe 073public class TridentPlayer extends OfflinePlayer { 074 private static final Map<UUID, Player> ONLINE_PLAYERS = new ConcurrentHashMap<>(); 075 private static final int MAX_VIEW = Trident.config().getInt("view-distance", 15); 076 077 private final PlayerConnection connection; 078 public final ChunkLocationSet knownChunks = new ChunkLocationSet(this); 079 private final LinkedHashSet<Integer> dragSlots = new LinkedHashSet<>(); 080 private volatile ClickAction drag; 081 private volatile boolean loggingIn = true; 082 private volatile boolean sprinting; 083 private volatile boolean crouching; 084 private volatile boolean flying; 085 private volatile byte skinFlags; 086 private volatile Locale locale; 087 private volatile int viewDistance = MAX_VIEW; 088 private volatile Item pickedItem; 089 private volatile String header; 090 private volatile String footer; 091 092 private TridentPlayer(UUID uuid, CompoundTag tag, TridentWorld world, ClientConnection connection) { 093 super(uuid, tag, world); 094 095 this.connection = PlayerConnection.createPlayerConnection(connection, this); 096 // inventory.sendTo(this); 097 } 098 099 public static void sendAll(Packet packet) { 100 players().stream().forEach(p -> ((TridentPlayer) p).connection.sendPacket(packet)); 101 } 102 103 public static void sendFiltered(Packet packet, Predicate<Player> predicate) { 104 players().stream() 105 .filter(predicate) 106 .forEach(p -> ((TridentPlayer) p).connection.sendPacket(packet)); 107 } 108 109 public static TridentPlayer spawnPlayer(ClientConnection connection, UUID id, String name) { 110 // determine if this player has logged in before 111 CompoundTag playerTag = OfflinePlayer.getOfflinePlayer( 112 id) == null ? null : OfflinePlayer.getOfflinePlayer(id).asNbt(); 113 114 // if this player is new 115 if (playerTag == null) { 116 playerTag = OfflinePlayer.generatePlayer(id); 117 } 118 119 TridentPlayer p = new TridentPlayer(id, playerTag, TridentServer.WORLD, connection); 120 p.executor = ThreadsHandler.playerExecutor(); 121 122 // fixeme ?? OfflinePlayer.OFFLINE_PLAYERS.put(id, p); 123 ONLINE_PLAYERS.put(id, p); 124 125 p.name = name; 126 127 p.gameMode = GameMode.CREATIVE;//GameMode.of(((IntTag) playerTag.getTag("playerGameType")).value()); 128 129 p.executor.execute(() -> { 130 p.connection.sendPacket(new PacketPlayOutJoinGame().set("entityId", p.entityId()) 131 .set("gamemode", p.gameMode) 132 .set("dimension", p.world().settings().dimension()) 133 .set("difficulty", p.world().settings().difficulty()) 134 .set("maxPlayers", (short) Trident.config().getInt("max-players")) 135 .set("levelType", LevelType.DEFAULT)); 136 137 p.abilities.creative = 1; 138 p.abilities.flySpeed = 0.135F; 139 p.abilities.canFly = 1; 140 141 p.spawnPosition = TridentServer.WORLD.spawnPosition(); 142 143 p.connection.sendPacket(PacketPlayOutPluginMessage.VANILLA_CHANNEL); 144 p.connection.sendPacket(new PacketPlayOutServerDifficulty().set("difficulty", p.world().settings().difficulty())); 145 p.connection.sendPacket(new PacketPlayOutSpawnPosition().set("location", p.spawnLocation())); 146 p.connection.sendPacket(p.abilities.asPacket()); 147 p.connection.sendPacket(new PacketPlayOutPlayerCompleteMove().set("location", 148 p.spawnLocation()).set("flags", (byte) 0)); 149 150 /* 151 sendAll(new PacketPlayOutPlayerListItem() 152 .set("action", 0) 153 .set("playerListData", new PlayerListDataBuilder[]{p.listData()})); 154 155 List<PlayerListDataBuilder> builders = new ArrayList<>(); 156 157 players().stream().filter(player -> !player.equals(p)) 158 .forEach(player -> builders.add(((TridentPlayer) player).listData())); 159 */ 160 161 // p.connection.sendPacket(new PacketPlayOutPlayerListItem() 162 // .set("action", 0) 163 // .set("playerListData", builders.stream().toArray(PlayerListDataBuilder[]::new))); 164 }); 165 166 return p; 167 } 168 169 public static Player getPlayer(UUID id) { 170 return ONLINE_PLAYERS.get(id); 171 } 172 173 public static Collection<Player> players() { 174 return ONLINE_PLAYERS.values(); 175 } 176 177 @Override 178 protected void doEncodeMeta(ProtocolMetadata protocolMeta) { 179 protocolMeta.setMeta(0, MetadataType.BYTE, (byte) ((fireTicks.intValue() == 0 ? 1 : 0) | (isCrouching() ? 2 : 0) 180 | (isSprinting() ? 8 : 0))); // TODO invisibility & blocking/eating 181 protocolMeta.setMeta(10, MetadataType.BYTE, skinFlags); 182 protocolMeta.setMeta(16, MetadataType.BYTE, (byte) 0); // hide cape, might need changing 183 protocolMeta.setMeta(17, MetadataType.FLOAT, 0F); // absorption hearts TODO 184 protocolMeta.setMeta(18, MetadataType.INT, 0); // TODO scoreboard system (this value is the player's score) 185 } 186 187 public boolean isLoggingIn() { 188 return loggingIn; 189 } 190 191 @InternalUseOnly 192 public void resumeLogin() { 193 if (!loggingIn) 194 return; 195 196 knownChunks.update(7); 197 connection.sendPacket(PacketPlayOutStatistics.DEFAULT_STATISTIC); 198 199 // Wait for response 200 for (Entity entity : world().entities()) { 201 // Register mob, packet sent to new player 202 } 203 204 loggingIn = false; 205 spawn(); 206 connection.sendPacket(new PacketPlayOutEntityVelocity() 207 .set("entityId", entityId()) 208 .set("velocity", new Vector(0, -0.07, 0))); 209 connection.sendPacket(new PacketPlayOutGameStateChange().set("reason", 3).set("value", (float) gameMode().asByte())); 210 for (Tile tile : ((TridentWorld) world()).tilesInternal()) { 211 tile.update(this); 212 } 213 214 EventProcessor.fire(new PlayerJoinEvent(this)); 215 216 TridentLogger.get().log(name + " has joined the server"); 217 MessageBuilder builder = new MessageBuilder(name + " has joined the server").color(ChatColor.YELLOW).build(); 218 for (Player player : players()) { 219 TridentPlayer p = (TridentPlayer) player; 220 builder.sendTo(player); 221 222 if (!p.equals(this)) { 223 ProtocolMetadata metadata = new ProtocolMetadata(); 224 encodeMetadata(metadata); 225 226 p.connection.sendPacket(new PacketPlayOutSpawnPlayer() 227 .set("entityId", id) 228 .set("player", this) 229 .set("metadata", metadata)); 230 231 metadata = new ProtocolMetadata(); 232 p.encodeMetadata(metadata); 233 // connection.sendPacket(new PacketPlayOutSpawnPlayer() 234 // .set("entityId", p.id) 235 // .set("player", p) 236 // .set("metadata", metadata)); 237 } 238 } 239 } 240 241 @Override 242 protected void doTick() { 243 int distance = viewDistance(); 244 if (!loggingIn) { 245 ThreadsHandler.chunkExecutor().execute(() -> { 246 knownChunks.clean(distance); 247 knownChunks.update(distance); 248 }); 249 } 250 251 connection.tick(); 252 } 253 254 @Override 255 protected void doRemove() { 256 knownChunks.clear(); 257 258 PacketPlayOutPlayerListItem item = new PacketPlayOutPlayerListItem(); 259 item.set("action", 4).set("playerListData", new PlayerListDataBuilder[]{ 260 new PlayerListDataBuilder().id(uniqueId).values(new Object[0])}); 261 sendAll(item); 262 263 players().forEach(p -> 264 new MessageBuilder(name + " has left the server").color(ChatColor.YELLOW).build().sendTo(p)); 265 TridentLogger.get().log(name + " has left the server"); 266 ONLINE_PLAYERS.remove(uniqueId()); 267 EventProcessor.fire(new PlayerDisconnectEvent(this)); 268 } 269 270 @Override 271 public void setPosition(Position loc) { 272 double dX = loc.x() - position().x(); 273 double dY = loc.y() - position().y(); 274 double dZ = loc.z() - position().z(); 275 276 PlayerMoveEvent event = EventProcessor.fire(new PlayerMoveEvent(this, position(), loc)); 277 278 if (event.isIgnored()) { 279 PacketPlayOutEntityTeleport packet = new PacketPlayOutEntityTeleport(); 280 281 packet.set("entityId", entityId()); 282 packet.set("location", position()); 283 packet.set("onGround", onGround()); 284 285 connection.sendPacket(packet); 286 return; 287 } 288 289 super.setPosition(loc); 290 291 if(/* health() > 0 && */ gameMode() != GameMode.SPECTATE){ 292 BoundingBox checkBox = boundingBox().grow(1, 0.5, 1); 293 ArrayList<Entity> items = position().world().getEntities(this, checkBox, entity -> entity instanceof TridentDroppedItem); 294 items.stream().filter(item -> ((TridentDroppedItem) item).canPickupItem()).forEach(item -> { 295 int started = ((TridentDroppedItem) item).item().quantity(); 296 window().putItem(((TridentDroppedItem) item).item()); 297 298 if(started > ((TridentDroppedItem) item).item().quantity()){ 299 SoundEffect soundEffect = loc.world().playSound(SoundEffectType.RANDOM_POP); 300 soundEffect.setPosition(position().asVector()); 301 soundEffect.apply(this); 302 } 303 304 if(((TridentDroppedItem) item).item().quantity() <= 0){ 305 PacketPlayOutCollectItem collectItem = new PacketPlayOutCollectItem(); 306 collectItem.set("collectedId", item.entityId()); 307 collectItem.set("collectorId", entityId()); 308 sendAll(collectItem); 309 item.remove(); 310 } 311 }); 312 313 if (!items.isEmpty()) { 314 window().sendTo(this); 315 } 316 } 317 318 // fixme floating point comparison 319 if (dX == 0 && dY == 0 && dZ == 0) { 320 sendFiltered(new PacketPlayOutEntityLook().set("entityId", entityId()) 321 .set("location", loc).set("onGround", onGround), player -> !player.equals(this) 322 ); 323 324 return; 325 } 326 327 if (dX > 4 || dY > 4 || dZ > 4 || (ticksExisted.get() & 1) == 0) { 328 sendFiltered(new PacketPlayOutEntityTeleport() 329 .set("entityId", entityId()) 330 .set("location", loc) 331 .set("onGround", onGround), player -> !player.equals(this)); 332 } else { 333 for (Player player : players()) { 334 if (player.equals(this)) continue; 335 336 Packet packet = new PacketPlayOutEntityRelativeMove() 337 .set("entityId", entityId()) 338 .set("difference", new Vector(dX, dY, dZ)) 339 .set("onGround", onGround); 340 341 ((TridentPlayer) player).connection.sendPacket(packet); 342 } 343 } 344 } 345 346 /* 347 * @NotJavaDoc 348 * TODO: Create Message API and utilize it 349 */ 350 public void kickPlayer(String reason) { 351 connection.sendPacket(new PacketPlayOutDisconnect().set("reason", new MessageBuilder(reason).build().asJson())); 352 TridentLogger.get().log(name + " was kicked for " + reason); 353 } 354 355 private static final Map<UUID, String> textures = new ConcurrentHashMap<>(); 356 public PlayerListDataBuilder listData() { 357 String[] texture = texture().split("#"); 358 return new PlayerListDataBuilder() 359 .id(uniqueId) 360 .values(name, 361 1, new Object[]{"textures", texture[0], true, texture[1]}, 362 (int) gameMode.asByte(), 363 0, 364 displayName != null, 365 displayName); 366 } 367 368 // TODO move to login 369 private String texture() { 370 String tex = textures.get(uniqueId()); 371 372 if (tex == null) { 373 try { 374 URL mojang = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + 375 uniqueId.toString().replace("-", "") + "?unsigned=false"); 376 StringBuilder builder = new StringBuilder(); 377 URLConnection connection = mojang.openConnection(); 378 BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); 379 String line; 380 while ((line = reader.readLine()) != null) { 381 builder.append(line).append("\n"); 382 } 383 384 JsonElement object = new JsonParser().parse(builder.toString()); 385 if (object.isJsonNull()) { 386 return " # "; 387 } 388 389 JsonArray properties = object.getAsJsonObject().get("properties").getAsJsonArray(); 390 391 for (int i = 0; i < properties.size(); i++) { 392 JsonObject element = properties.get(i).getAsJsonObject(); 393 if (element.get("name").getAsString().equals("textures")) { 394 String value = element.get("value").getAsString(); 395 String sig = element.get("signature").getAsString(); 396 397 tex = value + "#" + sig; 398 textures.put(uniqueId(), tex); 399 } 400 } 401 } catch (Exception e) { 402 e.printStackTrace(); 403 } 404 } 405 406 return tex; 407 } 408 409 public PlayerConnection connection() { 410 return connection; 411 } 412 413 public static final int SLOT_OFFSET = 36; 414 415 public void setSlot(short slot) { 416 if ((int) slot > 8 || (int) slot < 0) { 417 TridentLogger.get().error(new IllegalArgumentException("Slot must be within the ranges of 0-8")); 418 return; 419 } 420 421 TridentPlayer.super.selectedSlot = slot; 422 423 setSelectedSlot(slot); 424 setHeldItem(heldItem()); // Updates inventory 425 } 426 427 @Override 428 public void sendMessage(String message) { 429 // fixme 430 new MessageBuilder(message) 431 .build() 432 .sendTo(this); 433 } 434 435 @Override 436 public void sendRaw(String... messages) { 437 Stream.of(messages) 438 .filter(m -> m != null) 439 .forEach(message -> connection.sendPacket(new PacketPlayOutChat() 440 .set("jsonMessage", message) 441 .set("position", ChatPosition.CHAT))); 442 } 443 444 @Override 445 public void setGameMode(GameMode mode) { 446 super.setGameMode(mode); 447 448 connection.sendPacket(abilities.asPacket()); 449 } 450 451 public boolean isFlying() { 452 return flying; 453 } 454 455 public void setFlying(boolean flying) { 456 this.flying = flying; 457 458 abilities.flying = flying ? (byte) 1 : (byte) 0; 459 connection.sendPacket(abilities.asPacket()); 460 } 461 462 public boolean isFlyMode() { 463 return abilities.canFly(); 464 } 465 466 public void setFlyMode(boolean flying) { 467 abilities.canFly = flying ? (byte) 1 : (byte) 0; 468 } 469 470 public boolean isSprinting() { 471 return sprinting; 472 } 473 474 public void setSprinting(boolean sprinting) { 475 this.sprinting = sprinting; 476 477 ProtocolMetadata meta = new ProtocolMetadata(); 478 encodeMetadata(meta); 479 sendFiltered(new PacketPlayOutEntityMetadata().set("entityId", entityId()).set("metadata", meta), 480 p -> !p.equals(this)); 481 } 482 483 public boolean isCrouching() { 484 return crouching; 485 } 486 487 @InternalUseOnly 488 public void setCrouching(boolean crouching) { 489 this.crouching = crouching; 490 491 ProtocolMetadata meta = new ProtocolMetadata(); 492 encodeMetadata(meta); 493 sendFiltered(new PacketPlayOutEntityMetadata().set("entityId", entityId()).set("metadata", meta), 494 p -> !p.equals(this)); 495 } 496 497 public void setLocale(Locale locale) { 498 this.locale = locale; 499 } 500 501 public void setSkinFlags(byte flags) { 502 skinFlags = flags; 503 } 504 505 public void setViewDistance(int viewDistance) { 506 this.viewDistance = viewDistance; 507 } 508 509 public int viewDistance() { 510 return Math.min(viewDistance, MAX_VIEW); 511 } 512 513 @Override 514 public boolean connected() { 515 return true; 516 } 517 518 @Override 519 public Player asPlayer() { 520 return this; 521 } 522 523 @Override 524 public EntityType type() { 525 return EntityType.PLAYER; 526 } 527 528 @Override 529 public Item pickedItem() { 530 return pickedItem; 531 } 532 533 @Override 534 public void setPickedItem(Item item) { 535 pickedItem = item; 536 } 537 538 @Override 539 public String header() { 540 return header; 541 } 542 543 @Override 544 public void setHeader(MessageBuilder builder) { 545 if (!builder.isBuilt()) { 546 builder.build(); 547 } 548 549 header = builder.asJson(); 550 connection.sendPacket(new PacketPlayOutPlayerListUpdate() 551 .set("header", header) 552 .set("footer", footer == null ? "{\"text\": \"\"}" : footer)); 553 } 554 555 @Override 556 public String footer() { 557 return footer; 558 } 559 560 @Override 561 public void setFooter(MessageBuilder builder) { 562 if (!builder.isBuilt()) { 563 builder.build(); 564 } 565 566 footer = builder.asJson(); 567 connection.sendPacket(new PacketPlayOutPlayerListUpdate() 568 .set("header", header == null ? "{\"text\": \"\"}" : header) 569 .set("footer", footer)); 570 } 571 572 public LinkedHashSet<Integer> dragSlots() { 573 return dragSlots; 574 } 575 576 public ClickAction drag() { 577 return drag; 578 } 579 580 public void setDrag(ClickAction drag) { 581 this.drag = drag; 582 } 583 584 @Override 585 public void sendBar(BarType barType, String s) { 586 if(barType == BarType.ACTION_BAR) { 587 PacketPlayOutChat actionBarPacket = new PacketPlayOutChat(); 588 actionBarPacket.set("jsonMessage", "{\"text\": \"" + s +"\"}"); 589 actionBarPacket.set("position", ChatPosition.ABOVE_BAR); 590 connection.sendPacket(actionBarPacket); 591 return; 592 } 593 } 594 595 @Override 596 public void sendTitle(String s) { 597 PacketPlayOutTitle titlePacket = new PacketPlayOutTitle(); 598 titlePacket.set("action", PacketPlayOutTitle.TitleAction.TITLE.id()); 599 titlePacket.set("values", new Object[]{ "{\"text\": \"" + s +"\"}" }); 600 connection.sendPacket(titlePacket); 601 } 602 603 @Override 604 public void sendTitle(String s, String s1) { 605 PacketPlayOutTitle subtitlePacket = new PacketPlayOutTitle(); 606 subtitlePacket.set("action", PacketPlayOutTitle.TitleAction.SUBTITLE.id()); 607 subtitlePacket.set("values", new Object[] { "{\"text\": \"" + s1 +"\"}" }); 608 sendTitle(s); 609 connection.sendPacket(subtitlePacket); 610 } 611 612 @Override 613 public void sendTitle(String s, TitleTransition titleTransition) { 614 PacketPlayOutTitle transitionPacket = new PacketPlayOutTitle(); 615 transitionPacket.set("action", PacketPlayOutTitle.TitleAction.TIMES_AND_DISPLAY.id()); 616 transitionPacket.set("values", new Object[] { titleTransition.getFadeInTime(), titleTransition.getTitleTime(), titleTransition.getFadeOutTime() }); 617 sendTitle(s); 618 connection.sendPacket(transitionPacket); 619 } 620 621 @Override 622 public void sendTitle(String s, String s1, TitleTransition titleTransition) { 623 sendTitle(s, s1); 624 PacketPlayOutTitle transitionPacket = new PacketPlayOutTitle(); 625 transitionPacket.set("action", PacketPlayOutTitle.TitleAction.TIMES_AND_DISPLAY.id()); 626 transitionPacket.set("values", new Object[] { titleTransition.getFadeInTime(), titleTransition.getTitleTime(), titleTransition.getFadeOutTime() }); 627 connection.sendPacket(transitionPacket); 628 } 629}