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}