From 03acea6ba5b9bcfcd82b92dd034b0455626dd860 Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Sat, 8 Aug 2026 12:51:55 -0400 Subject: [PATCH] fix(stats): use compatible Bukkit registry keys --- .../us-004-capture-player-snapshots.md | 1 + .../stats/BukkitRegistryKeys.java | 17 +++++++++++++ .../stats/PlayerSnapshotCollector.java | 15 ++++++++---- .../stats/BukkitRegistryKeysTest.java | 24 +++++++++++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 src/main/java/games/dmg/spigotevents/stats/BukkitRegistryKeys.java create mode 100644 src/test/java/games/dmg/spigotevents/stats/BukkitRegistryKeysTest.java diff --git a/design/user-stories/us-004-capture-player-snapshots.md b/design/user-stories/us-004-capture-player-snapshots.md index 4f4764c..22d05e9 100644 --- a/design/user-stories/us-004-capture-player-snapshots.md +++ b/design/user-stories/us-004-capture-player-snapshots.md @@ -12,6 +12,7 @@ As a **server analyst**, I want periodic player location and statistic snapshots ## Acceptance criteria - [x] Each configured interval captures every online player's world identity, coordinates, orientation, game mode, and biome. +- [x] Registry-backed snapshot values use Bukkit's stable `Keyed` contract so collection remains compatible with the supported Purpur runtime. - [x] Every Bukkit untyped, block, item, and entity statistic combination accepted by Bukkit is captured, including zero values. - [x] Invalid statistic subtype combinations are skipped without stopping the snapshot. - [x] Statistic reads are spread across ticks according to a configurable operation budget and run through Bukkit scheduling. diff --git a/src/main/java/games/dmg/spigotevents/stats/BukkitRegistryKeys.java b/src/main/java/games/dmg/spigotevents/stats/BukkitRegistryKeys.java new file mode 100644 index 0000000..9d15aac --- /dev/null +++ b/src/main/java/games/dmg/spigotevents/stats/BukkitRegistryKeys.java @@ -0,0 +1,17 @@ +package games.dmg.spigotevents.stats; + +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; + +final class BukkitRegistryKeys { + private BukkitRegistryKeys() {} + + static NamespacedKey get(Keyed value) { + return value.getKey(); + } + + static String format(Keyed value, String fallback) { + NamespacedKey key = get(value); + return key == null ? fallback : key.toString(); + } +} diff --git a/src/main/java/games/dmg/spigotevents/stats/PlayerSnapshotCollector.java b/src/main/java/games/dmg/spigotevents/stats/PlayerSnapshotCollector.java index b0c2558..364437c 100644 --- a/src/main/java/games/dmg/spigotevents/stats/PlayerSnapshotCollector.java +++ b/src/main/java/games/dmg/spigotevents/stats/PlayerSnapshotCollector.java @@ -44,8 +44,7 @@ public final class PlayerSnapshotCollector { data.put("pitch", location.getPitch()); data.put("game_mode", player.getGameMode().name().toLowerCase()); var biome = location.getBlock().getBiome(); - var biomeKey = biome.getKeyOrNull(); - data.put("biome", biomeKey == null ? biome.toString() : biomeKey.toString()); + data.put("biome", BukkitRegistryKeys.format(biome, biome.toString())); publish("player.location", identity(player), data); } @@ -157,9 +156,13 @@ public final class PlayerSnapshotCollector { if ((blocksOnly && !material.isBlock()) || (!blocksOnly && !material.isItem())) { continue; } + var materialKey = BukkitRegistryKeys.get(material); + if (materialKey == null) { + continue; + } try { target.put( - statistic.getKey() + "/" + material.getKeyOrThrow(), + statistic.getKey() + "/" + materialKey, currentPlayer.getStatistic(statistic, material)); } catch (IllegalArgumentException ignored) { // Bukkit rejects combinations not represented by the game. @@ -180,9 +183,13 @@ public final class PlayerSnapshotCollector { while (operations < budget && subtypeIndex < ENTITY_TYPES.length) { EntityType entityType = ENTITY_TYPES[subtypeIndex++]; operations++; + var entityTypeKey = BukkitRegistryKeys.get(entityType); + if (entityTypeKey == null) { + continue; + } try { target.put( - statistic.getKey() + "/" + entityType.getKeyOrThrow(), + statistic.getKey() + "/" + entityTypeKey, currentPlayer.getStatistic(statistic, entityType)); } catch (IllegalArgumentException ignored) { // Bukkit rejects unsupported entity types. diff --git a/src/test/java/games/dmg/spigotevents/stats/BukkitRegistryKeysTest.java b/src/test/java/games/dmg/spigotevents/stats/BukkitRegistryKeysTest.java new file mode 100644 index 0000000..cdc5f2e --- /dev/null +++ b/src/test/java/games/dmg/spigotevents/stats/BukkitRegistryKeysTest.java @@ -0,0 +1,24 @@ +package games.dmg.spigotevents.stats; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.bukkit.NamespacedKey; +import org.bukkit.Keyed; +import org.junit.jupiter.api.Test; + +class BukkitRegistryKeysTest { + @Test + void formatsARegisteredKey() { + Keyed keyed = () -> NamespacedKey.minecraft("plains"); + + assertEquals("minecraft:plains", BukkitRegistryKeys.format(keyed, "fallback")); + } + + @Test + void reportsAnUnavailableKey() { + Keyed keyed = () -> null; + + assertNull(BukkitRegistryKeys.get(keyed)); + } +}