Exclude NPCs from player statistics
This commit is contained in:
@@ -60,6 +60,12 @@ final class FlatFileStatsStorage implements StatsStorage {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(UUID uuid) throws Exception {
|
||||
Files.deleteIfExists(playersDirectory.resolve(uuid + ".properties"));
|
||||
Files.deleteIfExists(playersDirectory.resolve(uuid + ".properties.tmp"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void journal(UUID uuid, String playerName, String eventType, String detail) throws Exception {
|
||||
if (!journalEnabled) {
|
||||
|
||||
@@ -86,6 +86,10 @@ final class PlayerStatsListener implements Listener {
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (stats.isNpc(player)) {
|
||||
stats.purgeNpcAsync(player);
|
||||
return;
|
||||
}
|
||||
stats.record(player);
|
||||
stats.increment(player, "sessions.joins");
|
||||
sessionStarts.put(player.getUniqueId(), System.currentTimeMillis());
|
||||
@@ -105,6 +109,11 @@ final class PlayerStatsListener implements Listener {
|
||||
}
|
||||
|
||||
private void finishSession(Player player, String eventType) {
|
||||
if (stats.isNpc(player)) {
|
||||
sessionStarts.remove(player.getUniqueId());
|
||||
stats.purgeNpcAsync(player);
|
||||
return;
|
||||
}
|
||||
Long started = sessionStarts.remove(player.getUniqueId());
|
||||
if (started != null) {
|
||||
stats.increment(player, "sessions.play_time_ms", Math.max(0L, System.currentTimeMillis() - started));
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.therosegarden.playerstats;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
@@ -44,6 +45,10 @@ public final class RosePlayerStatsPlugin extends JavaPlugin {
|
||||
Bukkit.getScheduler().runTaskTimerAsynchronously(this, stats::flushJournal, 20L, 20L);
|
||||
|
||||
for (var player : Bukkit.getOnlinePlayers()) {
|
||||
if (stats.isNpc(player)) {
|
||||
stats.purgeNpcAsync(player);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
stats.preload(player.getUniqueId(), player.getName());
|
||||
} catch (Exception ex) {
|
||||
@@ -52,10 +57,51 @@ public final class RosePlayerStatsPlugin extends JavaPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> {
|
||||
purgeCitizensNpcData();
|
||||
for (var world : Bukkit.getWorlds()) {
|
||||
for (var player : world.getEntitiesByClass(org.bukkit.entity.Player.class)) {
|
||||
if (stats.isNpc(player)) {
|
||||
stats.purgeNpcAsync(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 40L);
|
||||
|
||||
getLogger().info("RosePlayerStats enabled using " + getConfig().getString("storage.type", "flatfile")
|
||||
+ " storage. Player name and UUID are recorded with statistics and journal events.");
|
||||
}
|
||||
|
||||
private void purgeCitizensNpcData() {
|
||||
var citizens = Bukkit.getPluginManager().getPlugin("Citizens");
|
||||
if (citizens == null || !citizens.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Class<?> citizensApi = Class.forName("net.citizensnpcs.api.CitizensAPI");
|
||||
Object registry = citizensApi.getMethod("getNPCRegistry").invoke(null);
|
||||
if (!(registry instanceof Iterable<?> npcs)) {
|
||||
getLogger().warning("Citizens NPC registry was not iterable; existing NPC statistics could not be purged by registry.");
|
||||
return;
|
||||
}
|
||||
|
||||
Class<?> npcType = Class.forName("net.citizensnpcs.api.npc.NPC");
|
||||
var getUniqueId = npcType.getMethod("getUniqueId");
|
||||
var getName = npcType.getMethod("getName");
|
||||
|
||||
for (Object npc : npcs) {
|
||||
Object uuidValue = getUniqueId.invoke(npc);
|
||||
if (uuidValue instanceof UUID uuid) {
|
||||
Object nameValue = getName.invoke(npc);
|
||||
stats.purgeNpcAsync(uuid, nameValue == null ? "unknown" : nameValue.toString());
|
||||
}
|
||||
}
|
||||
} catch (ReflectiveOperationException | LinkageError ex) {
|
||||
getLogger().warning("Could not enumerate Citizens NPCs for statistics cleanup: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (stats != null) {
|
||||
|
||||
@@ -191,6 +191,35 @@ final class SqlStatsStorage implements StatsStorage {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void delete(UUID uuid) throws Exception {
|
||||
Connection db = ensureConnection();
|
||||
boolean previousAutoCommit = db.getAutoCommit();
|
||||
db.setAutoCommit(false);
|
||||
try {
|
||||
try (PreparedStatement statement = db.prepareStatement(
|
||||
"DELETE FROM rose_player_events WHERE uuid=?")) {
|
||||
statement.setString(1, uuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
try (PreparedStatement statement = db.prepareStatement(
|
||||
"DELETE FROM rose_player_stats WHERE uuid=?")) {
|
||||
statement.setString(1, uuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
try (PreparedStatement statement = db.prepareStatement(
|
||||
"DELETE FROM rose_players WHERE uuid=?")) {
|
||||
statement.setString(1, uuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
db.commit();
|
||||
} catch (SQLException ex) {
|
||||
db.rollback();
|
||||
throw ex;
|
||||
} finally {
|
||||
db.setAutoCommit(previousAutoCommit);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public synchronized void journal(UUID uuid, String playerName, String eventType, String detail) throws Exception {
|
||||
if (!journalEnabled) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
@@ -16,6 +17,7 @@ final class StatsService {
|
||||
private final StatsStorage storage;
|
||||
private final ConcurrentHashMap<UUID, PlayerRecord> records = new ConcurrentHashMap<>();
|
||||
private final ConcurrentLinkedQueue<JournalEntry> journalQueue = new ConcurrentLinkedQueue<>();
|
||||
private final Set<UUID> npcUuids = ConcurrentHashMap.newKeySet();
|
||||
|
||||
StatsService(RosePlayerStatsPlugin plugin, StatsStorage storage) {
|
||||
this.plugin = plugin;
|
||||
@@ -27,6 +29,11 @@ final class StatsService {
|
||||
}
|
||||
|
||||
PlayerRecord record(Player player) {
|
||||
if (isNpc(player)) {
|
||||
purgeNpcAsync(player);
|
||||
return new PlayerRecord(player.getUniqueId(), player.getName(), System.currentTimeMillis());
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
PlayerRecord record = records.computeIfAbsent(player.getUniqueId(),
|
||||
id -> new PlayerRecord(id, player.getName(), now));
|
||||
@@ -62,14 +69,26 @@ final class StatsService {
|
||||
}
|
||||
|
||||
void increment(Player player, String key, long amount) {
|
||||
if (isNpc(player)) {
|
||||
purgeNpcAsync(player);
|
||||
return;
|
||||
}
|
||||
record(player).increment(normalizeKey(key), amount);
|
||||
}
|
||||
|
||||
void set(Player player, String key, long value) {
|
||||
if (isNpc(player)) {
|
||||
purgeNpcAsync(player);
|
||||
return;
|
||||
}
|
||||
record(player).set(normalizeKey(key), value);
|
||||
}
|
||||
|
||||
void journal(Player player, String eventType, String detail) {
|
||||
if (isNpc(player)) {
|
||||
purgeNpcAsync(player);
|
||||
return;
|
||||
}
|
||||
if (!plugin.getConfig().getBoolean("journal.enabled", true)) {
|
||||
return;
|
||||
}
|
||||
@@ -112,10 +131,49 @@ final class StatsService {
|
||||
}
|
||||
|
||||
void savePlayerAsync(Player player) {
|
||||
if (isNpc(player)) {
|
||||
purgeNpcAsync(player);
|
||||
return;
|
||||
}
|
||||
PlayerRecord record = record(player);
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> save(record));
|
||||
}
|
||||
|
||||
boolean isNpc(Player player) {
|
||||
return player != null && player.hasMetadata("NPC");
|
||||
}
|
||||
|
||||
void purgeNpcAsync(Player player) {
|
||||
if (player == null || !isNpc(player)) {
|
||||
return;
|
||||
}
|
||||
purgeNpcAsync(player.getUniqueId(), player.getName());
|
||||
}
|
||||
|
||||
void purgeNpcAsync(UUID uuid, String playerName) {
|
||||
if (uuid == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
records.remove(uuid);
|
||||
journalQueue.removeIf(entry -> entry.uuid().equals(uuid));
|
||||
|
||||
if (!npcUuids.add(uuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String displayName = playerName == null || playerName.isBlank() ? "unknown" : playerName;
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
storage.delete(uuid);
|
||||
plugin.getLogger().info("Removed NPC from player statistics: " + displayName + " (" + uuid + ")");
|
||||
} catch (Exception ex) {
|
||||
npcUuids.remove(uuid);
|
||||
plugin.getLogger().warning("Failed removing NPC statistics for " + displayName + " (" + uuid + "): " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Collection<PlayerRecord> records() {
|
||||
return records.values();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ interface StatsStorage extends Closeable {
|
||||
|
||||
void save(PlayerRecord record) throws Exception;
|
||||
|
||||
void delete(UUID uuid) throws Exception;
|
||||
|
||||
default void saveAll(Collection<PlayerRecord> records) throws Exception {
|
||||
for (PlayerRecord record : records) {
|
||||
save(record);
|
||||
|
||||
@@ -3,6 +3,7 @@ version: '${version}'
|
||||
main: net.therosegarden.playerstats.RosePlayerStatsPlugin
|
||||
api-version: '26.2'
|
||||
description: Comprehensive player statistics and event journaling for The Rose Garden.
|
||||
softdepend: [Citizens]
|
||||
commands:
|
||||
playerstats:
|
||||
description: View or manage RosePlayerStats data.
|
||||
|
||||
Reference in New Issue
Block a user