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
|
@Override
|
||||||
public synchronized void journal(UUID uuid, String playerName, String eventType, String detail) throws Exception {
|
public synchronized void journal(UUID uuid, String playerName, String eventType, String detail) throws Exception {
|
||||||
if (!journalEnabled) {
|
if (!journalEnabled) {
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ final class PlayerStatsListener implements Listener {
|
|||||||
@EventHandler(priority = EventPriority.MONITOR)
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
public void onJoin(PlayerJoinEvent event) {
|
public void onJoin(PlayerJoinEvent event) {
|
||||||
Player player = event.getPlayer();
|
Player player = event.getPlayer();
|
||||||
|
if (stats.isNpc(player)) {
|
||||||
|
stats.purgeNpcAsync(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
stats.record(player);
|
stats.record(player);
|
||||||
stats.increment(player, "sessions.joins");
|
stats.increment(player, "sessions.joins");
|
||||||
sessionStarts.put(player.getUniqueId(), System.currentTimeMillis());
|
sessionStarts.put(player.getUniqueId(), System.currentTimeMillis());
|
||||||
@@ -105,6 +109,11 @@ final class PlayerStatsListener implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void finishSession(Player player, String eventType) {
|
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());
|
Long started = sessionStarts.remove(player.getUniqueId());
|
||||||
if (started != null) {
|
if (started != null) {
|
||||||
stats.increment(player, "sessions.play_time_ms", Math.max(0L, System.currentTimeMillis() - started));
|
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.nio.file.Path;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.command.PluginCommand;
|
import org.bukkit.command.PluginCommand;
|
||||||
@@ -44,6 +45,10 @@ public final class RosePlayerStatsPlugin extends JavaPlugin {
|
|||||||
Bukkit.getScheduler().runTaskTimerAsynchronously(this, stats::flushJournal, 20L, 20L);
|
Bukkit.getScheduler().runTaskTimerAsynchronously(this, stats::flushJournal, 20L, 20L);
|
||||||
|
|
||||||
for (var player : Bukkit.getOnlinePlayers()) {
|
for (var player : Bukkit.getOnlinePlayers()) {
|
||||||
|
if (stats.isNpc(player)) {
|
||||||
|
stats.purgeNpcAsync(player);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
stats.preload(player.getUniqueId(), player.getName());
|
stats.preload(player.getUniqueId(), player.getName());
|
||||||
} catch (Exception ex) {
|
} 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")
|
getLogger().info("RosePlayerStats enabled using " + getConfig().getString("storage.type", "flatfile")
|
||||||
+ " storage. Player name and UUID are recorded with statistics and journal events.");
|
+ " 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
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
if (stats != null) {
|
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
|
@Override
|
||||||
public synchronized void journal(UUID uuid, String playerName, String eventType, String detail) throws Exception {
|
public synchronized void journal(UUID uuid, String playerName, String eventType, String detail) throws Exception {
|
||||||
if (!journalEnabled) {
|
if (!journalEnabled) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
@@ -16,6 +17,7 @@ final class StatsService {
|
|||||||
private final StatsStorage storage;
|
private final StatsStorage storage;
|
||||||
private final ConcurrentHashMap<UUID, PlayerRecord> records = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<UUID, PlayerRecord> records = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentLinkedQueue<JournalEntry> journalQueue = new ConcurrentLinkedQueue<>();
|
private final ConcurrentLinkedQueue<JournalEntry> journalQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
private final Set<UUID> npcUuids = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
StatsService(RosePlayerStatsPlugin plugin, StatsStorage storage) {
|
StatsService(RosePlayerStatsPlugin plugin, StatsStorage storage) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
@@ -27,6 +29,11 @@ final class StatsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PlayerRecord record(Player player) {
|
PlayerRecord record(Player player) {
|
||||||
|
if (isNpc(player)) {
|
||||||
|
purgeNpcAsync(player);
|
||||||
|
return new PlayerRecord(player.getUniqueId(), player.getName(), System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
PlayerRecord record = records.computeIfAbsent(player.getUniqueId(),
|
PlayerRecord record = records.computeIfAbsent(player.getUniqueId(),
|
||||||
id -> new PlayerRecord(id, player.getName(), now));
|
id -> new PlayerRecord(id, player.getName(), now));
|
||||||
@@ -62,14 +69,26 @@ final class StatsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void increment(Player player, String key, long amount) {
|
void increment(Player player, String key, long amount) {
|
||||||
|
if (isNpc(player)) {
|
||||||
|
purgeNpcAsync(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
record(player).increment(normalizeKey(key), amount);
|
record(player).increment(normalizeKey(key), amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void set(Player player, String key, long value) {
|
void set(Player player, String key, long value) {
|
||||||
|
if (isNpc(player)) {
|
||||||
|
purgeNpcAsync(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
record(player).set(normalizeKey(key), value);
|
record(player).set(normalizeKey(key), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void journal(Player player, String eventType, String detail) {
|
void journal(Player player, String eventType, String detail) {
|
||||||
|
if (isNpc(player)) {
|
||||||
|
purgeNpcAsync(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!plugin.getConfig().getBoolean("journal.enabled", true)) {
|
if (!plugin.getConfig().getBoolean("journal.enabled", true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,10 +131,49 @@ final class StatsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void savePlayerAsync(Player player) {
|
void savePlayerAsync(Player player) {
|
||||||
|
if (isNpc(player)) {
|
||||||
|
purgeNpcAsync(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
PlayerRecord record = record(player);
|
PlayerRecord record = record(player);
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> save(record));
|
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() {
|
Collection<PlayerRecord> records() {
|
||||||
return records.values();
|
return records.values();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ interface StatsStorage extends Closeable {
|
|||||||
|
|
||||||
void save(PlayerRecord record) throws Exception;
|
void save(PlayerRecord record) throws Exception;
|
||||||
|
|
||||||
|
void delete(UUID uuid) throws Exception;
|
||||||
|
|
||||||
default void saveAll(Collection<PlayerRecord> records) throws Exception {
|
default void saveAll(Collection<PlayerRecord> records) throws Exception {
|
||||||
for (PlayerRecord record : records) {
|
for (PlayerRecord record : records) {
|
||||||
save(record);
|
save(record);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ version: '${version}'
|
|||||||
main: net.therosegarden.playerstats.RosePlayerStatsPlugin
|
main: net.therosegarden.playerstats.RosePlayerStatsPlugin
|
||||||
api-version: '26.2'
|
api-version: '26.2'
|
||||||
description: Comprehensive player statistics and event journaling for The Rose Garden.
|
description: Comprehensive player statistics and event journaling for The Rose Garden.
|
||||||
|
softdepend: [Citizens]
|
||||||
commands:
|
commands:
|
||||||
playerstats:
|
playerstats:
|
||||||
description: View or manage RosePlayerStats data.
|
description: View or manage RosePlayerStats data.
|
||||||
|
|||||||
Reference in New Issue
Block a user