/*
* 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 org.lanternpowered.server.boss.LanternBossBar;
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.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.LanternGame;
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.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.MessagePlayOutSetReducedDebug;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutSetWindowSlot;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutStatistics;
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.statistic.achievement.IAchievement;
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.LanternWorldProperties;
import org.lanternpowered.server.world.TimeUniverse;
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.DataTransactionResult;
import org.spongepowered.api.data.DataView;
import org.spongepowered.api.data.MemoryDataContainer;
import org.spongepowered.api.data.key.Key;
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.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.cause.Cause;
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.statistic.achievement.Achievement;
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 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 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;
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);
}
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();
registerKey(Keys.LAST_DATE_PLAYED, null);
registerKey(Keys.FIRST_DATE_PLAYED, null);
registerKey(Keys.IS_FLYING, false).notRemovable();
registerKey(Keys.IS_SNEAKING, false).notRemovable();
registerKey(Keys.IS_SPRINTING, false).notRemovable();
registerKey(Keys.FLYING_SPEED, 0.1).notRemovable();
registerKey(Keys.CAN_FLY, false).notRemovable();
registerKey(Keys.RESPAWN_LOCATIONS, new HashMap<>()).notRemovable();
registerKey(Keys.GAME_MODE, GameModes.NOT_SET).notRemovable();
registerKey(Keys.DOMINANT_HAND, HandPreferences.RIGHT).notRemovable();
registerKey(LanternKeys.IS_ELYTRA_FLYING, false).notRemovable();
registerKey(LanternKeys.ELYTRA_SPEED_BOOST, false).notRemovable();
registerKey(LanternKeys.SUPER_STEVE, false).notRemovable();
registerKey(LanternKeys.SCORE, 0).notRemovable();
registerProcessorKey(Keys.STATISTICS).applyValueProcessor(builder -> builder
.offerHandler((key, valueContainer, map) -> {
this.statisticMap.setStatisticValues(map);
return DataTransactionResult.successNoData();
})
.retrieveHandler((key, valueContainer) -> Optional.of(this.statisticMap.getStatisticValues()))
.failAlwaysRemoveHandler());
}
@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 (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(LanternGame.IMPL_NAME));
// 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);
this.session.send(this.statisticMap.createAchievementsMessage(true));
} 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));
}
// Send the first chunks
pulseChunkChanges();
this.session.send(world.getProperties().createWorldBorderMessage());
world.getWeatherUniverse().ifPresent(u -> this.session.send(((LanternWeatherUniverse) u).createSkyUpdateMessage()));
this.session.send(world.getComponent(TimeUniverse.class).get().createUpdateTimeMessage());
this.session.send(new MessagePlayInOutHeldItemChange(this.inventory.getHotbar().getSelectedSlotIndex()));
setScoreboard(world.getScoreboard());
this.inventoryContainer.openInventoryForAndInitialize(this);
this.bossBars.forEach(bossBar -> bossBar.resendBossBar(this));
// Add the player to the world
world.addPlayer(this);
} else {
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("disconnect.idleTimeout"));
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();
// Update achievements
final MessagePlayOutStatistics achievementsMessage = this.statisticMap.createAchievementsMessage(false);
if (achievementsMessage != null) {
this.session.send(achievementsMessage);
}
this.resourcePackSendQueue.pulse();
if (get(LanternKeys.IS_ELYTRA_FLYING).get()) {
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) {
this.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 = new MemoryDataContainer(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;
}
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(this.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;
}
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::getItem).orElse(null) != ItemTypes.ELYTRA) {
return;
}
offer(LanternKeys.IS_ELYTRA_FLYING, true);
}
public StatisticMap getStatisticMap() {
return this.statisticMap;
}
public void triggerAchievement(Achievement achievement) {
final Optional<Achievement> parent = achievement.getParent();
if (parent.isPresent() && this.statisticMap.get(parent.get()).get() < ((IAchievement) parent.get()).getStatisticTargetValue()) {
return;
}
this.statisticMap.get(achievement).set(((IAchievement) achievement).getStatisticTargetValue());
}
}