All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.nukkit.positiontracking.PositionTrackingService Maven / Gradle / Ivy

There is a newer version: 1.20.40-r1
Show newest version
package cn.nukkit.positiontracking;

import cn.nukkit.Player;
import cn.nukkit.Server;
import cn.nukkit.api.PowerNukkitOnly;
import cn.nukkit.api.Since;
import cn.nukkit.inventory.Inventory;
import cn.nukkit.item.Item;
import cn.nukkit.item.ItemCompassLodestone;
import cn.nukkit.item.ItemID;
import cn.nukkit.network.protocol.DataPacket;
import cn.nukkit.network.protocol.PositionTrackingDBServerBroadcastPacket;
import com.google.common.base.Preconditions;
import com.google.common.collect.MapMaker;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.extern.log4j.Log4j2;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import java.io.*;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.IntConsumer;
import java.util.regex.Pattern;
import java.util.stream.IntStream;

/**
 * A position tracking db service. It holds file resources that needs to be closed when not needed anymore. 
 * @author joserobjr
 */
@PowerNukkitOnly
@Since("1.4.0.0-PN")
@ParametersAreNonnullByDefault
@Log4j2
public class PositionTrackingService implements Closeable {
    private static final Pattern FILENAME_PATTERN = Pattern.compile("^\\d+\\.pnt$", Pattern.CASE_INSENSITIVE);
    private static final FilenameFilter FILENAME_FILTER = (dir, name) -> FILENAME_PATTERN.matcher(name).matches() && new File(dir, name).isFile();
    private final TreeMap> storage = new TreeMap<>(Comparator.reverseOrder());
    private final AtomicBoolean closed = new AtomicBoolean(false);
    private final File folder;
    private final Map> tracking = new MapMaker().weakKeys().makeMap();

    /**
     * Creates position tracking db service. The service is ready to be used right after the creation.
     * @param folder The folder that will hold the position tracking db files
     * @throws FileNotFoundException If the folder does not exists and can't be created 
     */
    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public PositionTrackingService(File folder) throws FileNotFoundException {
        if (!folder.isDirectory() && !folder.mkdirs()) {
            throw new FileNotFoundException("Failed to create the folder "+folder);
        }
        this.folder = folder;
        WeakReference emptyRef = new WeakReference<>(null);
        Arrays.stream(Optional.ofNullable(folder.list(FILENAME_FILTER)).orElseThrow(()-> new FileNotFoundException("Invalid folder: "+folder)))
                .map(name-> Integer.parseInt(name.substring(0, name.length()-4)))
                .forEachOrdered(startIndex-> storage.put(startIndex, emptyRef));
    }
    
    private boolean hasTrackingDevice(Player player, @Nullable Inventory inventory, int trackingHandler) throws IOException {
        if (inventory == null) {
            return false;
        }
        int size = inventory.getSize();
        for (int i = 0; i < size; i++) {
            if (isTrackingDevice(player, inventory.getItem(i), trackingHandler)) {
                return true;
            }
        }
        return false;
    }
    
    private boolean isTrackingDevice(Player player, @Nullable Item item, int trackingHandler) throws IOException {
        if (!(item != null && item.getId() == ItemID.LODESTONE_COMPASS && item instanceof ItemCompassLodestone)) {
            return false;
        }
        ItemCompassLodestone compassLodestone = (ItemCompassLodestone) item;
        if (compassLodestone.getTrackingHandle() != trackingHandler) {
            return false;
        }
        PositionTracking position = getPosition(trackingHandler);
        return position != null && position.getLevelName().equals(player.getLevelName());
    }
    
    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public boolean hasTrackingDevice(Player player, int trackingHandler) throws IOException {
        for (Inventory inventory: inventories(player)) {
            if (hasTrackingDevice(player, inventory, trackingHandler)) {
                return true;
            }
        }
        return false;
    }
    
    private void sendTrackingUpdate(Player player, int trackingHandler, PositionTracking pos) {
        if (player.getLevelName().equals(pos.getLevelName())) {
            PositionTrackingDBServerBroadcastPacket packet = new PositionTrackingDBServerBroadcastPacket();
            packet.setAction(PositionTrackingDBServerBroadcastPacket.Action.UPDATE);
            packet.setPosition(pos);
            packet.setDimension(player.getLevel().getDimension());
            packet.setTrackingId(trackingHandler);
            packet.setStatus(0);
            player.dataPacket(packet);
        } else {
            sendTrackingDestroy(player, trackingHandler);
        }
    }
    
    private void sendTrackingDestroy(Player player, int trackingHandler) {
        PositionTrackingDBServerBroadcastPacket packet = destroyPacket(trackingHandler);
        player.dataPacket(packet);
    }
    
    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    @Nullable
    public synchronized PositionTracking startTracking(Player player, int trackingHandler, boolean validate) throws IOException {
        Preconditions.checkArgument(trackingHandler >= 0, "Tracking handler must be positive");
        if (isTracking(player, trackingHandler, validate)) {
            PositionTracking position = getPosition(trackingHandler);
            if (position != null) {
                sendTrackingUpdate(player, trackingHandler, position);
                return position;
            }
            stopTracking(player, trackingHandler);
            return null;
        }
        
        if (validate && !hasTrackingDevice(player, trackingHandler)) {
            return null;
        }
        
        PositionTrackingStorage storage = getStorageForHandler(trackingHandler);
        if (storage == null) {
            return null;
        }
        
        PositionTracking position = storage.getPosition(trackingHandler);
        if (position == null) {
            return null; 
        }
        
        tracking.computeIfAbsent(player, p -> new HashMap<>()).computeIfAbsent(storage, s -> new IntOpenHashSet(3)).add(trackingHandler);
        return position;
    }
    
    private PositionTrackingDBServerBroadcastPacket destroyPacket(int trackingHandler) {
        PositionTrackingDBServerBroadcastPacket packet = new PositionTrackingDBServerBroadcastPacket();
        packet.setAction(PositionTrackingDBServerBroadcastPacket.Action.DESTROY);
        packet.setTrackingId(trackingHandler);
        packet.setDimension(0);
        packet.setPosition(0, 0, 0);
        packet.setStatus(2);
        return packet;
    } 

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized boolean stopTracking(Player player) {
        Map toRemove = tracking.remove(player);
        if (toRemove != null && player.isOnline()) {
            DataPacket[] packets = toRemove.values().stream()
                    .flatMapToInt(handlers -> IntStream.of(handlers.toIntArray()))
                    .mapToObj(this::destroyPacket)
                    .toArray(DataPacket[]::new);
            player.getServer().batchPackets(new Player[]{player}, packets);
        }
        return toRemove != null;
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized boolean stopTracking(Player player, int trackingHandler) {
        Map tracking = this.tracking.get(player);
        if (tracking == null) {
            return false;
        }
        
        for (Map.Entry entry : tracking.entrySet()) {
            if (entry.getValue().remove(trackingHandler)) {
                if (entry.getValue().isEmpty()) {
                    tracking.remove(entry.getKey());
                }
                player.dataPacket(destroyPacket(trackingHandler));
                return true;
            }
        }
        return false;
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized boolean isTracking(Player player, int trackingHandler, boolean validate) throws IOException {
        Map tracking = this.tracking.get(player);
        if (tracking == null) {
            return false;
        }

        for (IntSet value : tracking.values()) {
            if (value.contains(trackingHandler)) {
                if (validate && !hasTrackingDevice(player, trackingHandler)) {
                    stopTracking(player, trackingHandler);
                    return false;
                }
                return true;
            }
        }
        return false;
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized void forceRecheckAllPlayers() {
        tracking.keySet().removeIf(p-> !p.isOnline());
        Map toRemove = new HashMap<>(2);
        for (Map.Entry> entry : tracking.entrySet()) {
            Player player = entry.getKey();
            for (Map.Entry entry2 : entry.getValue().entrySet()) {
                entry2.getValue().forEach((IntConsumer) trackingHandler-> {
                    try {
                        if (!hasTrackingDevice(player, trackingHandler)) {
                            toRemove.computeIfAbsent(player, p -> new IntArrayList(2)).add(trackingHandler);
                        }
                    } catch (IOException e) {
                        log.error("Failed to update the tracking handler {} for player {}", trackingHandler, player.getName(), e);
                    }
                });
            }
        }
        
        toRemove.forEach((player, list) -> 
                list.forEach((IntConsumer) handler -> 
                        stopTracking(player, handler)));

        Server.getInstance().getOnlinePlayers().values().forEach(this::detectNeededUpdates);
    }
    
    private Iterable inventories(Player player) {
        return () -> new Iterator() {
            int next = 0;
            @Override
            public boolean hasNext() {
                return next <= 4;
            }

            @Override
            public Inventory next() {
                switch (next++) {
                    case 0: return player.getInventory();
                    case 1: return player.getCursorInventory();
                    case 2: return player.getOffhandInventory();
                    case 3: return player.getCraftingGrid();
                    case 4: return player.getTopWindow().orElse(null);
                    default: throw new NoSuchElementException(); 
                }
            }
        };
    }
    
    private void detectNeededUpdates(Player player) {
        for (Inventory inventory: inventories(player)) {
            if (inventory == null) {
                continue;
            }
            int size = inventory.getSize();
            for (int slot = 0; slot < size; slot++) {
                Item item = inventory.getItem(slot);
                if (item.getId() == ItemID.LODESTONE_COMPASS && item instanceof ItemCompassLodestone) {
                    ItemCompassLodestone compass = (ItemCompassLodestone) item;
                    int trackingHandle = compass.getTrackingHandle();
                    if (trackingHandle != 0) {
                        PositionTracking pos;
                        try {
                            pos = getPosition(trackingHandle);
                            if (pos != null && pos.getLevelName().equals(player.getLevelName())) {
                                startTracking(player, trackingHandle, false);
                            }
                        } catch (IOException e) {
                            log.error("Failed to get the position of the tracking handler {}", trackingHandle, e);
                        }
                    }
                }
            }
        }
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public void forceRecheck(Player player) {
        Map tracking = this.tracking.get(player);
        if (tracking != null) {
            IntList toRemove = new IntArrayList(2);
            for (Map.Entry entry2 : tracking.entrySet()) {
                entry2.getValue().forEach((IntConsumer) trackingHandler-> {
                    try {
                        if (!hasTrackingDevice(player, trackingHandler)) {
                            toRemove.add(trackingHandler);
                        }
                    } catch (IOException e) {
                        log.error("Failed to update the tracking handler {} for player {}", trackingHandler, player.getName(), e);
                    }
                });
            }
            toRemove.forEach((IntConsumer) handler-> stopTracking(player, handler));
        }
        
        detectNeededUpdates(player);
    }
    
    @Nullable
    private synchronized Integer findStorageForHandler(@Nonnull Integer handler) {
        Integer best = null;
        for (Integer startIndex : storage.keySet()) {
            int comp = startIndex.compareTo(handler);
            if (comp == 0) {
                return startIndex;
            }
            if (comp < 0 && (best == null || best.compareTo(startIndex) < 0)) {
                best = startIndex;
            }
        }
        return best;
    }
    
    @Nonnull
    private synchronized PositionTrackingStorage loadStorage(@Nonnull Integer startIndex) throws IOException {
        PositionTrackingStorage trackingStorage = storage.get(startIndex).get();
        if (trackingStorage != null) {
            return trackingStorage;
        }
        PositionTrackingStorage positionTrackingStorage = new PositionTrackingStorage(startIndex, new File(folder, startIndex+".pnt"));
        storage.put(startIndex, new WeakReference<>(positionTrackingStorage));
        return positionTrackingStorage;
    }
    
    @Nullable
    private synchronized PositionTrackingStorage getStorageForHandler(@Nonnull Integer trackingHandler) throws IOException{
        Integer startIndex = findStorageForHandler(trackingHandler);
        if (startIndex == null) {
            return null;
        }
        
        PositionTrackingStorage storage = loadStorage(startIndex);
        if (trackingHandler > storage.getMaxHandler()) {
            return null;
        }
        
        return storage;
    }
    
    /**
     * Attempts to reuse an existing and enabled trackingHandler for the given position, if none is found than a new handler is created
     * if the limit was not exceeded.
     * @param position The position that needs a handler 
     * @return The trackingHandler assigned to the position or an empty OptionalInt if none was found and this storage is full
     * @throws IOException If an error occurred while reading or writing the file
     */
    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized int addOrReusePosition(NamedPosition position) throws IOException {
        checkClosed();
        OptionalInt trackingHandler = findTrackingHandler(position);
        if (trackingHandler.isPresent()) {
            return trackingHandler.getAsInt();
        }
        return addNewPosition(position);
    }

    /**
     * Adds the given position as a new entry in this storage, even if the position is already registered and enabled.
     * @param position The position that needs a handler 
     * @return The trackingHandler assigned to the position or an empty OptionalInt if none was found and this storage is full
     * @throws IOException If an error occurred while reading or writing the file
     */
    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized int addNewPosition(NamedPosition position) throws IOException {
        return addNewPosition(position, true);
    }

    /**
     * Adds the given position as a new entry in this storage, even if the position is already registered and enabled.
     * @param position The position that needs a handler 
     * @param enabled If the position will be added as enabled or disabled
     * @return The trackingHandler assigned to the position or an empty OptionalInt if none was found and this storage is full
     * @throws IOException If an error occurred while reading or writing the file
     */
    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized int addNewPosition(NamedPosition position, boolean enabled) throws IOException {
        checkClosed();
        int next = 1;
        if (!storage.isEmpty()) {
            PositionTrackingStorage trackingStorage = loadStorage(storage.firstKey());
            OptionalInt handler = trackingStorage.addNewPosition(position, enabled);
            if (handler.isPresent()) {
                return handler.getAsInt();
            }
            next = trackingStorage.getMaxHandler();
        }

        PositionTrackingStorage trackingStorage = new PositionTrackingStorage(next, new File(folder, next + ".pnt"));
        storage.put(next, new WeakReference<>(trackingStorage));
        return trackingStorage.addNewPosition(position, enabled).orElseThrow(InternalError::new);
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    @Nonnull
    public OptionalInt findTrackingHandler(NamedPosition position) throws IOException {
        IntList handlers = findTrackingHandlers(position, true, 1);
        if (!handlers.isEmpty()) {
            return OptionalInt.of(handlers.getInt(0));
        }
        return OptionalInt.empty();
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized boolean invalidateHandler(int trackingHandler) throws IOException {
        checkClosed();
        PositionTrackingStorage storage = getStorageForHandler(trackingHandler);
        if (storage == null) {
            return false;
        }

        if (!storage.hasPosition(trackingHandler, false)) {
            return false;
        }
        storage.invalidateHandler(trackingHandler);
        
        handlerDisabled(trackingHandler);
        
        return true;
    }
    
    private void handlerDisabled(int trackingHandler) {
        List players = new ArrayList<>();
        for (Map.Entry> playerMapEntry : tracking.entrySet()) {
            for (IntSet value : playerMapEntry.getValue().values()) {
                if (value.contains(trackingHandler)) {
                    players.add(playerMapEntry.getKey());
                    break;
                }
            }
        }

        if (!players.isEmpty()) {
            Server.getInstance().batchPackets(players.toArray(Player.EMPTY_ARRAY), new DataPacket[]{destroyPacket(trackingHandler)});
        }
    }
    
    private void handlerEnabled(int trackingHandler) throws IOException {
        Server server = Server.getInstance();
        for (Player player : server.getOnlinePlayers().values()) {
            if (hasTrackingDevice(player, trackingHandler) && !isTracking(player, trackingHandler, false)) {
                startTracking(player, trackingHandler, false);
            }
        }
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    @Nullable
    public PositionTracking getPosition(int trackingHandle) throws IOException {
        return getPosition(trackingHandle, true);
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    @Nullable
    public PositionTracking getPosition(int trackingHandle, boolean onlyEnabled) throws IOException {
        checkClosed();
        PositionTrackingStorage trackingStorage = getStorageForHandler(trackingHandle);
        if (trackingStorage == null) {
            return null;
        }
        
        return trackingStorage.getPosition(trackingHandle, onlyEnabled);
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized boolean isEnabled(int trackingHandler) throws IOException {
        checkClosed();
        PositionTrackingStorage trackingStorage = getStorageForHandler(trackingHandler);
        return trackingStorage != null && trackingStorage.isEnabled(trackingHandler);
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized boolean setEnabled(int trackingHandler, boolean enabled) throws IOException {
        checkClosed();
        PositionTrackingStorage trackingStorage = getStorageForHandler(trackingHandler);
        if (trackingStorage == null) {
            return false;
        }
        if (trackingStorage.setEnabled(trackingHandler, enabled)) {
            if (enabled) {
                handlerEnabled(trackingHandler);
            } else {
                handlerDisabled(trackingHandler);
            }
            return true;
        }
        
        return false;
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized boolean hasPosition(int trackingHandler) throws IOException {
        return hasPosition(trackingHandler, true);
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    public synchronized boolean hasPosition(int trackingHandler, boolean onlyEnabled) throws IOException {
        checkClosed();
        Integer startIndex = findStorageForHandler(trackingHandler);
        if (startIndex == null) {
            return false;
        }
        
        if (!storage.containsKey(startIndex)) {
            return false;
        }
        
        return loadStorage(startIndex).hasPosition(trackingHandler, onlyEnabled);
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    @Nonnull
    public synchronized IntList findTrackingHandlers(NamedPosition pos) throws IOException {
        return findTrackingHandlers(pos, true);
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    @Nonnull
    public synchronized IntList findTrackingHandlers(NamedPosition pos, boolean onlyEnabled) throws IOException {
        return findTrackingHandlers(pos, onlyEnabled, Integer.MAX_VALUE);
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    @Nonnull
    public synchronized IntList findTrackingHandlers(NamedPosition pos, boolean onlyEnabled, int limit) throws IOException {
        checkClosed();
        IntList list = new IntArrayList();
        for (Integer startIndex : storage.descendingKeySet()) {
            list.addAll(loadStorage(startIndex).findTrackingHandlers(pos, onlyEnabled, limit - list.size()));
            if (list.size() >= limit) {
                break;
            }
        }
        return list;
    }

    /**
     * Close all active 
     * @throws IOException If any resource failed to close properly. 
     * The detailed exceptions will be in getCause() and and getSuppressed()
     */
    @Override
    public synchronized void close() throws IOException {
        closed.set(true);
        IOException exception = null;
        for (WeakReference ref : storage.values()) {
            PositionTrackingStorage positionTrackingStorage = ref.get();
            if (positionTrackingStorage != null) {
                try {
                    positionTrackingStorage.close();
                } catch (Throwable e) {
                    if (exception == null) {
                        exception = new IOException(e);
                    } else {
                        exception.addSuppressed(e);
                    }
                }
            }
        }
        if (exception != null) {
            throw exception;
        }
    }

    @Override
    protected void finalize() throws Throwable {
        close();
    }

    private void checkClosed() throws IOException {
        if (closed.get()) {
            throw new IOException("The service is closed");
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy