/*
* 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.data.io.store.entity;
import static org.lanternpowered.server.data.util.DataUtil.getOrCreateView;
import com.flowpowered.math.vector.Vector3d;
import org.lanternpowered.server.data.io.store.ObjectSerializer;
import org.lanternpowered.server.data.io.store.ObjectStore;
import org.lanternpowered.server.data.io.store.ObjectStoreRegistry;
import org.lanternpowered.server.data.io.store.SimpleValueContainer;
import org.lanternpowered.server.data.key.LanternKeys;
import org.lanternpowered.server.data.util.DataQueries;
import org.lanternpowered.server.entity.living.player.LanternPlayer;
import org.lanternpowered.server.entity.living.player.gamemode.LanternGameMode;
import org.lanternpowered.server.game.Lantern;
import org.lanternpowered.server.game.registry.type.entity.player.GameModeRegistryModule;
import org.lanternpowered.server.inventory.LanternEquipmentInventory;
import org.lanternpowered.server.inventory.LanternItemStack;
import org.lanternpowered.server.inventory.entity.HumanMainInventory;
import org.lanternpowered.server.inventory.entity.LanternPlayerInventory;
import org.lanternpowered.server.inventory.entity.OffHandSlot;
import org.lanternpowered.server.world.LanternWorld;
import org.lanternpowered.server.world.LanternWorldProperties;
import org.spongepowered.api.data.DataQuery;
import org.spongepowered.api.data.DataView;
import org.spongepowered.api.data.MemoryDataContainer;
import org.spongepowered.api.data.Queries;
import org.spongepowered.api.data.key.Keys;
import org.spongepowered.api.entity.living.player.gamemode.GameMode;
import org.spongepowered.api.entity.living.player.gamemode.GameModes;
import org.spongepowered.api.item.inventory.Inventory;
import org.spongepowered.api.item.inventory.ItemStack;
import org.spongepowered.api.item.inventory.Slot;
import org.spongepowered.api.item.inventory.property.SlotIndex;
import org.spongepowered.api.item.inventory.type.GridInventory;
import org.spongepowered.api.util.RespawnLocation;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
* Note: This store assumes that all the Sponge data (world/data/sponge) is merged
* into one {@link DataView}. This listed under a sub view with the data query
* {@link DataQueries#SPONGE_DATA}.
*/
public class PlayerStore extends LivingStore<LanternPlayer> {
private static final DataQuery ABILITIES = DataQuery.of("abilities");
private static final DataQuery FLYING = DataQuery.of("flying");
private static final DataQuery FLYING_SPEED = DataQuery.of("flySpeed");
private static final DataQuery CAN_FLY = DataQuery.of("mayfly");
private static final DataQuery SCORE = DataQuery.of("Score");
private static final DataQuery GAME_MODE = DataQuery.of("playerGameType");
private static final DataQuery SELECTED_ITEM_SLOT = DataQuery.of("SelectedItemSlot");
private static final DataQuery DIMENSION = DataQuery.of("Dimension");
private static final DataQuery BUKKIT_FIRST_DATE_PLAYED = DataQuery.of('.', "bukkit.firstPlayed");
private static final DataQuery BUKKIT_LAST_DATE_PLAYED = DataQuery.of('.', "bukkit.lastPlayed");
private static final DataQuery FIRST_DATE_PLAYED = DataQuery.of("FirstJoin");
private static final DataQuery LAST_DATE_PLAYED = DataQuery.of("LastPlayed");
private static final DataQuery RESPAWN_LOCATIONS = DataQuery.of("Spawns");
private static final DataQuery RESPAWN_LOCATIONS_DIMENSION = DataQuery.of("Dim");
private static final DataQuery RESPAWN_LOCATIONS_X = DataQuery.of("SpawnX");
private static final DataQuery RESPAWN_LOCATIONS_Y = DataQuery.of("SpawnY");
private static final DataQuery RESPAWN_LOCATIONS_Z = DataQuery.of("SpawnZ");
private static final DataQuery RESPAWN_LOCATIONS_FORCED = DataQuery.of("SpawnForced");
private static final DataQuery SLOT = DataQuery.of("Slot");
private static final DataQuery INVENTORY = DataQuery.of("Inventory");
private static final DataQuery ENDER_CHEST_INVENTORY = DataQuery.of("EnderItems");
@Override
public void deserialize(LanternPlayer player, DataView dataView) {
super.deserialize(player, dataView);
final int dimension = dataView.getInt(DIMENSION).orElse(0);
Lantern.getWorldManager().getWorldProperties(dimension).ifPresent(worldProperties -> {
final LanternWorldProperties worldProperties0 = (LanternWorldProperties) worldProperties;
final Optional<LanternWorld> optWorld = worldProperties0.getWorld();
if (optWorld.isPresent()) {
player.setRawWorld(optWorld.get());
} else {
player.setTempWorld(worldProperties0);
}
});
}
@Override
public void serialize(LanternPlayer entity, DataView dataView) {
super.serialize(entity, dataView);
dataView.remove(HEAD_ROTATION);
dataView.set(DIMENSION, Lantern.getWorldManager().getWorldDimensionId(entity.getWorld().getUniqueId()).orElse(0));
}
@Override
public void serializeValues(LanternPlayer player, SimpleValueContainer valueContainer, DataView dataView) {
valueContainer.remove(Keys.IS_SPRINTING);
valueContainer.remove(Keys.IS_SNEAKING);
final DataView abilities = dataView.createView(ABILITIES);
abilities.set(FLYING, (byte) (valueContainer.remove(Keys.IS_FLYING).orElse(false) ? 1 : 0));
abilities.set(FLYING_SPEED, valueContainer.remove(Keys.FLYING_SPEED).orElse(0.1).floatValue());
abilities.set(CAN_FLY, (byte) (valueContainer.remove(Keys.CAN_FLY).orElse(false) ? 1 : 0));
final DataView spongeData = getOrCreateView(dataView, DataQueries.EXTENDED_SPONGE_DATA);
spongeData.set(FIRST_DATE_PLAYED, valueContainer.remove(Keys.FIRST_DATE_PLAYED).orElse(Instant.now()).toEpochMilli());
spongeData.set(LAST_DATE_PLAYED, valueContainer.remove(Keys.LAST_DATE_PLAYED).orElse(Instant.now()).toEpochMilli());
spongeData.set(UNIQUE_ID, player.getUniqueId().toString());
spongeData.set(Queries.CONTENT_VERSION, 1);
final Map<UUID, RespawnLocation> respawnLocations = valueContainer.remove(Keys.RESPAWN_LOCATIONS).get();
final List<DataView> respawnLocationViews = new ArrayList<>();
for (RespawnLocation respawnLocation : respawnLocations.values()) {
Lantern.getWorldManager().getWorldDimensionId(respawnLocation.getWorldUniqueId()).ifPresent(dimensionId -> {
// Overworld respawn location is saved in the root container
if (dimensionId == 0) {
serializeRespawnLocationTo(dataView, respawnLocation);
} else {
respawnLocationViews.add(serializeRespawnLocationTo(new MemoryDataContainer(DataView.SafetyMode.NO_DATA_CLONED), respawnLocation)
.set(RESPAWN_LOCATIONS_DIMENSION, dimensionId));
}
});
}
dataView.set(RESPAWN_LOCATIONS, respawnLocationViews);
dataView.set(GAME_MODE, ((LanternGameMode) valueContainer.remove(Keys.GAME_MODE).orElse(GameModes.NOT_SET)).getInternalId());
dataView.set(SELECTED_ITEM_SLOT, player.getInventory().getHotbar().getSelectedSlotIndex());
dataView.set(SCORE, valueContainer.remove(LanternKeys.SCORE).get());
// Serialize the player inventory
dataView.set(INVENTORY, serializePlayerInventory(player.getInventory()));
// Serialize the ender chest inventory
dataView.set(ENDER_CHEST_INVENTORY, serializeEnderChest(player.getEnderChestInventory()));
super.serializeValues(player, valueContainer, dataView);
}
private static DataView serializeRespawnLocationTo(DataView dataView, RespawnLocation respawnLocation) {
final Vector3d position = respawnLocation.getPosition();
return dataView
.set(RESPAWN_LOCATIONS_X, position.getX())
.set(RESPAWN_LOCATIONS_Y, position.getY())
.set(RESPAWN_LOCATIONS_Z, position.getZ())
.set(RESPAWN_LOCATIONS_FORCED, respawnLocation.isForced());
}
@Override
public void deserializeValues(LanternPlayer player, SimpleValueContainer valueContainer, DataView dataView) {
// Try to convert old bukkit values first
dataView.getLong(BUKKIT_FIRST_DATE_PLAYED).ifPresent(v -> valueContainer.set(Keys.FIRST_DATE_PLAYED, Instant.ofEpochMilli(v)));
dataView.getLong(BUKKIT_LAST_DATE_PLAYED).ifPresent(v -> valueContainer.set(Keys.LAST_DATE_PLAYED, Instant.ofEpochMilli(v)));
// Deserialize sponge data
dataView.getView(DataQueries.EXTENDED_SPONGE_DATA).ifPresent(view -> {
view.getLong(FIRST_DATE_PLAYED).ifPresent(v -> valueContainer.set(Keys.FIRST_DATE_PLAYED, Instant.ofEpochMilli(v)));
view.getLong(LAST_DATE_PLAYED).ifPresent(v -> valueContainer.set(Keys.LAST_DATE_PLAYED, Instant.ofEpochMilli(v)));
});
dataView.getView(ABILITIES).ifPresent(view -> {
view.getInt(FLYING).ifPresent(v -> valueContainer.set(Keys.IS_FLYING, v > 0));
view.getDouble(FLYING_SPEED).ifPresent(v -> valueContainer.set(Keys.FLYING_SPEED, v));
view.getInt(CAN_FLY).ifPresent(v -> valueContainer.set(Keys.CAN_FLY, v > 0));
});
final Map<UUID, RespawnLocation> respawnLocations = new HashMap<>();
// Overworld respawn location is saved in the root container
final Optional<Double> optSpawnX = dataView.getDouble(RESPAWN_LOCATIONS_X);
final Optional<Double> optSpawnY = dataView.getDouble(RESPAWN_LOCATIONS_Y);
final Optional<Double> optSpawnZ = dataView.getDouble(RESPAWN_LOCATIONS_Z);
if (optSpawnX.isPresent() && optSpawnY.isPresent() && optSpawnZ.isPresent()) {
UUID uniqueId = Lantern.getWorldManager().getWorldProperties(0).get().getUniqueId();
respawnLocations.put(uniqueId, deserializeRespawnLocation(dataView, uniqueId, optSpawnX.get(),
optSpawnY.get(), optSpawnZ.get()));
}
dataView.getViewList(RESPAWN_LOCATIONS).ifPresent(v -> v.forEach(view -> {
int dimensionId = view.getInt(RESPAWN_LOCATIONS_DIMENSION).get();
Lantern.getWorldManager().getWorldProperties(dimensionId).ifPresent(props -> {
UUID uniqueId = props.getUniqueId();
double x = view.getDouble(RESPAWN_LOCATIONS_X).get();
double y = view.getDouble(RESPAWN_LOCATIONS_Y).get();
double z = view.getDouble(RESPAWN_LOCATIONS_Z).get();
respawnLocations.put(uniqueId, deserializeRespawnLocation(view, uniqueId, x, y, z));
});
}));
valueContainer.set(Keys.RESPAWN_LOCATIONS, respawnLocations);
dataView.getInt(SCORE).ifPresent(v -> valueContainer.set(LanternKeys.SCORE, v));
final GameMode gameMode = dataView.getInt(GAME_MODE)
.flatMap(v -> GameModeRegistryModule.get().getByInternalId(v)).orElse(GameModes.NOT_SET);
valueContainer.set(Keys.GAME_MODE, gameMode);
player.getInventory().getHotbar().setRawSelectedSlotIndex(dataView.getInt(SELECTED_ITEM_SLOT).orElse(0));
// Deserialize the player inventory
dataView.getViewList(INVENTORY).ifPresent(views -> deserializePlayerInventory(player.getInventory(), views));
// Deserialize the ender chest inventory
dataView.getViewList(ENDER_CHEST_INVENTORY).ifPresent(views -> deserializeEnderChest(player.getEnderChestInventory(), views));
super.deserializeValues(player, valueContainer, dataView);
}
private static RespawnLocation deserializeRespawnLocation(DataView dataView, UUID worldUUID, double x, double y, double z) {
boolean forced = dataView.getInt(RESPAWN_LOCATIONS_FORCED).orElse(0) > 0;
return RespawnLocation.builder()
.world(worldUUID)
.position(new Vector3d(x, y, z))
.forceSpawn(forced)
.build();
}
private static List<DataView> serializeEnderChest(GridInventory enderChestInventory) {
final ObjectStore<LanternItemStack> itemStackStore = ObjectStoreRegistry.get().get(LanternItemStack.class).get();
//noinspection unchecked
final ObjectSerializer<LanternItemStack> itemStackSerializer = (ObjectSerializer<LanternItemStack>) itemStackStore;
final List<DataView> itemViews = new ArrayList<>();
final Iterable<Slot> slots = enderChestInventory.slots();
for (Slot slot : slots) {
final Optional<ItemStack> optItemStack = slot.peek();
if (!optItemStack.isPresent()) {
continue;
}
final DataView itemView = itemStackSerializer.serialize((LanternItemStack) optItemStack.get());
//noinspection ConstantConditions
itemView.set(SLOT, (byte) enderChestInventory.getProperty(slot, SlotIndex.class, null).get().getValue().intValue());
itemViews.add(itemView);
}
return itemViews;
}
private static void deserializeEnderChest(GridInventory enderChestInventory, List<DataView> itemViews) {
final ObjectStore<LanternItemStack> itemStackStore = ObjectStoreRegistry.get().get(LanternItemStack.class).get();
//noinspection unchecked
final ObjectSerializer<LanternItemStack> itemStackSerializer = (ObjectSerializer<LanternItemStack>) itemStackStore;
for (DataView itemView : itemViews) {
final int slot = itemView.getByte(SLOT).get() & 0xff;
final LanternItemStack itemStack = itemStackSerializer.deserialize(itemView);
enderChestInventory.set(new SlotIndex(slot), itemStack);
}
}
private static void deserializePlayerInventory(LanternPlayerInventory inventory, List<DataView> itemViews) {
final ObjectStore<LanternItemStack> itemStackStore = ObjectStoreRegistry.get().get(LanternItemStack.class).get();
//noinspection unchecked
final ObjectSerializer<LanternItemStack> itemStackSerializer = (ObjectSerializer<LanternItemStack>) itemStackStore;
final HumanMainInventory mainInventory = inventory.getMain();
final LanternEquipmentInventory equipmentInventory = inventory.getEquipment();
final OffHandSlot offHandSlot = inventory.getOffhand();
for (DataView itemView : itemViews) {
final int slot = itemView.getByte(SLOT).get() & 0xff;
final LanternItemStack itemStack = itemStackSerializer.deserialize(itemView);
if (slot >= 0 && slot < mainInventory.slotCount()) {
mainInventory.set(new SlotIndex(slot), itemStack);
} else if (slot >= 100 && slot - 100 < equipmentInventory.slotCount()) {
equipmentInventory.set(new SlotIndex(slot - 100), itemStack);
} else if (slot == 150) {
offHandSlot.set(itemStack);
}
}
}
private static List<DataView> serializePlayerInventory(LanternPlayerInventory inventory) {
final List<DataView> itemViews = new ArrayList<>();
final ObjectStore<LanternItemStack> itemStackStore = ObjectStoreRegistry.get().get(LanternItemStack.class).get();
//noinspection unchecked
final ObjectSerializer<LanternItemStack> itemStackSerializer = (ObjectSerializer<LanternItemStack>) itemStackStore;
final HumanMainInventory mainInventory = inventory.getMain();
final LanternEquipmentInventory equipmentInventory = inventory.getEquipment();
final OffHandSlot offHandSlot = inventory.getOffhand();
Iterable<Slot> slots = mainInventory.slots();
for (Slot slot : slots) {
serializeSlot(mainInventory, slot, 0, itemStackSerializer, itemViews);
}
slots = equipmentInventory.slots();
for (Slot slot : slots) {
serializeSlot(equipmentInventory, slot, 100, itemStackSerializer, itemViews);
}
serializeSlot(150, offHandSlot, itemStackSerializer, itemViews);
return itemViews;
}
private static void serializeSlot(Inventory parent, Slot slot, int indexOffset,
ObjectSerializer<LanternItemStack> itemStackSerializer, List<DataView> views) {
final SlotIndex index = parent.getProperty(slot, SlotIndex.class, "index").get(); // Key doesn't matter
//noinspection ConstantConditions
serializeSlot(index.getValue() + indexOffset, slot, itemStackSerializer, views);
}
private static void serializeSlot(int index, Slot slot, ObjectSerializer<LanternItemStack> itemStackSerializer, List<DataView> views) {
final Optional<ItemStack> optItemStack = slot.peek();
if (!optItemStack.isPresent()) {
return;
}
final ItemStack itemStack = optItemStack.get();
final DataView itemView = itemStackSerializer.serialize((LanternItemStack) itemStack);
//noinspection ConstantConditions
itemView.set(SLOT, (byte) index);
views.add(itemView);
}
}