/*
* 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 org.lanternpowered.server.world.chunk.LanternChunk.ALL_SECTIONS_BIT_MASK;
import static org.lanternpowered.server.world.chunk.LanternChunk.CHUNK_SECTION_SIZE;
import static org.lanternpowered.server.world.chunk.LanternChunk.CHUNK_SECTION_VOLUME;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import it.unimi.dsi.fastutil.shorts.Short2ObjectMap;
import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.shorts.Short2ShortMap;
import it.unimi.dsi.fastutil.shorts.Short2ShortOpenHashMap;
import org.lanternpowered.server.block.action.BlockAction;
import org.lanternpowered.server.block.tile.LanternTileEntity;
import org.lanternpowered.server.data.io.store.ObjectSerializer;
import org.lanternpowered.server.data.io.store.ObjectSerializerRegistry;
import org.lanternpowered.server.game.registry.type.block.BlockRegistryModule;
import org.lanternpowered.server.network.message.Message;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutBlockAction;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutBlockChange;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutChunkData;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutMultiBlockChange;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutUnloadChunk;
import org.lanternpowered.server.util.VariableValueArray;
import org.lanternpowered.server.world.LanternWorld;
import org.lanternpowered.server.world.WorldEventListener;
import org.lanternpowered.server.world.chunk.LanternChunk;
import org.spongepowered.api.block.BlockState;
import org.spongepowered.api.block.BlockType;
import org.spongepowered.api.data.DataView;
import org.spongepowered.api.world.World;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public final class ObservedChunkManager implements WorldEventListener {
/**
* The {@link World} attached to the observed chunk manager.
*/
private final LanternWorld world;
/**
* All the chunks that are being observed.
*/
private final Map<Long, ObservedChunk> observedChunks = Maps.newConcurrentMap();
public ObservedChunkManager(LanternWorld world) {
this.world = world;
}
public void pulse() {
this.observedChunks.values().forEach(ObservedChunkManager.ObservedChunk::streamChanges);
}
@Override
public void onLoadChunk(LanternChunk chunk) {
final ObservedChunk observedChunk = this.observedChunks.get(chunk.getKey());
if (observedChunk != null) {
observedChunk.streamChunk(chunk);
}
}
@Override
public void onUnloadChunk(LanternChunk chunk) {
final ObservedChunk observedChunk = this.observedChunks.get(chunk.getKey());
if (observedChunk != null) {
observedChunk.streamChunk(chunk);
}
}
@Override
public void onPopulateChunk(LanternChunk chunk) {
final ObservedChunk observedChunk = this.observedChunks.get(chunk.getKey());
if (observedChunk != null) {
observedChunk.dirtyChunk = true;
}
}
@Override
public void onBlockChange(int x, int y, int z, BlockState oldBlockState, BlockState newBlockState) {
final long key = LanternChunk.key(x >> 4, z >> 4);
final ObservedChunk observedChunk = this.observedChunks.get(key);
if (observedChunk != null) {
observedChunk.addBlockChange(() -> new Vector3i(x, y, z));
if (oldBlockState.getType() != newBlockState.getType()) {
observedChunk.removeBlockAction(new Vector3i(x, y, z));
}
}
}
@Override
public void onBlockAction(int x, int y, int z, BlockType blockType, BlockAction blockAction) {
final long key = LanternChunk.key(x >> 4, z >> 4);
final ObservedChunk observedChunk = this.observedChunks.get(key);
if (observedChunk != null) {
observedChunk.addBlockAction(new Vector3i(x, y, z), blockType, blockAction);
}
}
void addObserver(Vector2i coords, LanternPlayer observer) {
final long key = LanternChunk.key(coords.getX(), coords.getY());
final ObservedChunk observedChunk = this.observedChunks.computeIfAbsent(key, key1 -> new ObservedChunk(coords));
observedChunk.addObserver(observer);
}
void removeObserver(Vector2i coords, LanternPlayer observer, boolean updateClient) {
final long key = LanternChunk.key(coords.getX(), coords.getY());
final ObservedChunk observedChunk = this.observedChunks.get(key);
if (observedChunk != null) {
observedChunk.removeObserver(observer, updateClient);
if (observedChunk.observers.isEmpty()) {
this.observedChunks.remove(key);
}
}
}
private static final VariableValueArray EMPTY_SECTION_TYPES = new VariableValueArray(4, CHUNK_SECTION_VOLUME);
private static final byte[] EMPTY_SECTION_LIGHT = new byte[CHUNK_SECTION_SIZE];
private static final byte[] EMPTY_SECTION_SKY_LIGHT = new byte[CHUNK_SECTION_SIZE];
static {
Arrays.fill(EMPTY_SECTION_SKY_LIGHT, (byte) 255);
}
private static final MessagePlayOutChunkData.Section EMPTY_SECTION_SKYLIGHT = new MessagePlayOutChunkData.Section(
EMPTY_SECTION_TYPES, new int[1], EMPTY_SECTION_LIGHT, EMPTY_SECTION_SKY_LIGHT, new Short2ObjectOpenHashMap<>());
private static final MessagePlayOutChunkData.Section EMPTY_SECTION = new MessagePlayOutChunkData.Section(
EMPTY_SECTION_TYPES, new int[1], EMPTY_SECTION_LIGHT, null, new Short2ObjectOpenHashMap<>());
private class ObservedChunk {
private final class QueuedBlockAction {
private final BlockAction blockAction;
private final MessagePlayOutBlockAction blockActionData;
private QueuedBlockAction(BlockAction blockAction, MessagePlayOutBlockAction blockActionData) {
this.blockAction = blockAction;
this.blockActionData = blockActionData;
}
}
/**
* The coordinates of this chunk.
*/
private final Vector2i coords;
/**
* All the observers of the chunk.
*/
private final Set<LanternPlayer> observers = Sets.newConcurrentHashSet();
/**
* All the observers that already know this chunk on the client.
*/
private final Set<LanternPlayer> clientObservers = Sets.newConcurrentHashSet();
/**
* All the block changes that should be send to the observers.
*/
private final Queue<Vector3i> dirtyBlocks = new ConcurrentLinkedQueue<>();
/**
* All the block events that should be send to the observers.
*/
private final Map<Vector3i, QueuedBlockAction> addedBlockActions = new ConcurrentHashMap<>();
private final Map<Vector3i, QueuedBlockAction> activeBlockActions = new ConcurrentHashMap<>();
/**
* Whether all the chunk sections are modified or whether the biomes are modified
* and the client should be updated.
*
* TODO: Add a listener to change this state for biomes
*/
private volatile boolean dirtyChunk;
ObservedChunk(Vector2i coords) {
this.coords = coords;
}
void removeBlockAction(Vector3i coords) {
this.addedBlockActions.remove(coords);
this.activeBlockActions.remove(coords);
}
void addBlockAction(Vector3i coords, BlockType blockType, BlockAction blockAction) {
final BlockAction.Type type = blockAction.type();
// Don't store single events if there are no observers
if (type == BlockAction.Type.SINGLE &&
this.observers.isEmpty() && this.clientObservers.isEmpty()) {
return;
}
// Create the message
final MessagePlayOutBlockAction blockActionData = new MessagePlayOutBlockAction(coords,
BlockRegistryModule.get().getStateInternalId(blockType.getDefaultState()));
blockAction.fill(blockActionData);
this.addedBlockActions.put(coords, new QueuedBlockAction(blockAction, blockActionData));
}
void addBlockChange(Supplier<Vector3i> coords) {
// There is not need to track the changes if no one wants to see them
// dirtyBiomes will force the chunk to be completely resend
if (!this.dirtyChunk && !this.clientObservers.isEmpty()) {
this.dirtyBlocks.add(coords.get());
}
}
void streamChanges() {
final LanternChunk chunk = world.getChunkManager().getChunkIfLoaded(this.coords);
if (chunk == null || this.clientObservers.isEmpty()) {
return;
}
if (this.dirtyChunk) {
final MessagePlayOutChunkData message = this.createLoadChunkMessage(chunk, ALL_SECTIONS_BIT_MASK, true);
this.clientObservers.forEach(player -> player.getConnection().send(message));
this.dirtyChunk = false;
this.dirtyBlocks.clear();
return;
}
if (!this.dirtyBlocks.isEmpty()) {
// All the changes per coordinate
final Set<Vector3i> changes = new HashSet<>();
// All the section which contain a block change
int dirtySections = 0;
// Get all the changes
Vector3i dirtyBlock;
while ((dirtyBlock = this.dirtyBlocks.poll()) != null) {
dirtySections |= 1 << (dirtyBlock.getY() >> 4);
changes.add(dirtyBlock);
}
final int clumpingThreshold = world.getProperties().getConfig().getChunkClumpingThreshold();
if (changes.size() >= clumpingThreshold) {
final MessagePlayOutChunkData message = this.createLoadChunkMessage(chunk, dirtySections, false);
this.clientObservers.forEach(player -> player.getConnection().send(message));
} else if (changes.size() > 1) {
final MessagePlayOutMultiBlockChange message = new MessagePlayOutMultiBlockChange(
this.coords.getX(), this.coords.getY(), changes.stream().map(coords -> {
final int x = coords.getX() & 0xf;
final int z = coords.getZ() & 0xf;
return new MessagePlayOutBlockChange(new Vector3i(x, coords.getY(), z), chunk.getType(coords));
}).collect(Collectors.toList()));
this.clientObservers.forEach(player -> player.getConnection().send(message));
} else {
dirtyBlock = changes.iterator().next();
final MessagePlayOutBlockChange message = new MessagePlayOutBlockChange(dirtyBlock, chunk.getType(dirtyBlock));
this.clientObservers.forEach(player -> player.getConnection().send(message));
}
// TODO: Also update tile entities
}
if (!this.addedBlockActions.isEmpty()) {
final Set<Message> messages = new HashSet<>();
for (Map.Entry<Vector3i, QueuedBlockAction> entry : this.addedBlockActions.entrySet()) {
final QueuedBlockAction blockAction = entry.getValue();
messages.add(blockAction.blockActionData);
if (blockAction.blockAction.type() == BlockAction.Type.CONTINUOUS) {
this.activeBlockActions.put(entry.getKey(), blockAction);
} else {
this.activeBlockActions.remove(entry.getKey());
}
}
this.addedBlockActions.clear();
this.clientObservers.forEach(player -> player.getConnection().send(messages));
}
}
private List<Message> createChunkLoadMessages(LanternChunk chunk) {
final List<Message> messages = new ArrayList<>();
messages.add(this.createLoadChunkMessage(chunk, ALL_SECTIONS_BIT_MASK, true));
if (!this.activeBlockActions.isEmpty()) {
this.activeBlockActions.values().forEach(queuedBlockAction -> messages.add(queuedBlockAction.blockActionData));
}
return messages;
}
/**
* Sends a chunk load message to all the observers
* of this chunk.
*
* @param chunk The chunk
*/
void streamChunk(LanternChunk chunk) {
List<Message> messages = null;
for (LanternPlayer observer : this.observers) {
if (this.clientObservers.add(observer)) {
if (messages == null) {
messages = this.createChunkLoadMessages(chunk);
}
observer.getConnection().send(messages);
}
}
// TODO: Also send tile entities
}
private MessagePlayOutChunkData createLoadChunkMessage(LanternChunk chunk, int sectionsBitMask, boolean biomes) {
// Whether we should send sky light
final boolean skyLight = world.getDimension().hasSky();
final LanternChunk.ChunkSectionSnapshot[] sections = chunk.getSectionSnapshots(skyLight, sectionsBitMask);
final MessagePlayOutChunkData.Section[] msgSections = new MessagePlayOutChunkData.Section[sections.length];
for (int i = 0; i < sections.length; i++) {
if (sections[i] != null) {
final LanternChunk.ChunkSectionSnapshot section = sections[i];
// The size of the palette
int paletteSize = section.typesCountMap.size();
// The amount of bits for every block state
int bitsPerValue = Integer.highestOneBit(paletteSize);
// The palette that will be send to the client
int[] palette;
// The lookup for global to local palette id
Short2ShortMap globalToLocalPalette;
// There seems to be a weird issue, some blocks are not rendered
// on the client (bedrock with the flat generator) and it cannot
// be placed in creative
if (bitsPerValue <= 8) {
// The vanilla client/server will not go lower then 4 bits
if (bitsPerValue < 4) {
bitsPerValue = 4;
}
globalToLocalPalette = new Short2ShortOpenHashMap(paletteSize);
palette = new int[paletteSize];
short currentId = 0;
for (short type : section.typesCountMap.keySet().toShortArray()) {
globalToLocalPalette.put(type, currentId);
palette[currentId] = type;
currentId++;
}
} else {
// int statesCount = Registries.getBlockRegistry().getBlockStatesCount();
// bitsPerValue = Integer.highestOneBit(statesCount);
// The value should be the amount of bits per value of
// the CLIENT palette, it will otherwise not work.
// This is sadly enough hardcoded in the client
bitsPerValue = 13;
globalToLocalPalette = null;
palette = null;
}
final short[] types = section.types;
final VariableValueArray array = new VariableValueArray(bitsPerValue, types.length);
if (globalToLocalPalette != null) {
for (int j = 0; j < types.length; j++) {
array.set(j, globalToLocalPalette.get(types[j]));
}
} else {
for (int j = 0; j < types.length; j++) {
array.set(j, types[j]);
}
}
final Short2ObjectMap<DataView> tileEntityDataViews = new Short2ObjectOpenHashMap<>();
// Serialize the tile entities
for (Short2ObjectMap.Entry<LanternTileEntity> tileEntityEntry : section.tileEntities.short2ObjectEntrySet()) {
if (!tileEntityEntry.getValue().isValid()) {
continue;
}
//noinspection unchecked
final ObjectSerializer<LanternTileEntity> store = ObjectSerializerRegistry.get().get(LanternTileEntity.class).get();
final DataView dataView = store.serialize(tileEntityEntry.getValue());
tileEntityDataViews.put(tileEntityEntry.getShortKey(), dataView);
}
msgSections[i] = new MessagePlayOutChunkData.Section(array, palette,
section.lightFromBlock, section.lightFromSky, tileEntityDataViews);
// The insert entry setting is used to send a "null" chunk
// after the chunk is already send to the client
// TODO: Better way to do this?
} else if (!biomes && ((1 << i) & sectionsBitMask) != 0) {
msgSections[i] = skyLight ? EMPTY_SECTION_SKYLIGHT : EMPTY_SECTION;
}
}
byte[] biomesArray = null;
if (biomes) {
short[] biomesArray0 = chunk.getBiomes();
biomesArray = new byte[biomesArray0.length];
for (int i = 0; i < biomesArray0.length; i++) {
// TODO: Only allow non-custom biome types to be send and maybe the ones supported by forge mods?
biomesArray[i] = (byte) (biomesArray0[i] & 0xff);
}
}
return new MessagePlayOutChunkData(this.coords.getX(), this.coords.getY(), skyLight, msgSections, biomesArray);
}
/**
* Removes the observer from this chunk.
*
* @param observer the observer
* @param updateClient whether the client should be notified, this may be {@code false} when
* the player is respawning on the client in a different dimension, causing
* all the chunks to be unloaded, making it unneeded for this method to do it
* again.
*/
public void removeObserver(LanternPlayer observer, boolean updateClient) {
if (this.observers.remove(observer) &&
this.clientObservers.remove(observer) && updateClient) {
observer.getConnection().send(new MessagePlayOutUnloadChunk(this.coords.getX(), this.coords.getY()));
}
// Clear the dirty states, since no one will still want to see them
if (this.clientObservers.isEmpty()) {
this.dirtyBlocks.clear();
this.dirtyChunk = false;
}
}
/**
* Adds the observer to this chunk, this may trigger the chunk to
* be loaded on the client if the chunk is already loaded.
*
* @param observer the observer
*/
public void addObserver(LanternPlayer observer) {
if (this.observers.add(observer)) {
LanternChunk chunk = world.getChunkManager().getChunkIfLoaded(this.coords);
// The chunk is already loaded, we can directly send the messages
// to the player
if (chunk != null) {
this.clientObservers.add(observer);
observer.getConnection().send(this.createChunkLoadMessages(chunk));
}
// Otherwise we will wait for the LoadChunkEvent to be called and
// send the messages at that point
}
}
}
}