Java tutorial
/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the Software), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.lanternpowered.server.entity.living.player; import static com.google.common.base.Preconditions.checkNotNull; import static org.lanternpowered.server.text.translation.TranslationHelper.t; import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector3d; import com.flowpowered.math.vector.Vector3i; import com.google.common.collect.Sets; import it.unimi.dsi.fastutil.ints.IntArrayList; import org.lanternpowered.server.advancement.AdvancementTree; import org.lanternpowered.server.advancement.AdvancementTrees; import org.lanternpowered.server.advancement.AdvancementsProgress; import org.lanternpowered.server.advancement.TestAdvancementTree; import org.lanternpowered.server.boss.LanternBossBar; import org.lanternpowered.server.data.ValueCollection; import org.lanternpowered.server.data.io.store.entity.PlayerStore; import org.lanternpowered.server.data.io.store.item.WrittenBookItemTypeObjectSerializer; import org.lanternpowered.server.data.key.LanternKeys; import org.lanternpowered.server.effect.AbstractViewer; import org.lanternpowered.server.effect.sound.LanternSoundType; import org.lanternpowered.server.entity.LanternHumanoid; import org.lanternpowered.server.entity.event.SpectateEntityEvent; import org.lanternpowered.server.entity.living.player.gamemode.LanternGameMode; import org.lanternpowered.server.entity.living.player.tab.GlobalTabList; import org.lanternpowered.server.entity.living.player.tab.GlobalTabListEntry; import org.lanternpowered.server.entity.living.player.tab.LanternTabList; import org.lanternpowered.server.entity.living.player.tab.LanternTabListEntry; import org.lanternpowered.server.entity.living.player.tab.LanternTabListEntryBuilder; import org.lanternpowered.server.game.Lantern; import org.lanternpowered.server.game.registry.type.block.BlockRegistryModule; import org.lanternpowered.server.inventory.LanternContainer; import org.lanternpowered.server.inventory.PlayerContainerSession; import org.lanternpowered.server.inventory.PlayerInventoryContainer; import org.lanternpowered.server.inventory.block.EnderChestInventory; import org.lanternpowered.server.inventory.block.IChestInventory; import org.lanternpowered.server.inventory.container.ChestInventoryContainer; import org.lanternpowered.server.inventory.entity.LanternPlayerInventory; import org.lanternpowered.server.item.CooldownTracker; import org.lanternpowered.server.network.NetworkSession; import org.lanternpowered.server.network.entity.NetworkIdHolder; import org.lanternpowered.server.network.objects.RawItemStack; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayInOutBrand; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayInOutHeldItemChange; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutBlockChange; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutOpenBook; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutParticleEffect; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutPlayerJoinGame; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutPlayerPositionAndLook; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutPlayerRespawn; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutSelectAdvancementTree; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutSetReducedDebug; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutSetWindowSlot; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutUnlockRecipes; import org.lanternpowered.server.permission.AbstractSubject; import org.lanternpowered.server.profile.LanternGameProfile; import org.lanternpowered.server.scoreboard.LanternScoreboard; import org.lanternpowered.server.statistic.StatisticMap; import org.lanternpowered.server.text.chat.LanternChatType; import org.lanternpowered.server.text.title.LanternTitles; import org.lanternpowered.server.world.LanternWeatherUniverse; import org.lanternpowered.server.world.LanternWorld; import org.lanternpowered.server.world.LanternWorldBorder; import org.lanternpowered.server.world.LanternWorldProperties; import org.lanternpowered.server.world.chunk.ChunkLoadingTicket; import org.lanternpowered.server.world.difficulty.LanternDifficulty; import org.lanternpowered.server.world.dimension.LanternDimensionType; import org.lanternpowered.server.world.rules.RuleTypes; import org.spongepowered.api.Sponge; import org.spongepowered.api.block.BlockState; import org.spongepowered.api.command.CommandSource; import org.spongepowered.api.data.DataContainer; import org.spongepowered.api.data.DataTransactionResult; import org.spongepowered.api.data.DataView; import org.spongepowered.api.data.key.Keys; import org.spongepowered.api.data.type.HandPreferences; import org.spongepowered.api.data.type.HandTypes; import org.spongepowered.api.data.type.SkinPart; import org.spongepowered.api.effect.particle.ParticleEffect; import org.spongepowered.api.effect.sound.SoundCategory; import org.spongepowered.api.effect.sound.SoundType; import org.spongepowered.api.entity.Entity; import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.entity.living.player.User; import org.spongepowered.api.entity.living.player.gamemode.GameModes; import org.spongepowered.api.event.SpongeEventFactory; import org.spongepowered.api.event.cause.Cause; import org.spongepowered.api.event.message.MessageChannelEvent; import org.spongepowered.api.event.message.MessageEvent; import org.spongepowered.api.event.world.ChangeWorldBorderEvent; import org.spongepowered.api.item.ItemTypes; import org.spongepowered.api.item.inventory.Container; import org.spongepowered.api.item.inventory.Inventory; import org.spongepowered.api.item.inventory.ItemStack; import org.spongepowered.api.item.inventory.entity.PlayerInventory; import org.spongepowered.api.item.inventory.equipment.EquipmentTypes; import org.spongepowered.api.profile.GameProfile; import org.spongepowered.api.resourcepack.ResourcePack; import org.spongepowered.api.scoreboard.Scoreboard; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.user.UserStorageService; import org.spongepowered.api.text.BookView; import org.spongepowered.api.text.Text; import org.spongepowered.api.text.channel.MessageChannel; import org.spongepowered.api.text.chat.ChatType; import org.spongepowered.api.text.chat.ChatTypes; import org.spongepowered.api.text.chat.ChatVisibilities; import org.spongepowered.api.text.chat.ChatVisibility; import org.spongepowered.api.text.title.Title; import org.spongepowered.api.util.AABB; import org.spongepowered.api.util.RelativePositions; import org.spongepowered.api.util.Tristate; import org.spongepowered.api.world.ChunkTicketManager; import org.spongepowered.api.world.DimensionTypes; import org.spongepowered.api.world.Location; import org.spongepowered.api.world.World; import org.spongepowered.api.world.WorldBorder; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; public class LanternPlayer extends LanternHumanoid implements AbstractSubject, Player, AbstractViewer, NetworkIdHolder { private final static AABB BOUNDING_BOX_BASE = new AABB(new Vector3d(-0.3, 0, -0.3), new Vector3d(0.3, 1.8, 0.3)); private final LanternUser user; private final LanternGameProfile gameProfile; private final NetworkSession session; private final LanternTabList tabList = new LanternTabList(this); // The statistics of this player private final StatisticMap statisticMap = new StatisticMap(); // The entity id that will be used for the client private int networkEntityId = -1; private MessageChannel messageChannel = MessageChannel.TO_ALL; // The (client) locale of the player private Locale locale = Locale.ENGLISH; // The (client) render distance of the player // When specified -1, the render distance will match the server one private int viewDistance = -1; // The chat visibility private ChatVisibility chatVisibility = ChatVisibilities.FULL; // Whether the chat colors are enabled private boolean chatColorsEnabled; private LanternScoreboard scoreboard; // Whether you should ignore this player when checking for sleeping players to reset the time private boolean sleepingIgnored; // The chunks the client knowns about private final Set<Vector2i> knownChunks = new HashSet<>(); // The interaction handler private final PlayerInteractionHandler interactionHandler; // The chunk position since the last #pulseChunkChanges call @Nullable private Vector2i lastChunkPos = null; // The loading ticket that will force the chunks to be loaded @Nullable private ChunkTicketManager.PlayerEntityLoadingTicket loadingTicket; private final ResourcePackSendQueue resourcePackSendQueue = new ResourcePackSendQueue(this); /** * The inventory of this {@link Player}. */ private final LanternPlayerInventory inventory; /** * The ender chest inventory of this {@link Player}. */ private final EnderChestInventory enderChestInventory; /** * The {@link LanternContainer} of the players inventory. */ private final PlayerInventoryContainer inventoryContainer; /** * The container session of this {@link Player}. */ private final PlayerContainerSession containerSession; /** * All the boss bars that are visible for this {@link Player}. */ private final Set<LanternBossBar> bossBars = new HashSet<>(); /** * The item cooldown tracker of this {@link Player}. */ private final CooldownTracker cooldownTracker = new PlayerCooldownTracker(this); /** * The last time that the player was active. */ private long lastActiveTime; /** * This field is for internal use only, it is used while finding a proper * world to spawn the player in. Used at {@link NetworkSession#initPlayer()} and * {@link PlayerStore}. */ @Nullable private LanternWorldProperties tempWorld; /** * The entity that is being spectated by this player. */ @Nullable private Entity spectatorEntity; // The world border the player is currently tracking, if null, it will track the // border of the world the player is located in @Nullable private LanternWorldBorder worldBorder; private final AdvancementsProgress advancementsProgress = new AdvancementsProgress(); public LanternPlayer(LanternGameProfile gameProfile, NetworkSession session) { super(checkNotNull(gameProfile, "gameProfile").getUniqueId()); this.interactionHandler = new PlayerInteractionHandler(this); this.inventory = new LanternPlayerInventory(null, null, this); this.inventoryContainer = new PlayerInventoryContainer(null, this.inventory); this.enderChestInventory = new EnderChestInventory(null); this.containerSession = new PlayerContainerSession(this); this.session = session; this.gameProfile = gameProfile; // Get or create the user object this.user = (LanternUser) Sponge.getServiceManager().provideUnchecked(UserStorageService.class) .getOrCreate(gameProfile); this.user.setPlayer(this); resetIdleTimeoutCounter(); setBoundingBoxBase(BOUNDING_BOX_BASE); offer(Keys.DISPLAY_NAME, Text.of(gameProfile.getName().get())); } public Set<LanternBossBar> getBossBars() { return this.bossBars; } @Override public int getNetworkId() { return this.networkEntityId; } /** * Sets the network entity id. * * @param entityId The network entity id */ public void setNetworkId(int entityId) { this.networkEntityId = entityId; } /** * Resets the timeout counter. */ public void resetIdleTimeoutCounter() { this.lastActiveTime = System.currentTimeMillis(); } @Override public void registerKeys() { super.registerKeys(); final ValueCollection c = getValueCollection(); c.register(LanternKeys.ACCESSORIES, new ArrayList<>()); c.register(LanternKeys.MAX_FOOD_LEVEL, 20, 0, Integer.MAX_VALUE); c.register(Keys.FOOD_LEVEL, 20, 0, LanternKeys.MAX_FOOD_LEVEL); c.register(LanternKeys.MAX_SATURATION, 40.0, 0.0, Double.MAX_VALUE); c.register(Keys.SATURATION, 40.0, 0.0, LanternKeys.MAX_SATURATION); c.register(Keys.LAST_DATE_PLAYED, null); c.register(Keys.FIRST_DATE_PLAYED, null); c.registerNonRemovable(Keys.WALKING_SPEED, 0.1); c.registerNonRemovable(LanternKeys.FIELD_OF_VIEW_MODIFIER, 1.0); c.registerNonRemovable(Keys.IS_FLYING, false); c.registerNonRemovable(Keys.IS_SNEAKING, false); c.registerNonRemovable(Keys.IS_SPRINTING, false); c.registerNonRemovable(Keys.FLYING_SPEED, 0.1); c.registerNonRemovable(Keys.CAN_FLY, false); c.registerNonRemovable(Keys.RESPAWN_LOCATIONS, new HashMap<>()); c.registerNonRemovable(Keys.GAME_MODE, GameModes.NOT_SET).addListener((oldElement, newElement) -> { ((LanternGameMode) newElement).getAbilityApplier().accept(this); // This MUST be updated, unless you want strange behavior on the client, // the client has 3 different concepts of 'isCreative', and each combination // gives a different outcome... // For example: // - Disable noClip and glow in spectator, but you can place blocks // - NoClip in creative, but you cannot change your hotbar, or drop items // Not really worth the trouble right now // TODO: Differentiate the 'global tab list entry' and the entry to update // TODO: these kind of settings to avoid possible 'strange' behavior. GlobalTabList.getInstance().get(this.gameProfile).ifPresent(e -> e.setGameMode(newElement)); }); c.registerNonRemovable(Keys.DOMINANT_HAND, HandPreferences.RIGHT); c.registerNonRemovable(LanternKeys.IS_ELYTRA_FLYING, false); c.registerNonRemovable(LanternKeys.ELYTRA_GLIDE_SPEED, 0.1); c.registerNonRemovable(LanternKeys.ELYTRA_SPEED_BOOST, false); c.registerNonRemovable(LanternKeys.SUPER_STEVE, false); c.registerNonRemovable(LanternKeys.CAN_WALL_JUMP, false); c.registerNonRemovable(LanternKeys.CAN_DUAL_WIELD, false); c.registerNonRemovable(LanternKeys.SCORE, 0); c.registerNonRemovable(LanternKeys.ACTIVE_HAND, Optional.empty()); c.registerNonRemovable(LanternKeys.RECIPE_BOOK_FILTER_ACTIVE, false); c.registerNonRemovable(LanternKeys.RECIPE_BOOK_GUI_OPEN, false); c.registerProcessor(Keys.STATISTICS).add(builder -> builder.offerHandler((key, valueContainer, map) -> { this.statisticMap.setStatisticValues(map); return DataTransactionResult.successNoData(); }).retrieveHandler((key, valueContainer) -> Optional.of(this.statisticMap.getStatisticValues())) .failAlwaysRemoveHandler()); c.registerNonRemovable(LanternKeys.OPEN_ADVANCEMENT_TREE, Optional.empty()) .addListener((oldElement, newElement) -> { //noinspection ConstantConditions if (getWorld() != null) { this.session.send(new MessagePlayOutSelectAdvancementTree( newElement.map(AdvancementTree::getInternalId).orElse(null))); } }); } @Nullable public LanternWorldProperties getTempWorld() { return this.tempWorld; } public void setTempWorld(@Nullable LanternWorldProperties tempTargetWorld) { this.tempWorld = tempTargetWorld; } @Override public String getName() { return this.gameProfile.getName().get(); } /** * Sets the {@link LanternWorld} without triggering * any changes for this player. * * @param world The world */ public void setRawWorld(@Nullable LanternWorld world) { super.setWorld(world); } @Override public void setWorld(@Nullable LanternWorld world) { final LanternWorld oldWorld = getWorld(); if (oldWorld != world) { this.interactionHandler.reset(); } super.setWorld(world); if (world == oldWorld) { return; } //noinspection ConstantConditions if (oldWorld != null) { if (this.loadingTicket != null) { this.loadingTicket.release(); this.loadingTicket = null; } // Remove the player from all the observed chunks, there is no need // to send unload messages because we will respawn in a different world final ObservedChunkManager observedChunkManager = oldWorld.getObservedChunkManager(); final Set<Vector2i> knownChunks = new HashSet<>(this.knownChunks); knownChunks.forEach(coords -> observedChunkManager.removeObserver(coords, this, false)); this.knownChunks.clear(); // Clear the last chunk pos this.lastChunkPos = null; // Remove the player from the world oldWorld.removePlayer(this); if (this.worldBorder == null) { oldWorld.getWorldBorder().removePlayer(this); } } if (world != null) { final LanternGameMode gameMode = (LanternGameMode) get(Keys.GAME_MODE).get(); final LanternDimensionType dimensionType = (LanternDimensionType) world.getDimension().getType(); final LanternDifficulty difficulty = (LanternDifficulty) world.getDifficulty(); final boolean reducedDebug = world.getOrCreateRule(RuleTypes.REDUCED_DEBUG_INFO).getValue(); final boolean lowHorizon = world.getProperties().getConfig().isLowHorizon(); // The player has joined the server //noinspection ConstantConditions if (oldWorld == null) { this.session.getServer().addPlayer(this); this.session.send( new MessagePlayOutPlayerJoinGame(gameMode, dimensionType, difficulty, this.networkEntityId, this.session.getServer().getMaxPlayers(), reducedDebug, false, lowHorizon)); // Send the server brand this.session.send(new MessagePlayInOutBrand(Lantern.getImplementationPlugin().getName())); // Send the player list final List<LanternTabListEntry> tabListEntries = new ArrayList<>(); final LanternTabListEntryBuilder thisBuilder = createTabListEntryBuilder(this); for (Player player : Sponge.getServer().getOnlinePlayers()) { final LanternTabListEntryBuilder builder = player == this ? thisBuilder : createTabListEntryBuilder((LanternPlayer) player); tabListEntries.add(builder.list(this.tabList).build()); if (player != this) { player.getTabList().addEntry(thisBuilder.list(player.getTabList()).build()); } } this.tabList.init(tabListEntries); TestAdvancementTree.A.addRawTracker(this); TestAdvancementTree.B.addRawTracker(this); AdvancementTrees.INSTANCE.initialize(this); getAdvancementsProgress().get(TestAdvancementTree.DIG_DIRT) .tryGet(TestAdvancementTree.DIG_DIRT_CRITERION).set(4); } else { //noinspection ConstantConditions if (oldWorld != null && oldWorld != world) { LanternDimensionType oldDimensionType = (LanternDimensionType) oldWorld.getDimension() .getType(); // The client only creates a new world instance on the client if a // different dimension is used, that is why we will send two respawn // messages to trick the client to do it anyway // This is also needed to avoid weird client bugs if (oldDimensionType == dimensionType) { oldDimensionType = (LanternDimensionType) (dimensionType == DimensionTypes.OVERWORLD ? DimensionTypes.NETHER : DimensionTypes.OVERWORLD); this.session.send(new MessagePlayOutPlayerRespawn(gameMode, oldDimensionType, difficulty, lowHorizon)); } } // Send a respawn message this.session.send(new MessagePlayOutPlayerRespawn(gameMode, dimensionType, difficulty, lowHorizon)); this.session.send(new MessagePlayOutSetReducedDebug(reducedDebug)); } if (this.worldBorder == null) { world.getWorldBorder().addPlayer(this); } // Send the first chunks pulseChunkChanges(); world.getWeatherUniverse() .ifPresent(u -> this.session.send(((LanternWeatherUniverse) u).createSkyUpdateMessage())); this.session.send(world.getTimeUniverse().createUpdateTimeMessage()); this.session .send(new MessagePlayInOutHeldItemChange(this.inventory.getHotbar().getSelectedSlotIndex())); this.session.send(new MessagePlayOutSelectAdvancementTree( get(LanternKeys.OPEN_ADVANCEMENT_TREE).get().map(AdvancementTree::getInternalId).orElse(null))); setScoreboard(world.getScoreboard()); this.inventoryContainer.openInventoryForAndInitialize(this); this.bossBars.forEach(bossBar -> bossBar.resendBossBar(this)); // Add the player to the world world.addPlayer(this); // TODO: Unlock all the recipes for now, mappings between the internal ids and // TODO: the readable ids still has to be made final int[] recipes = new int[435]; for (int i = 0; i < recipes.length; i++) { recipes[i] = i; } /* this.session.send(new MessagePlayOutUnlockRecipes.Init( get(LanternKeys.RECIPE_BOOK_GUI_OPEN).get(), get(LanternKeys.RECIPE_BOOK_FILTER_ACTIVE).get(), new IntArrayList(recipes), new IntArrayList(recipes))); */ this.session.send(new MessagePlayOutUnlockRecipes.Add(get(LanternKeys.RECIPE_BOOK_GUI_OPEN).get(), get(LanternKeys.RECIPE_BOOK_FILTER_ACTIVE).get(), new IntArrayList(recipes))); } else { if (this.worldBorder != null) { this.worldBorder.removePlayer(this); } AdvancementTrees.INSTANCE.removeTracker(this); this.session.getServer().removePlayer(this); this.bossBars.forEach(bossBar -> bossBar.removeRawPlayer(this)); this.tabList.clear(); // Remove this player from the global tab list GlobalTabList.getInstance().get(this.gameProfile).ifPresent(GlobalTabListEntry::removeEntry); } } private static LanternTabListEntryBuilder createTabListEntryBuilder(LanternPlayer player) { return new LanternTabListEntryBuilder().profile(player.getProfile()).displayName(Text.of(player.getName())) // TODO .gameMode(player.get(Keys.GAME_MODE).get()).latency(player.getConnection().getLatency()); } private static final Set<RelativePositions> RELATIVE_ROTATION = Sets.immutableEnumSet(RelativePositions.PITCH, RelativePositions.YAW); private static final Set<RelativePositions> RELATIVE_POSITION = Sets.immutableEnumSet(RelativePositions.X, RelativePositions.Y, RelativePositions.Z); @Override public boolean setPositionAndWorld(World world, Vector3d position) { final LanternWorld oldWorld = this.getWorld(); final boolean success = super.setPositionAndWorld(world, position); if (success && world == oldWorld) { this.session.send(new MessagePlayOutPlayerPositionAndLook(position.getX(), position.getY(), position.getZ(), 0, 0, RELATIVE_ROTATION, 0)); } return success; } @Override public void setPosition(Vector3d position) { super.setPosition(position); final LanternWorld world = getWorld(); //noinspection ConstantConditions if (world != null) { this.session.send(new MessagePlayOutPlayerPositionAndLook(position.getX(), position.getY(), position.getZ(), 0, 0, RELATIVE_ROTATION, 0)); } } @Override public void setRotation(Vector3d rotation) { super.setRotation(rotation); final LanternWorld world = getWorld(); //noinspection ConstantConditions if (world != null) { this.session.send(new MessagePlayOutPlayerPositionAndLook(0, 0, 0, (float) rotation.getX(), (float) rotation.getY(), RELATIVE_POSITION, 0)); } } @Override public void setHeadRotation(Vector3d rotation) { setRotation(rotation); } @Override public Vector3d getHeadRotation() { return super.getRotation(); } @Override public void setRawRotation(Vector3d rotation) { super.setRawRotation(rotation); } @Override protected void setRawHeadRotation(Vector3d rotation) { super.setRawRotation(rotation); } @Override public boolean setLocationAndRotation(Location<World> location, Vector3d rotation) { final World oldWorld = getWorld(); final boolean success = super.setLocationAndRotation(location, rotation); if (success) { final World world = location.getExtent(); // Only send this if the world isn't changed, otherwise will the position be resend anyway if (oldWorld == world) { final Vector3d pos = location.getPosition(); final MessagePlayOutPlayerPositionAndLook message = new MessagePlayOutPlayerPositionAndLook( pos.getX(), pos.getY(), pos.getZ(), (float) rotation.getX(), (float) rotation.getY(), Collections.emptySet(), 0); this.session.send(message); } } return success; } @Override public boolean setLocationAndRotation(Location<World> location, Vector3d rotation, EnumSet<RelativePositions> relativePositions) { final World oldWorld = getWorld(); final boolean success = super.setLocationAndRotation(location, rotation, relativePositions); if (success) { final World world = location.getExtent(); // Only send this if the world isn't changed, otherwise will the position be resend anyway if (oldWorld == world) { final Vector3d pos = location.getPosition(); final MessagePlayOutPlayerPositionAndLook message = new MessagePlayOutPlayerPositionAndLook( pos.getX(), pos.getY(), pos.getZ(), (float) rotation.getX(), (float) rotation.getY(), Sets.immutableEnumSet(relativePositions), 0); this.session.send(message); } } return success; } @Override public void setRawPosition(Vector3d position) { super.setRawPosition(position); } @Override public void pulse() { // Check whether the player is still active int timeout = Lantern.getGame().getGlobalConfig().getPlayerIdleTimeout(); if (timeout > 0 && System.currentTimeMillis() - this.lastActiveTime >= timeout * 60000) { this.session.disconnect(t("multiplayer.disconnect.idling")); return; } super.pulse(); // TODO: Maybe async? pulseChunkChanges(); // Pulse the interaction handler this.interactionHandler.pulse(); // Stream the inventory updates final LanternContainer container = this.containerSession.getOpenContainer(); (container == null ? this.inventoryContainer : container).streamSlotChanges(); this.resourcePackSendQueue.pulse(); if (get(LanternKeys.IS_ELYTRA_FLYING).get()) { if (get(Keys.IS_SNEAKING).get()) { offer(LanternKeys.IS_ELYTRA_FLYING, false); offer(LanternKeys.ELYTRA_SPEED_BOOST, false); } else { offer(LanternKeys.ELYTRA_SPEED_BOOST, get(Keys.IS_SPRINTING).get()); } } } /** * Gets the {@link ChunkLoadingTicket} that should be used * for this player. * * @return the chunk loading ticket */ public ChunkLoadingTicket getChunkLoadingTicket() { // Allocate a new loading ticket, this can be null after // joining the server or switching worlds if (this.loadingTicket == null || ((ChunkLoadingTicket) this.loadingTicket).isReleased()) { this.loadingTicket = this.getWorld().getChunkManager() .createPlayerEntityTicket(Lantern.getMinecraftPlugin(), this.gameProfile.getUniqueId()).get(); this.loadingTicket.bindToEntity(this); } return (ChunkLoadingTicket) this.loadingTicket; } public void pulseChunkChanges() { final LanternWorld world = getWorld(); //noinspection ConstantConditions if (world == null) { return; } ChunkLoadingTicket loadingTicket = this.getChunkLoadingTicket(); Vector3d position = this.getPosition(); double xPos = position.getX(); double zPos = position.getZ(); int centralX = ((int) xPos) >> 4; int centralZ = ((int) zPos) >> 4; // Fail fast if the player hasn't moved a chunk if (this.lastChunkPos != null && this.lastChunkPos.getX() == centralX && this.lastChunkPos.getY() == centralZ) { return; } this.lastChunkPos = new Vector2i(centralX, centralZ); // Get the radius of visible chunks int radius = Math.min(world.getProperties().getConfig().getGeneration().getViewDistance(), this.viewDistance == -1 ? Integer.MAX_VALUE : this.viewDistance + 1); final Set<Vector2i> previousChunks = new HashSet<>(this.knownChunks); final List<Vector2i> newChunks = new ArrayList<>(); for (int x = (centralX - radius); x <= (centralX + radius); x++) { for (int z = (centralZ - radius); z <= (centralZ + radius); z++) { final Vector2i coords = new Vector2i(x, z); if (!previousChunks.remove(coords)) { newChunks.add(coords); } } } // Early end if there's no changes if (newChunks.size() == 0 && previousChunks.size() == 0) { return; } // Sort chunks by distance from player - closer chunks sent/forced first Collections.sort(newChunks, (a, b) -> { double dx = 16 * a.getX() + 8 - xPos; double dz = 16 * a.getY() + 8 - zPos; double da = dx * dx + dz * dz; dx = 16 * b.getX() + 8 - xPos; dz = 16 * b.getY() + 8 - zPos; double db = dx * dx + dz * dz; return Double.compare(da, db); }); ObservedChunkManager observedChunkManager = world.getObservedChunkManager(); // Force all the new chunks to be loaded and track the changes newChunks.forEach(coords -> { observedChunkManager.addObserver(coords, this); loadingTicket.forceChunk(coords); }); // Unforce old chunks so they can unload and untrack the chunk previousChunks.forEach(coords -> { observedChunkManager.removeObserver(coords, this, true); loadingTicket.unforceChunk(coords); }); this.knownChunks.removeAll(previousChunks); this.knownChunks.addAll(newChunks); } public User getUserObject() { return this.user; } @Override public void setInternalSubject(@Nullable Subject subject) { // We don't have to set the internal subject in the player instance // because it's already set in the user } @Override public Subject getInternalSubject() { return this.user.getInternalSubject(); } @Override public String getSubjectCollectionIdentifier() { return this.user.getSubjectCollectionIdentifier(); } @Override public Tristate getPermissionDefault(String permission) { return this.user.getPermissionDefault(permission); } @Override public boolean isOnline() { return this.session.getChannel().isActive(); } @Override public Optional<Player> getPlayer() { return Optional.of(this); } @Override public Optional<CommandSource> getCommandSource() { return Optional.of(this); } @Override public GameProfile getProfile() { return this.gameProfile; } @Override public String getIdentifier() { return this.getUniqueId().toString(); } @Override public void sendMessage(ChatType type, Text message) { checkNotNull(message, "message"); checkNotNull(type, "type"); if (this.chatVisibility.isVisible(type)) { this.session.send(((LanternChatType) type).getMessageProvider().apply(message, this.locale)); } } @Override public void sendMessage(Text message) { sendMessage(ChatTypes.CHAT, message); } @Override public MessageChannel getMessageChannel() { return this.messageChannel; } @Override public void setMessageChannel(MessageChannel channel) { this.messageChannel = checkNotNull(channel, "channel"); } @Override public void spawnParticles(ParticleEffect particleEffect, Vector3d position) { this.session.send(new MessagePlayOutParticleEffect(checkNotNull(position, "position"), checkNotNull(particleEffect, "particleEffect"))); } @Override public void spawnParticles(ParticleEffect particleEffect, Vector3d position, int radius) { checkNotNull(position, "position"); checkNotNull(particleEffect, "particleEffect"); if (getPosition().distanceSquared(position) < radius * radius) { spawnParticles(particleEffect, position); } } @Override public void playSound(SoundType sound, SoundCategory category, Vector3d position, double volume, double pitch, double minVolume) { checkNotNull(sound, "sound"); checkNotNull(position, "position"); checkNotNull(category, "category"); this.session.send(((LanternSoundType) sound).createMessage(position, category, (float) Math.max(minVolume, volume), (float) pitch)); } @Override public void sendTitle(Title title) { this.session.send(LanternTitles.getMessages(checkNotNull(title, "title"))); } @Override public void sendBookView(BookView bookView) { checkNotNull(bookView, "bookView"); final DataView dataView = DataContainer.createNew(DataView.SafetyMode.NO_DATA_CLONED); WrittenBookItemTypeObjectSerializer.writeBookData(dataView, bookView, this.locale); // Written book internal id final RawItemStack rawItemStack = new RawItemStack(387, 0, 1, dataView); final int slot = this.inventory.getHotbar().getSelectedSlotIndex(); this.session.send(new MessagePlayOutSetWindowSlot(-2, slot, rawItemStack)); this.session.send(new MessagePlayOutOpenBook(HandTypes.MAIN_HAND)); this.session.send(new MessagePlayOutSetWindowSlot(-2, slot, this.inventory.getHotbar().getSelectedSlot().peek().orElse(null))); } @Override public void sendBlockChange(Vector3i position, BlockState state) { checkNotNull(state, "state"); checkNotNull(position, "position"); this.session.send(new MessagePlayOutBlockChange(position, BlockRegistryModule.get().getStateInternalIdAndData(state))); } @Override public void sendBlockChange(int x, int y, int z, BlockState state) { this.sendBlockChange(new Vector3i(x, y, z), state); } @Override public void resetBlockChange(Vector3i position) { checkNotNull(position, "position"); LanternWorld world = this.getWorld(); if (world == null) { return; } this.session.send(new MessagePlayOutBlockChange(position, BlockRegistryModule.get().getStateInternalIdAndData(world.getBlock(position)))); } @Override public void resetBlockChange(int x, int y, int z) { this.resetBlockChange(new Vector3i(x, y, z)); } @Override public Locale getLocale() { return this.locale; } public void setLocale(Locale locale) { this.locale = checkNotNull(locale, "locale"); } @Override public boolean isViewingInventory() { return false; } @Override public Optional<Container> getOpenInventory() { return Optional.ofNullable(this.containerSession.getOpenContainer()); } @Override public Optional<Container> openInventory(Inventory inventory, Cause cause) { checkNotNull(inventory, "inventory"); checkNotNull(cause, "cause"); // TODO: Make this better LanternContainer container; if (inventory instanceof IChestInventory) { container = new ChestInventoryContainer(this.inventory, (IChestInventory) inventory); } else if (inventory instanceof PlayerInventory) { return Optional.empty(); } else { throw new UnsupportedOperationException("Unsupported inventory type: " + inventory); } if (this.containerSession.setOpenContainer(container, cause)) { return Optional.of(container); } return Optional.empty(); } @Override public boolean closeInventory(Cause cause) { checkNotNull(cause, "cause"); return this.containerSession.setOpenContainer(null, cause); } @Override public int getViewDistance() { return this.viewDistance; } public void setViewDistance(int viewDistance) { this.viewDistance = viewDistance; } @Override public ChatVisibility getChatVisibility() { return this.chatVisibility; } public void setChatVisibility(ChatVisibility chatVisibility) { this.chatVisibility = checkNotNull(chatVisibility, "chatVisibility"); } @Override public boolean isChatColorsEnabled() { return this.chatColorsEnabled; } @Override public MessageChannelEvent.Chat simulateChat(Text message, Cause cause) { checkNotNull(message, "message"); checkNotNull(cause, "cause"); final Text nameText = get(Keys.DISPLAY_NAME).get(); final MessageChannel channel = getMessageChannel(); final MessageChannelEvent.Chat event = SpongeEventFactory.createMessageChannelEventChat(cause, channel, Optional.of(channel), new MessageEvent.MessageFormatter(nameText, message), message, false); if (!Sponge.getEventManager().post(event) && !event.isMessageCancelled()) { event.getChannel().ifPresent(c -> c.send(this, event.getMessage(), ChatTypes.CHAT)); } return event; } public void setChatColorsEnabled(boolean enabled) { this.chatColorsEnabled = enabled; } @Override public Set<SkinPart> getDisplayedSkinParts() { return this.get(LanternKeys.DISPLAYED_SKIN_PARTS).get(); } @Override public NetworkSession getConnection() { return this.session; } public ResourcePackSendQueue getResourcePackSendQueue() { return this.resourcePackSendQueue; } @Override public void sendResourcePack(ResourcePack resourcePack) { this.resourcePackSendQueue.offer(resourcePack); } @Override public LanternTabList getTabList() { return this.tabList; } @Override public void kick() { this.session.disconnect(); } @Override public void kick(Text reason) { this.session.disconnect(reason); } @Override public Scoreboard getScoreboard() { return this.scoreboard; } @Override public void setScoreboard(Scoreboard scoreboard) { checkNotNull(scoreboard, "scoreboard"); //noinspection ConstantConditions if (this.scoreboard != null && scoreboard != this.scoreboard) { this.scoreboard.removePlayer(this); } this.scoreboard = (LanternScoreboard) scoreboard; this.scoreboard.addPlayer(this); } @Override public Text getTeamRepresentation() { return Text.of(getName()); } @Override public boolean isSleepingIgnored() { return this.sleepingIgnored; } @Override public void setSleepingIgnored(boolean sleepingIgnored) { this.sleepingIgnored = sleepingIgnored; } @Override public EnderChestInventory getEnderChestInventory() { return this.enderChestInventory; } @Override public boolean respawnPlayer() { return false; } @Override public Optional<Entity> getSpectatorTarget() { return Optional.ofNullable(this.spectatorEntity); } @Override public void setSpectatorTarget(@Nullable Entity entity) { this.spectatorEntity = entity; triggerEvent(new SpectateEntityEvent(entity)); } @Override public Optional<WorldBorder> getWorldBorder() { return Optional.ofNullable(this.worldBorder); } @Override public void setWorldBorder(@Nullable WorldBorder border, Cause cause) { checkNotNull(cause, "cause"); if (this.worldBorder == border) { return; } final ChangeWorldBorderEvent.TargetPlayer event = SpongeEventFactory .createChangeWorldBorderEventTargetPlayer(cause, Optional.ofNullable(border), Optional.ofNullable(this.worldBorder), this); Sponge.getEventManager().post(event); if (event.isCancelled()) { return; } if (this.worldBorder != null) { this.worldBorder.removePlayer(this); } final LanternWorldBorder worldBorder = (LanternWorldBorder) border; if (worldBorder != null) { if (this.worldBorder == null) { getWorld().getWorldBorder().removePlayer(this); } worldBorder.addPlayer(this); } else { getWorld().getWorldBorder().addPlayer(this); } this.worldBorder = worldBorder; } public PlayerInteractionHandler getInteractionHandler() { return this.interactionHandler; } @Override public LanternPlayerInventory getInventory() { return this.inventory; } /** * Gets the {@link PlayerContainerSession}. * * @return The container session */ public PlayerContainerSession getContainerSession() { return this.containerSession; } public PlayerInventoryContainer getInventoryContainer() { return this.inventoryContainer; } public void handleOnGroundState(boolean state) { setOnGround(state); if (state) { offer(LanternKeys.IS_ELYTRA_FLYING, false); } } public void handleStartElytraFlying() { // Check for the elytra item if (getInventory().getEquipment().getSlot(EquipmentTypes.CHESTPLATE).get().peek().map(ItemStack::getType) .orElse(null) != ItemTypes.ELYTRA) { return; } offer(LanternKeys.IS_ELYTRA_FLYING, true); } public StatisticMap getStatisticMap() { return this.statisticMap; } public CooldownTracker getCooldownTracker() { return this.cooldownTracker; } public AdvancementsProgress getAdvancementsProgress() { return this.advancementsProgress; } }