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

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

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

import cn.nukkit.api.PowerNukkitOnly;
import cn.nukkit.api.Since;
import cn.nukkit.math.NukkitMath;
import com.google.common.base.Preconditions;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * Stores a sequential range of {@link PositionTracking} objects in a file. The read operation is cached.
 * 

This object holds a file handler and must be closed when it is no longer needed.

*

Once closed the instance cannot be reused.

* @author joserobjr */ @PowerNukkitOnly @Since("1.4.0.0-PN") @ParametersAreNonnullByDefault public class PositionTrackingStorage implements Closeable { @PowerNukkitOnly @Since("1.4.0.0-PN") public static final int DEFAULT_MAX_STORAGE = 500; private static final byte[] HEADER = new byte[]{ 12,32,32, 'P', 'N', 'P', 'T', 'D', 'B', '1' }; private final int startIndex; private final int maxStorage; private final long garbagePos; private final long stringHeapPos; private final RandomAccessFile persistence; private final Cache> cache = CacheBuilder.newBuilder().expireAfterAccess(5,TimeUnit.MINUTES).concurrencyLevel(1).build(); private int nextIndex; /** * Opens or create the file and all directories in the path automatically. The given start index will be used * in new files and will be checked when opening files. If the file being opened don't matches this value * internally than an IllegalArgumentException will be thrown. * @param startIndex The number of the first handler. Must be higher than 0 and must match the number of the existing file. * @param persistenceFile The file being opened or created. Parent directories will also be created if necessary. * @throws IOException If an error has occurred while reading, parsing or creating the file * @throws IllegalArgumentException If opening an existing file and the internal startIndex don't match the given startIndex */ @PowerNukkitOnly @Since("1.4.0.0-PN") public PositionTrackingStorage(int startIndex, File persistenceFile) throws IOException { this(startIndex, persistenceFile, 0); } /** * Opens or create the file and all directories in the path automatically. The given start index will be used * in new files and will be checked when opening files. If the file being opened don't matches this value * internally than an IllegalArgumentException will be thrown. * @param startIndex The number of the first handler. Must be higher than 0 and must match the number of the existing file. * @param persistenceFile The file being opened or created. Parent directories will also be created if necessary. * @param maxStorage The maximum amount of positions that this storage may hold. It cannot be changed after creation. * Ignored when loading an existing file. When zero or negative, a default value will be used. * @throws IOException If an error has occurred while reading, parsing or creating the file * @throws IllegalArgumentException If opening an existing file and the internal startIndex don't match the given startIndex */ @PowerNukkitOnly @Since("1.4.0.0-PN") public PositionTrackingStorage(int startIndex, File persistenceFile, int maxStorage) throws IOException { Preconditions.checkArgument(startIndex > 0, "Start index must be positive. Got {}", startIndex); this.startIndex = startIndex; if (maxStorage <= 0) { maxStorage = DEFAULT_MAX_STORAGE; } boolean created = false; if (!persistenceFile.isFile()) { if (!persistenceFile.getParentFile().isDirectory() && !persistenceFile.getParentFile().mkdirs()) { throw new FileNotFoundException("Could not create the directory "+persistenceFile.getParent()); } if (!persistenceFile.createNewFile()) { throw new FileNotFoundException("Could not create the file "+persistenceFile); } created = true; } else if(persistenceFile.length() == 0) { created = true; } this.persistence = new RandomAccessFile(persistenceFile, "rwd"); try { if (created) { persistence.write(ByteBuffer.allocate(HEADER.length + 4 + 4 + 4) .put(HEADER) .putInt(maxStorage) .putInt(startIndex) .putInt(startIndex) .array()); this.maxStorage = maxStorage; nextIndex = startIndex; } else { byte[] check = new byte[HEADER.length]; EOFException eof = null; int max; int next; int start; try { persistence.readFully(check); byte[] buf = new byte[4 + 4 + 4]; persistence.readFully(buf); ByteBuffer buffer = ByteBuffer.wrap(buf); max = buffer.getInt(); next = buffer.getInt(); start = buffer.getInt(); } catch (EOFException e) { eof = e; max = 0; next = 0; start = 0; } if (eof != null || max <= 0 || next <= 0 || start <= 0 || !Arrays.equals(check, HEADER)) { throw new IOException("The file " + persistenceFile + " is not a valid PowerNukkit TrackingPositionDB persistence file.", eof); } if (start != startIndex) { throw new IllegalArgumentException("The start index "+startIndex+" was given but the file "+persistenceFile+" has start index "+start); } this.maxStorage = maxStorage = max; this.nextIndex = next; } garbagePos = getAxisPos(startIndex + maxStorage); // cnt off len max stringHeapPos = garbagePos + 4 + (8 + 4) * 15; if (created) { persistence.seek(stringHeapPos - 1); persistence.writeByte(0); } } catch (Throwable e) { try { persistence.close(); } catch (Throwable e2) { e.addSuppressed(e2); } throw e; } } private long getAxisPos(int trackingHandler) { // max str cur on nam len x y z return HEADER.length + 4 + 4 + 4 + (1 + 8 + 4 + 8 + 8 + 8) * (long)(trackingHandler - startIndex); } private void validateHandler(int trackingHandler) { Preconditions.checkArgument(trackingHandler >= startIndex, "The trackingHandler {} is too low for this storage (starts at {})", trackingHandler, startIndex); int limit = startIndex + maxStorage; Preconditions.checkArgument(trackingHandler <= limit, "The trackingHandler {} is too high for this storage (ends at {})", trackingHandler, limit); } /** * Retrieves the {@link PositionTracking} object that is assigned to the given trackingHandler. * The handler must be valid for this storage. *

This call may return a cached result but the returned object can be modified freely.

* @param trackingHandler A valid handler for this storage * @return A clone of the cached result. * @throws IOException If an error has occurred while accessing the file * @throws IllegalArgumentException If the trackingHandler is not valid for this storage */ @PowerNukkitOnly @Since("1.4.0.0-PN") @Nullable public PositionTracking getPosition(int trackingHandler) throws IOException { validateHandler(trackingHandler); try { return cache.get(trackingHandler, ()-> loadPosition(trackingHandler, true)) .map(PositionTracking::clone) .orElse(null); } catch (ExecutionException e) { throw handleExecutionException(e); } } /** * Retrieves the {@link PositionTracking} object that is assigned to the given trackingHandler. * The handler must be valid for this storage. *

This call may return a cached result but the returned object can be modified freely.

* @param trackingHandler A valid handler for this storage * @param onlyEnabled When false, disabled positions that wasn't invalidated may be returned. * Caching only works when this is set to true * @return A clone of the cached result. * @throws IOException If an error has occurred while accessing the file * @throws IllegalArgumentException If the trackingHandler is not valid for this storage */ @PowerNukkitOnly @Since("1.4.0.0-PN") @Nullable public PositionTracking getPosition(int trackingHandler, boolean onlyEnabled) throws IOException { if (onlyEnabled) { return getPosition(trackingHandler); } validateHandler(trackingHandler); return loadPosition(trackingHandler, false).orElse(null); } /** * 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 OptionalInt addOrReusePosition(NamedPosition position) throws IOException { OptionalInt handler = findTrackingHandler(position); if (handler.isPresent()) { return handler; } 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 OptionalInt 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 OptionalInt addNewPosition(NamedPosition position, boolean enabled) throws IOException { OptionalInt handler = addNewPos(position, enabled); if (!handler.isPresent()) { return handler; } if (enabled) { cache.put(handler.getAsInt(), Optional.of(new PositionTracking(position))); } return handler; } @PowerNukkitOnly @Since("1.4.0.0-PN") @Nonnull public OptionalInt findTrackingHandler(NamedPosition position) throws IOException { OptionalInt cached = cache.asMap().entrySet().stream() .filter(e-> e.getValue().filter(position::matchesNamedPosition).isPresent()) .mapToInt(Map.Entry::getKey) .findFirst(); if (cached.isPresent()) { return cached; } IntList handlers = findTrackingHandlers(position, true, 1); if (handlers.isEmpty()) { return OptionalInt.empty(); } int found = handlers.getInt(0); cache.put(found, Optional.of(new PositionTracking(position))); return OptionalInt.of(found); } private IOException handleExecutionException(ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof IOException) { return (IOException) cause; } return new IOException(e); } @PowerNukkitOnly @Since("1.4.0.0-PN") public synchronized void invalidateHandler(int trackingHandler) throws IOException { validateHandler(trackingHandler); invalidatePos(trackingHandler); } @PowerNukkitOnly @Since("1.4.0.0-PN") public synchronized boolean isEnabled(int trackingHandler) throws IOException { validateHandler(trackingHandler); persistence.seek(getAxisPos(trackingHandler)); return persistence.readBoolean(); } @PowerNukkitOnly @Since("1.4.0.0-PN") public synchronized boolean setEnabled(int trackingHandler, boolean enabled) throws IOException { validateHandler(trackingHandler); long pos = getAxisPos(trackingHandler); persistence.seek(pos); if (persistence.readBoolean() == enabled) { return false; } if (persistence.readLong() == 0 && enabled) { return false; } persistence.seek(pos); persistence.writeBoolean(enabled); cache.invalidate(trackingHandler); return true; } @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 { validateHandler(trackingHandler); persistence.seek(getAxisPos(trackingHandler)); boolean enabled = persistence.readBoolean(); if (!enabled && onlyEnabled) { return false; } return persistence.readLong() != 0; } private synchronized void invalidatePos(int trackingHandler) throws IOException { long pos = getAxisPos(trackingHandler); persistence.seek(pos); persistence.writeBoolean(false); byte[] buf = new byte[8 + 4]; persistence.readFully(buf); ByteBuffer buffer = ByteBuffer.wrap(buf); long namePos = buffer.getLong(); int nameLen = buffer.getInt(); persistence.seek(pos + 1); persistence.write(new byte[8 + 4]); cache.put(trackingHandler, Optional.empty()); addGarbage(namePos, nameLen); } private synchronized void addGarbage(long pos, int len) throws IOException { persistence.seek(garbagePos); int count = persistence.readInt(); if (count >= 15) { return; } byte[] buf = new byte[4 + 8]; ByteBuffer buffer = ByteBuffer.wrap(buf); if (count > 0) { for (int attempt = 0; attempt < 15; attempt++) { persistence.readFully(buf); buffer.rewind(); long garbage = buffer.getLong(); int garbageLen = buffer.getInt(); if (garbage != 0) { if (garbage + garbageLen == pos) { persistence.seek(persistence.getFilePointer() - 4 - 8); buffer.rewind(); buffer.putLong(garbage) .putInt(garbageLen + len); persistence.write(buf); return; } else if (pos + len == garbage) { persistence.seek(persistence.getFilePointer() - 4 - 8); buffer.rewind(); buffer.putLong(pos) .putInt(garbageLen + len); persistence.write(buf); return; } } } persistence.seek(garbagePos + 4); } for (int attempt = 0; attempt < 15; attempt++) { persistence.readFully(buf); buffer.rewind(); long garbage = buffer.getLong(); if (garbage == 0) { persistence.seek(persistence.getFilePointer() - 4 - 8); buffer.rewind(); buffer.putLong(pos).putInt(len); persistence.write(buf); persistence.seek(garbagePos); persistence.writeInt(count + 1); return; } } } private synchronized long findSpaceInStringHeap(int len) throws IOException { persistence.seek(garbagePos); int remaining = persistence.readInt(); if (remaining <= 0) { return persistence.length(); } byte[] buf = new byte[4 + 8]; ByteBuffer buffer = ByteBuffer.wrap(buf); for (int attempt = 0; attempt < 15; attempt++) { persistence.readFully(buf); buffer.rewind(); long garbage = buffer.getLong(); int garbageLen = buffer.getInt(); if (garbage >= stringHeapPos && len <= garbageLen) { persistence.seek(persistence.getFilePointer() - 4 - 8); if (garbageLen == len) { persistence.write(new byte[8 + 4]); persistence.seek(garbagePos); persistence.writeInt(remaining - 1); } else { buffer.rewind(); buffer.putLong(garbage + len).putInt(garbageLen - len); persistence.write(buf); } return garbage; } } return persistence.length(); } private synchronized OptionalInt addNewPos(NamedPosition pos, boolean enabled) throws IOException { if (nextIndex - startIndex >= maxStorage) { return OptionalInt.empty(); } int handler = nextIndex++; writePos(handler, pos, enabled); persistence.seek(HEADER.length + 4); persistence.writeInt(nextIndex); return OptionalInt.of(handler); } private synchronized void writePos(int trackingHandler, NamedPosition pos, boolean enabled) throws IOException { byte[] name = pos.getLevelName().getBytes(StandardCharsets.UTF_8); long namePos = addLevelName(name); persistence.seek(getAxisPos(trackingHandler)); persistence.write(ByteBuffer.allocate(1 + 8 + 4 + 8 + 8 + 8) .put(enabled? (byte)1 : 0) .putLong(namePos) .putInt(name.length) .putDouble(pos.x) .putDouble(pos.y) .putDouble(pos.z) .array()); } private synchronized long addLevelName(byte[] name) throws IOException { long pos = findSpaceInStringHeap(name.length); persistence.seek(pos); persistence.write(name); return pos; } @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 { persistence.seek(HEADER.length + 4 + 4 + 4); int handler = startIndex - 1; final double lookingX = pos.x; final double lookingY = pos.y; final double lookingZ = pos.z; final byte[] lookingName = pos.getLevelName().getBytes(StandardCharsets.UTF_8); IntList results = new IntArrayList(NukkitMath.clamp(limit, 1, 16)); byte[] buf = new byte[8 + 4 + 8 + 8 + 8]; ByteBuffer buffer = ByteBuffer.wrap(buf); while (true) { handler++; if (handler >= nextIndex) { return results; } boolean enabled = persistence.readBoolean(); if (onlyEnabled && !enabled) { if (persistence.skipBytes(36) != 36) throw new EOFException(); continue; } persistence.readFully(buf); buffer.rewind(); long namePos = buffer.getLong(); int nameLen = buffer.getInt(); double x = buffer.getDouble(); double y = buffer.getDouble(); double z = buffer.getDouble(); if (namePos > 0 && nameLen > 0 && x == lookingX && y == lookingY && z == lookingZ) { long fp = persistence.getFilePointer(); byte[] nameBytes = new byte[nameLen]; persistence.seek(namePos); persistence.readFully(nameBytes); if (Arrays.equals(lookingName, nameBytes)) { results.add(handler); if (results.size() >= limit) { return results; } } persistence.seek(fp); } } } private synchronized Optional loadPosition(int trackingHandler, boolean onlyEnabled) throws IOException { if (trackingHandler >= nextIndex) { return Optional.empty(); } persistence.seek(getAxisPos(trackingHandler)); byte[] buf = new byte[1 + 8 + 4 + 8 + 8 + 8]; persistence.readFully(buf); boolean enabled = buf[0] == 1; if (!enabled && onlyEnabled) { return Optional.empty(); } ByteBuffer buffer = ByteBuffer.wrap(buf, 1, buf.length - 1); long namePos = buffer.getLong(); if (namePos == 0) { return Optional.empty(); } int nameLen = buffer.getInt(); double x = buffer.getDouble(); double y = buffer.getDouble(); double z = buffer.getDouble(); byte[] nameBytes = new byte[nameLen]; persistence.seek(namePos); persistence.readFully(nameBytes); String name = new String(nameBytes, StandardCharsets.UTF_8); return Optional.of(new PositionTracking(name, x, y, z)); } @PowerNukkitOnly @Since("1.4.0.0-PN") public int getStartingHandler() { return startIndex; } @PowerNukkitOnly @Since("1.4.0.0-PN") public int getMaxHandler() { return startIndex + maxStorage - 1; } @Override public synchronized void close() throws IOException { persistence.close(); } @Override protected void finalize() throws Throwable { if (persistence != null) { persistence.close(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy