/* * This file is part of Cubic Chunks Mod, licensed under the MIT License (MIT). * * Copyright (c) 2015 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 cubicchunks.server; import com.google.common.base.Predicate; import gnu.trove.list.TShortList; import gnu.trove.list.array.TShortArrayList; import gnu.trove.map.TIntObjectMap; import gnu.trove.map.hash.TIntObjectHashMap; import net.minecraft.block.state.IBlockState; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.network.Packet; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.math.BlockPos; import net.minecraftforge.common.ForgeModContainer; import net.minecraftforge.fml.common.network.simpleimpl.IMessage; import java.util.function.Consumer; import javax.annotation.ParametersAreNonnullByDefault; import cubicchunks.CubicChunks; import cubicchunks.network.PacketCube; import cubicchunks.network.PacketCubeBlockChange; import cubicchunks.network.PacketDispatcher; import cubicchunks.network.PacketUnloadCube; import cubicchunks.server.chunkio.async.forge.AsyncWorldIOExecutor; import cubicchunks.util.AddressTools; import cubicchunks.util.CubePos; import cubicchunks.util.XYZAddressable; import cubicchunks.util.ticket.ITicket; import cubicchunks.world.ICubicWorld; import cubicchunks.world.IProviderExtras; import cubicchunks.world.cube.Cube; import mcp.MethodsReturnNonnullByDefault; @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault public class CubeWatcher implements XYZAddressable, ITicket { private final Consumer<Cube> consumer = (c) -> { this.cube = c; this.loading = false; if (this.cube != null) { this.cube.getTickets().add(this); } }; private final CubeProviderServer cubeCache; private PlayerCubeMap playerCubeMap; private Cube cube; private final TIntObjectMap<WatcherPlayerEntry> players = new TIntObjectHashMap<>(); private final TShortList dirtyBlocks = new TShortArrayList(64); private final CubePos cubePos; private long previousWorldTime = 0; private boolean sentToPlayers = false; private boolean loading = true; // CHECKED: 1.10.2-12.18.1.2092 public CubeWatcher(PlayerCubeMap playerCubeMap, CubePos cubePos) { this.playerCubeMap = playerCubeMap; this.cubeCache = playerCubeMap.getWorld().getCubeCache(); this.cubeCache.asyncGetCube( cubePos.getX(), cubePos.getY(), cubePos.getZ(), IProviderExtras.Requirement.LOAD, consumer); this.cubePos = cubePos; } // CHECKED: 1.10.2-12.18.1.2092 public void addPlayer(EntityPlayerMP player) { if (this.players.containsKey(player.getEntityId())) { CubicChunks.LOGGER.debug("Failed to add player. {} already is in cube at {}", player, cubePos); return; } if (this.players.isEmpty()) { this.previousWorldTime = this.getWorldTime(); } this.players.put(player.getEntityId(), new WatcherPlayerEntry(player)); if (this.sentToPlayers) { this.sendToPlayer(player); //TODO: cube watch event? } } // CHECKED: 1.10.2-12.18.1.2092 public void removePlayer(EntityPlayerMP player) { if (!this.players.containsKey(player.getEntityId())) { return; } // If we haven't loaded yet don't load the chunk just so we can clean it up if (this.cube == null) { this.players.remove(player.getEntityId()); if (this.players.isEmpty()) { if (loading) { AsyncWorldIOExecutor.dropQueuedCubeLoad(this.playerCubeMap.getWorld(), cubePos.getX(), cubePos.getY(), cubePos.getZ(), c -> this.cube = c); } playerCubeMap.removeEntry(this); } return; } if (this.sentToPlayers) { PacketDispatcher.sendTo(new PacketUnloadCube(this.cubePos), player); } this.players.remove(player.getEntityId()); //TODO: Cube unwatch event //net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.event.world.ChunkWatchEvent.UnWatch(this.pos, player)); if (this.players.isEmpty()) { playerCubeMap.removeEntry(this); } } // CHECKED: 1.10.2-12.18.1.2092 public boolean providePlayerCube(boolean canGenerate) { if (loading) { return false; } if (this.cube != null && (!canGenerate || (cube.isFullyPopulated() && cube.isInitialLightingDone()))) { return true; } int cubeX = cubePos.getX(); int cubeY = cubePos.getY(); int cubeZ = cubePos.getZ(); playerCubeMap.getWorld().getProfiler().startSection("getCube"); if (canGenerate) { this.cube = this.cubeCache.getCube(cubeX, cubeY, cubeZ, IProviderExtras.Requirement.LIGHT); } else { this.cube = this.cubeCache.getCube(cubeX, cubeY, cubeZ, IProviderExtras.Requirement.LOAD); } if (this.cube != null) { this.cube.getTickets().add(this); } playerCubeMap.getWorld().getProfiler().endSection(); return this.cube != null; } public boolean isSentToPlayers() { return sentToPlayers; } // CHECKED: 1.10.2-12.18.1.2092 public boolean sendToPlayers() { if (this.sentToPlayers) { return true; } if (this.cube == null || !this.cube.isPopulated() || !this.cube.isInitialLightingDone()) { return false; } ColumnWatcher columnEntry = playerCubeMap.getColumnWatcher(this.cubePos.chunkPos()); //can't send cubes before columns if (columnEntry == null || !columnEntry.isSentToPlayers()) { return false; } this.dirtyBlocks.clear(); //set to true before adding to queue so that sendToPlayer can actually add it this.sentToPlayers = true; for (WatcherPlayerEntry playerEntry : this.players.valueCollection()) { //don't send entities here, Column sends them. //TODO: send entities per cube? Sending all entities from column may be bad on multiplayer sendToPlayer(playerEntry.player); } return true; } // CHECKED: 1.10.2-12.18.1.2092 public void sendToPlayer(EntityPlayerMP player) { if (!this.sentToPlayers) { return; } PacketDispatcher.sendTo(new PacketCube(this.cube), player); } // CHECKED: 1.10.2-12.18.1.2092 public void updateInhabitedTime() { final long now = getWorldTime(); if (this.cube == null) { this.previousWorldTime = now; return; } long inhabitedTime = this.cube.getColumn().getInhabitedTime(); inhabitedTime += now - this.previousWorldTime; this.cube.getColumn().setInhabitedTime(inhabitedTime); this.previousWorldTime = now; } // CHECKED: 1.10.2-12.18.1.2092 public void blockChanged(int localX, int localY, int localZ) { //if we are adding the first one, add it to update list if (this.dirtyBlocks.isEmpty()) { playerCubeMap.addToUpdateEntry(this); } // If the number of changes is above clumpingThreshold // we send the whole cube, but to decrease network usage // forge sends only TEs that have changed, // so we need to know all changed blocks. So add everything // it's a set so no need to check for duplicates this.dirtyBlocks.add(AddressTools.getLocalAddress(localX, localY, localZ)); } // CHECKED: 1.10.2-12.18.1.2092 public void update() { if (!this.sentToPlayers) { return; } assert cube != null; // are there any updates? if (this.dirtyBlocks.isEmpty()) { return; } ICubicWorld world = this.cube.getCubicWorld(); if (this.dirtyBlocks.size() >= ForgeModContainer.clumpingThreshold) { // send whole cube sendPacketToAllPlayers(new PacketCube(cube)); } else { // send all the dirty blocks sendPacketToAllPlayers(new PacketCubeBlockChange(this.cube, this.dirtyBlocks)); // send the block entites on those blocks too this.dirtyBlocks.forEach(localAddress -> { BlockPos pos = cube.localAddressToBlockPos(localAddress); IBlockState state = this.cube.getBlockState(pos); if (state.getBlock().hasTileEntity(state)) { sendBlockEntityToAllPlayers(world.getTileEntity(pos)); } return true; }); } this.dirtyBlocks.clear(); } private void sendBlockEntityToAllPlayers(TileEntity blockEntity) { if (blockEntity == null) { return; } Packet packet = blockEntity.getUpdatePacket(); if (packet == null) { return; } sendPacketToAllPlayers(packet); } public boolean containsPlayer(EntityPlayerMP player) { return this.players.containsKey(player.getEntityId()); } public boolean hasPlayerMatching(Predicate<EntityPlayerMP> predicate) { //if any of them is true - stop and return false, then negate the result to get true return !this.players.forEachValue(value -> !predicate.apply(value.player)); } public double getDistanceSq(CubePos cubePos, Entity entity) { double blockX = cubePos.getXCenter(); double blockY = cubePos.getYCenter(); double blockZ = cubePos.getZCenter(); double dx = blockX - entity.posX; double dy = blockY - entity.posY; double dz = blockZ - entity.posZ; return dx*dx + dy*dy + dz*dz; } public Cube getCube() { return this.cube; } public double getClosestPlayerDistance() { double min = Double.MAX_VALUE; for (WatcherPlayerEntry entry : this.players.valueCollection()) { double dist = getDistanceSq(cubePos, entry.player); if (dist < min) { min = dist; } } return min; } private long getWorldTime() { return playerCubeMap.getWorldServer().getWorldTime(); } private void sendPacketToAllPlayers(Packet<?> packet) { for (WatcherPlayerEntry entry : this.players.valueCollection()) { entry.player.connection.sendPacket(packet); } } private void sendPacketToAllPlayers(IMessage packet) { for (WatcherPlayerEntry entry : this.players.valueCollection()) { PacketDispatcher.sendTo(packet, entry.player); } } public CubePos getCubePos() { return cubePos; } @Override public int getX() { return this.cubePos.getX(); } @Override public int getY() { return this.cubePos.getY(); } @Override public int getZ() { return this.cubePos.getZ(); } @Override public boolean shouldTick() { return true; // Cubes that players can see should tick } }