Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions AreaShop/src/main/java/me/wiefferink/areashop/AreaShop.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
import me.wiefferink.areashop.interfaces.WorldEditInterface;
import me.wiefferink.areashop.interfaces.WorldGuardInterface;
import me.wiefferink.areashop.listeners.PlayerLoginLogoutListener;
import me.wiefferink.areashop.managers.CacheManager;
import me.wiefferink.areashop.managers.FeatureManager;
import me.wiefferink.areashop.managers.IFileManager;
import me.wiefferink.areashop.managers.FileManager;
import me.wiefferink.areashop.managers.IFileManager;
import me.wiefferink.areashop.managers.Manager;
import me.wiefferink.areashop.managers.SignErrorLogger;
import me.wiefferink.areashop.managers.SignLinkerManager;
Expand Down Expand Up @@ -56,6 +57,7 @@
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
Expand All @@ -81,6 +83,7 @@ public final class AreaShop extends JavaPlugin implements AreaShopApi {
private MessageBridge messageBridge;
private IFileManager fileManager = null;
private LanguageManager languageManager = null;
private CacheManager cacheManager = null;
private SignLinkerManager signLinkerManager = null;
private FeatureManager featureManager = null;
private SignManager signManager;
Expand Down Expand Up @@ -285,11 +288,17 @@ public void onEnable() {
managers.add(featureManager);
signManager = injector.getInstance(SignManager.class);
managers.add(signManager);
cacheManager = injector.getInstance(CacheManager.class);
String rawExpiryDuration = fileManager.getConfig().getString("cacheExpiryDuration", "7d");
long millis = Utils.durationStringToLong(rawExpiryDuration);
cacheManager.initialize(new File(getDataFolder(), "uuid-cache.bin"), Duration.ofMillis(millis));
cacheManager.loadCache();
managers.add(cacheManager);

loadExtensions();

// Register the event listeners
getServer().getPluginManager().registerEvents(new PlayerLoginLogoutListener(this, messageBridge), this);
getServer().getPluginManager().registerEvents(new PlayerLoginLogoutListener(this, messageBridge, this.cacheManager), this);

setupTasks();

Expand Down Expand Up @@ -384,6 +393,10 @@ private String cleanVersion(String version) {
return version;
}

public CacheManager getCacheManager() {
return cacheManager;
}

/**
* Called on shutdown or reload of the server.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import me.wiefferink.areashop.AreaShop;
import me.wiefferink.areashop.MessageBridge;
import me.wiefferink.areashop.managers.CacheManager;
import me.wiefferink.areashop.regions.BuyRegion;
import me.wiefferink.areashop.regions.GeneralRegion;
import me.wiefferink.areashop.regions.RentRegion;
import me.wiefferink.areashop.tools.Utils;
import me.wiefferink.areashop.tools.CacheWrapper;
import me.wiefferink.bukkitdo.Do;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
Expand All @@ -27,14 +29,20 @@ public final class PlayerLoginLogoutListener implements Listener {

private final AreaShop plugin;
private final MessageBridge messageBridge;
private final CacheManager cacheManager;

/**
* Constructor.
* @param plugin The AreaShop plugin
*/
public PlayerLoginLogoutListener(@Nonnull AreaShop plugin, @Nonnull MessageBridge messageBridge) {
public PlayerLoginLogoutListener(
@Nonnull AreaShop plugin,
@Nonnull MessageBridge messageBridge,
@Nonnull CacheManager cacheManager
) {
this.plugin = plugin;
this.messageBridge = messageBridge;
this.cacheManager = cacheManager;
}

/**
Expand All @@ -48,6 +56,8 @@ public void onPlayerLogin(PlayerLoginEvent event) {
}
final Player player = event.getPlayer();

this.cacheManager.computeIfAbsent(player.getUniqueId(), uuid -> new CacheWrapper(uuid, player.getName()));

// Schedule task to check for notifications, prevents a lag spike at login
Do.syncTimerLater(25, 25, () -> {
// Delay until all regions are loaded
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package me.wiefferink.areashop.managers;

import jakarta.inject.Inject;
import me.wiefferink.areashop.tools.CacheWrapper;
import org.bukkit.plugin.Plugin;

import javax.annotation.Nonnull;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.logging.Level;

public class CacheManager extends Manager {

private final Plugin plugin;
private final Map<UUID, CacheWrapper> cache;

private File cacheFile;
private Duration expiryDuration;

@Inject
CacheManager(@Nonnull Plugin plugin) {
this.plugin = plugin;
this.cache = new HashMap<>();
}


@Override
public void shutdown() {
if (this.expiryDuration != null) {
trimCache(expiryDuration);
}
if (this.cacheFile != null) {
saveCache(cacheFile);
}
}

public void initialize(@Nonnull File cacheFile, @Nonnull Duration expiryDuration) {
this.cacheFile = cacheFile;
this.expiryDuration = expiryDuration;
}

public void loadCache() {
if (this.cacheFile != null) {
loadCache(this.cacheFile);
}
}

/**
* Load the cache from the file
*/
public void loadCache(@Nonnull File cacheFile) {
if (Files.notExists(cacheFile.toPath())) {
this.plugin.getLogger().info("uuid cache file does not exist, loaded 0 entries");
return;
}
this.cache.clear();

try (FileInputStream input = new FileInputStream(cacheFile)) {
byte[] bytes = input.readAllBytes();
ByteBuffer buffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
while (buffer.hasRemaining()) {
long lastUsed = buffer.getLong();
long lsb = buffer.getLong();
long msb = buffer.getLong();
int stringLen = buffer.getInt();
byte[] stringBytes = new byte[stringLen];
buffer.get(stringBytes);
String name = new String(stringBytes, StandardCharsets.UTF_8);
UUID uuid = new UUID(msb, lsb);
CacheWrapper wrapper = new CacheWrapper(uuid, name, lastUsed);
this.cache.put(wrapper.getUuid(), wrapper);
}
this.plugin.getLogger()
.info(String.format("Loaded %d cached uuid name entries", this.cache.size()));
} catch (IOException ex) {
ex.printStackTrace();
this.plugin.getLogger().warning("Failed to load the uuid cache!");
}
}

public void saveCache() {
if (this.cacheFile != null) {
saveCache(this.cacheFile);
}
}

public void saveCacheAsync() {
if (this.cacheFile != null) {
saveCacheAsync(this.cacheFile);
}
}


/**
* Save the cache to the file
*/
public void saveCache(@Nonnull File cacheFile) {
List<CacheWrapper> copy = this.cache.values().stream().map(CacheWrapper::new).toList();
try {
// Ensure parent directory exists
Path parentDir = cacheFile.toPath().getParent();
if (parentDir != null) { // parent might be null if cacheFile is relative with no parent
Files.createDirectories(parentDir);
}
byte[] bytes = serialize(copy);
try (FileOutputStream fos = new FileOutputStream(cacheFile)) {
fos.write(bytes);
}
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to save cache.json", e);
}
}

public void saveCacheAsync(@Nonnull File cacheFile) {
List<CacheWrapper> copy = this.cache.values().stream().map(CacheWrapper::new).toList();
CompletableFuture.runAsync(() -> {
try {
// Ensure parent directory exists
Path parentDir = cacheFile.toPath().getParent();
if (parentDir != null) { // parent might be null if cacheFile is relative with no parent
Files.createDirectories(parentDir);
}
byte[] bytes = serialize(copy);
try (FileOutputStream fos = new FileOutputStream(cacheFile)) {
fos.write(bytes);
}
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to save cache.json", e);
}
});
}

private byte[] serialize(List<CacheWrapper> wrappers) {
ByteArrayOutputStream bos = new ByteArrayOutputStream(256 * wrappers.size());
for (CacheWrapper wrapper : wrappers) {
ByteBuffer buffer = ByteBuffer.allocate(256);
byte[] stringBytes = wrapper.getName().getBytes(StandardCharsets.UTF_8);
int stringLen = stringBytes.length;
long lsb = wrapper.getUuid().getLeastSignificantBits();
long msb = wrapper.getUuid().getMostSignificantBits();
long lastUsed = wrapper.getLastUsed();
buffer.putLong(lastUsed);
buffer.putLong(lsb);
buffer.putLong(msb);
buffer.putInt(stringLen);
buffer.put(stringBytes);

buffer.flip();

bos.write(buffer.array(), buffer.position(), buffer.remaining());
}
return bos.toByteArray();
}

/**
* Get a cache entry
*/
public CacheWrapper get(UUID uuid) {
return cache.get(uuid);
}

public CacheWrapper computeIfAbsent(@Nonnull UUID uuid, Function<UUID, CacheWrapper> function) {
return this.cache.computeIfAbsent(uuid, function);
}

/**
* Put a cache entry
*/
public void put(UUID uuid, CacheWrapper wrapper) {
cache.put(uuid, wrapper);
}

/**
* Remove a cache entry
*/
public CacheWrapper remove(UUID uuid) {
return cache.remove(uuid);
}

/**
* Check if cache contains a UUID
*/
public boolean contains(UUID uuid) {
return cache.containsKey(uuid);
}

/**
* Get all cache entries
*/
public Map<UUID, CacheWrapper> getAllEntries() {
return new HashMap<>(cache);
}

/**
* Clear all cache entries
*/
public void clear() {
cache.clear();
}

/**
* Get the size of the cache
*/
public int size() {
return cache.size();
}

public void trimCache() {
if (this.expiryDuration != null) {
trimCache(this.expiryDuration);
}
}

/**
* Trim the cache by removing entries older than 1 week
*/
public void trimCache(@Nonnull Duration expiryDuration) {
long expiryMillis = expiryDuration.toMillis();
long currentTime = System.currentTimeMillis();

Iterator<Map.Entry<UUID, CacheWrapper>> iterator = cache.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<UUID, CacheWrapper> entry = iterator.next();
CacheWrapper wrapper = entry.getValue();

if (wrapper.getLastUsed() != -1 && (currentTime - wrapper.getLastUsed()) >= expiryMillis) {
iterator.remove();
}
}

}
}
Loading
Loading