Bridge TrueOG Purpur's RocksDB player data API for other plugins (1.7.8)
This commit is contained in:
1 parent
87919ebf0e
commit
a9e74edc93
8 files changed
+608
-154
No files matched your search
@@ -1,3 +1,11 @@
|
||||
**1.7.8**
|
||||
- Utilities-OG is now the single bridge to the TrueOG Purpur RocksDB player data storage. New world scoped API with raw NMS tags as `Object`: `getPlayerData(world, uuid)`, `savePlayerData(world, uuid, tag)`, `savePlayerData(uuid, perWorld)` (atomic), `hasPlayerData(world, uuid)`, `getSeenPlayers(world)`, `dropWorldPlayerData(world)`, `copyWorldPlayerData(from, to)`, plus `isPlayerDataApiAvailable()`. MyWorlds uses it for per-world inventories.
|
||||
- The bridge is plain reflection into the server. Paperweight, the dev bundle, reobfuscation and the `v1_19_R3` pin are gone from the build.
|
||||
- Inventory helpers convert items with Item-NBT-API (now a hard `depend`, so the helpers always work when the fork API is present): `setInventoryData` returns `boolean` and writes nothing for a player without a stored profile or when the server refuses the write. `getInventoryData` and `getHeldItemSlot` return empty results instead of throwing for such players. Added `isPlayerInventoryApiAvailable()`.
|
||||
- Added `flushPlayerData()` (durable WAL flush, needs the matching Purpur `PlayerDataApi.flush()`; false on older forks) and per-player `lockPlayerData`/`unlockPlayerData`. `setInventoryData` now holds that lock so an offline edit and a MyWorlds save cannot interleave their read-modify-writes.
|
||||
- Removed the unused `hasPlayerData(UUID)` single-arg overload; use `hasPlayerData(world, uuid)`.
|
||||
- Added a `RegisterPlaceholders` config toggle (default true) to disable the built-in MiniPlaceholders (`servers_name`, `player_display_name`); the placeholder API for other plugins is unaffected.
|
||||
|
||||
**1.7.5**
|
||||
- Added `/ah` and `/auction` stubs explaining that TrueOG has no auction house, only the physical player market in the warzone.
|
||||
- Added `/info` stub that forwards to `/help` with the same arguments.
|
||||
|
||||
@@ -10,9 +10,7 @@ Current Target: Purpur 1.19.4
|
||||
|
||||
The resulting .jar file will be in build/libs
|
||||
|
||||
At the moment, any config change requires a server restart to take effect.
|
||||
|
||||
MiniPlaceholders support is always enabled. There is no config toggle for disabling the built-in placeholder registration.
|
||||
Any config change requires a server restart to take effect.
|
||||
|
||||
## Features:
|
||||
|
||||
@@ -29,7 +27,7 @@ MiniPlaceholders support is always enabled. There is no config toggle for disabl
|
||||
|
||||
**Warning:**
|
||||
|
||||
* **DisableEntityCramming (Default: false):** Disables entity cramming damage server-wide. This can cause lag if no other plugin manages entity limits. Enable this Module only if you have a separate plugin managing entity limitations.
|
||||
* **DisableEntityCramming (Default: true):** Disables entity cramming damage server-wide. This can cause lag if no other plugin manages entity limits. Keep this module enabled only if you have a separate plugin managing entity limitations.
|
||||
|
||||
## Permissions:
|
||||
|
||||
@@ -620,9 +618,57 @@ Java:
|
||||
UtilitiesOG.logToConsole("[MyPlugin-OG]", "&6This is a &*message with <green>color codes!");
|
||||
```
|
||||
|
||||
**Player data (TrueOG Purpur RocksDB storage)**
|
||||
|
||||
Utilities-OG is the only bridge to the server's RocksDB player data. Profiles are the raw NMS `NBTTagCompound`, passed as `Object`. Wrap them with Item-NBT-API `NBTContainer(Object)` or BKCommonLib `CommonTagCompound.create(Object)`. A null world, or the server's own level name, is the default storage that vanilla reads and writes. Any other world name is that world's own storage, created on first save. MyWorlds keeps its per-world inventory groups there. Every method throws `UnsupportedOperationException` on a server without the TrueOG Purpur patch.
|
||||
|
||||
**[boolean] isPlayerDataApiAvailable()**
|
||||
|
||||
True on a TrueOG Purpur server.
|
||||
|
||||
**[Object] getPlayerData(String world, UUID uuid)**
|
||||
|
||||
Profile of the player on that world, or null when there is none. Throws `IllegalStateException` when the storage failed or the stored profile can not be read, never a null in that case, so treat the exception as "do not touch this player".
|
||||
|
||||
**[boolean] savePlayerData(String world, UUID uuid, Object tag)**
|
||||
|
||||
Stores a profile. False when the server refused the write, for example while player data saving is disabled.
|
||||
|
||||
**[boolean] savePlayerData(UUID uuid, Map<String, Object> perWorld)**
|
||||
|
||||
Stores one player's profiles on several worlds as a single atomic write. Use a `HashMap`, a null key is the default storage.
|
||||
|
||||
**[boolean] hasPlayerData(String world, UUID uuid)**
|
||||
|
||||
True when a stored profile exists on that world (a null world is the default storage). Players who never joined have none. Throws `IllegalStateException` when the storage failed.
|
||||
|
||||
**[List<UUID>] getSeenPlayers(String world)**
|
||||
|
||||
Every player with a profile on that world. Empty for an unknown world.
|
||||
|
||||
**[boolean] dropWorldPlayerData(String world)**
|
||||
|
||||
Deletes a world's own storage. The default storage and the main worlds (the level, its nether and its end) can not be dropped; false is returned.
|
||||
|
||||
**[int] copyWorldPlayerData(String from, String to)**
|
||||
|
||||
Copies every profile of one storage into another. Returns the count, 0 when nothing to do, -1 on failure or when the target is a main world.
|
||||
|
||||
**[boolean] flushPlayerData()**
|
||||
|
||||
Forces buffered writes to durable storage. False when the fork is too old to support it or the flush failed. Call it before moving a migration source aside.
|
||||
|
||||
**[void] lockPlayerData(UUID uuid)** / **[void] unlockPlayerData(UUID uuid)**
|
||||
|
||||
Hold this lock around any read-modify-write of one player's stored data (release it in a `finally`). `setInventoryData` and MyWorlds' saves use it so they never interleave.
|
||||
|
||||
**Offline inventory helpers**
|
||||
|
||||
Built on the default storage and Item-NBT-API (a hard dependency, so it is always present). `isPlayerInventoryApiAvailable()` reports whether the TrueOG Purpur player data API is bound; the helpers throw `UnsupportedOperationException` otherwise.
|
||||
|
||||
**[ItemStack[]] getInventoryData(UUID uuid)**
|
||||
|
||||
Gets the inventory contents of an offline player using the TrueOG Purpur player data API.
|
||||
Gets the stored inventory contents of a player. 41 slots in `PlayerInventory.getContents()` order (36 main, 4 armor, offhand). All null when the player has no stored profile. On a MyWorlds server this is the main inventory group.
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
@@ -634,9 +680,9 @@ Java:
|
||||
ItemStack[] inventoryData = UtilitiesOG.getInventoryData(uuid);
|
||||
```
|
||||
|
||||
**[void] setInventoryData(UUID uuid, ItemStack[] items)**
|
||||
**[boolean] setInventoryData(UUID uuid, ItemStack[] items)**
|
||||
|
||||
Sets the inventory contents of an offline player using the TrueOG Purpur player data API.
|
||||
Replaces the stored inventory contents of an offline player using the TrueOG Purpur player data API. Same 41 slot layout as `getInventoryData`. Returns false, and writes nothing, when the player has no stored profile or the server refused the write. Use the live `Player` inventory for online players.
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
@@ -650,7 +696,7 @@ UtilitiesOG.setInventoryData(uuid, inventoryData);
|
||||
|
||||
**[int] getHeldItemSlot(UUID uuid)**
|
||||
|
||||
Gets the held item slot of an offline player using the TrueOG Purpur player data API.
|
||||
Gets the held hotbar slot of a player's stored profile using the TrueOG Purpur player data API. 0 when the player has no stored profile.
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
|
||||
+5
-12
@@ -6,7 +6,6 @@ plugins {
|
||||
id("java-library") // Import Java Library plugin.
|
||||
id("com.diffplug.spotless") version "8.1.0" // Import Spotless plugin.
|
||||
id("com.gradleup.shadow") version "8.3.9" // Import Shadow plugin.
|
||||
id("io.papermc.paperweight.userdev") version "2.0.0-beta.17"
|
||||
id("checkstyle") // Import Checkstyle plugin.
|
||||
eclipse // Import Eclipse plugin.
|
||||
kotlin("jvm") version "2.1.21" // Import Kotlin JVM plugin.
|
||||
@@ -26,7 +25,7 @@ kotlin { jvmToolchain(17) }
|
||||
/* ----------------------------- Metadata ------------------------------ */
|
||||
group = "net.trueog.utilities-og" // Declare bundle identifier.
|
||||
|
||||
version = "1.7.5" // Declare plugin version (will be in .jar).
|
||||
version = "1.7.8" // Declare plugin version (will be in .jar).
|
||||
|
||||
val apiVersion = "1.19" // Declare minecraft server target version.
|
||||
|
||||
@@ -50,7 +49,7 @@ repositories {
|
||||
|
||||
/* ---------------------- Java project deps ---------------------------- */
|
||||
dependencies {
|
||||
paperweightDevelopmentBundle("org.purpurmc.purpur:dev-bundle:1.19.4-R0.1-SNAPSHOT")
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") // Import Purpur API.
|
||||
compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.8") // Import WorldGuard API.
|
||||
compileOnly("de.tr7zw:item-nbt-api-plugin:2.14.1") // Import NBT API.
|
||||
compileOnly("io.github.miniplaceholders:miniplaceholders-api:2.2.3") // Import MiniPlaceholders API.
|
||||
@@ -80,19 +79,13 @@ tasks.jar {
|
||||
|
||||
tasks.shadowJar {
|
||||
exclude("io.github.miniplaceholders.*") // Exclude the MiniPlaceholders package from being shadowed.
|
||||
archiveClassifier.set("dev") // Mojang-mapped shaded jar, consumed by reobfJar.
|
||||
destinationDirectory.set(intermediateJars)
|
||||
archiveClassifier.set("") // Only jar in build/libs.
|
||||
minimize()
|
||||
}
|
||||
|
||||
/* --------------------------- Reobfuscation --------------------------- */
|
||||
tasks.reobfJar {
|
||||
outputJar.set(layout.buildDirectory.file("libs/${project.name}-${project.version}.jar")) // Only jar in build/libs.
|
||||
}
|
||||
tasks.assemble { dependsOn(tasks.shadowJar) } // Assemble depends on the shaded jar.
|
||||
|
||||
tasks.assemble { dependsOn(tasks.reobfJar) } // Assemble depends on the reobfuscated jar.
|
||||
|
||||
tasks.build { dependsOn(tasks.spotlessApply, tasks.reobfJar) } // Build depends on spotless and reobf.
|
||||
tasks.build { dependsOn(tasks.spotlessApply, tasks.shadowJar) } // Build depends on spotless and shadow.
|
||||
|
||||
/* --------------------------- Javac opts ------------------------------- */
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
|
||||
@@ -4,17 +4,17 @@ package net.trueog.utilitiesog;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.trueog.utilitiesog.utils.InventoryNbtCodec;
|
||||
import net.trueog.utilitiesog.utils.PlayerDataUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.craftbukkit.v1_19_R3.inventory.CraftItemStack;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
@@ -116,48 +116,54 @@ public final class UtilitiesOG extends JavaPlugin {
|
||||
|
||||
}
|
||||
|
||||
// Registering a global MiniPlaceholder.
|
||||
registerGlobalPlaceholder("servers_name", () -> "&aTrue&cOG &eNetwork");
|
||||
// Built-in MiniPlaceholders; disable in config.yml to let another plugin own
|
||||
// these names instead
|
||||
if (this.getConfig().getBoolean("RegisterPlaceholders", true)) {
|
||||
|
||||
// Registering an Audience MiniPlaceholder.
|
||||
registerAudiencePlaceholder("player_display_name", (Player player) -> {
|
||||
// Registering a global MiniPlaceholder.
|
||||
registerGlobalPlaceholder("servers_name", () -> "&aTrue&cOG &eNetwork");
|
||||
|
||||
final LuckPerms luckPerms = LuckPermsProvider.get();
|
||||
final User user = luckPerms.getUserManager().getUser(player.getUniqueId());
|
||||
if (user == null) {
|
||||
// Registering an Audience MiniPlaceholder.
|
||||
registerAudiencePlaceholder("player_display_name", (Player player) -> {
|
||||
|
||||
getLogger().info("ERROR: MiniPlaceholder processing error. Player: " + player.getName()
|
||||
+ " has no User object in LuckPerms!");
|
||||
final LuckPerms luckPerms = LuckPermsProvider.get();
|
||||
final User user = luckPerms.getUserManager().getUser(player.getUniqueId());
|
||||
if (user == null) {
|
||||
|
||||
return player.getName();
|
||||
getLogger().info("ERROR: MiniPlaceholder processing error. Player: " + player.getName()
|
||||
+ " has no User object in LuckPerms!");
|
||||
|
||||
}
|
||||
return player.getName();
|
||||
|
||||
final String primaryGroup = user.getPrimaryGroup();
|
||||
final Group group = luckPerms.getGroupManager().getGroup(primaryGroup);
|
||||
if (group == null) {
|
||||
}
|
||||
|
||||
getLogger().info("ERROR: MiniPlaceholder processing error. User: " + player.getName()
|
||||
+ " has no Group assignment in LuckPerms!");
|
||||
final String primaryGroup = user.getPrimaryGroup();
|
||||
final Group group = luckPerms.getGroupManager().getGroup(primaryGroup);
|
||||
if (group == null) {
|
||||
|
||||
return player.getName();
|
||||
getLogger().info("ERROR: MiniPlaceholder processing error. User: " + player.getName()
|
||||
+ " has no Group assignment in LuckPerms!");
|
||||
|
||||
}
|
||||
return player.getName();
|
||||
|
||||
final String prefix = group.getNodes().stream().filter(node -> node instanceof PrefixNode).findFirst()
|
||||
.map(node -> ((PrefixNode) node).getMetaValue()).orElse(null);
|
||||
}
|
||||
|
||||
if (prefix == null) {
|
||||
final String prefix = group.getNodes().stream().filter(node -> node instanceof PrefixNode).findFirst()
|
||||
.map(node -> ((PrefixNode) node).getMetaValue()).orElse(null);
|
||||
|
||||
return player.getName();
|
||||
if (prefix == null) {
|
||||
|
||||
}
|
||||
return player.getName();
|
||||
|
||||
final String colorCode = prefix.replaceAll(".*\\](.*)", "$1");
|
||||
}
|
||||
|
||||
return "<luckperms_prefix>" + colorCode + " " + player.getName();
|
||||
final String colorCode = prefix.replaceAll(".*\\](.*)", "$1");
|
||||
|
||||
});
|
||||
return "<luckperms_prefix>" + colorCode + " " + player.getName();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (this.getConfig().getBoolean("MockBamboo")) {
|
||||
|
||||
@@ -502,82 +508,160 @@ public final class UtilitiesOG extends JavaPlugin {
|
||||
|
||||
}
|
||||
|
||||
// TrueOG Purpur RocksDB player data, tags are raw NMS compound objects.
|
||||
|
||||
public static boolean isPlayerDataApiAvailable() {
|
||||
|
||||
return playerDataUtils != null && playerDataUtils.isAvailable();
|
||||
|
||||
}
|
||||
|
||||
// Profile on a world, null when absent. Null world is the default storage.
|
||||
public static @Nullable Object getPlayerData(@Nullable String world, UUID uuid) {
|
||||
|
||||
return playerDataUtils.get(world, uuid);
|
||||
|
||||
}
|
||||
|
||||
// False when the server refused the write.
|
||||
public static boolean savePlayerData(@Nullable String world, UUID uuid, Object tag) {
|
||||
|
||||
return playerDataUtils.save(world, uuid, tag);
|
||||
|
||||
}
|
||||
|
||||
// Atomic write across worlds. Use a HashMap, a null key is the default.
|
||||
public static boolean savePlayerData(UUID uuid, Map<String, Object> perWorld) {
|
||||
|
||||
return playerDataUtils.saveAll(uuid, perWorld);
|
||||
|
||||
}
|
||||
|
||||
public static boolean hasPlayerData(@Nullable String world, UUID uuid) {
|
||||
|
||||
return playerDataUtils.has(world, uuid);
|
||||
|
||||
}
|
||||
|
||||
// Every player with a profile on a world. Empty for an unknown world.
|
||||
public static List<UUID> getSeenPlayers(@Nullable String world) {
|
||||
|
||||
return playerDataUtils.seen(world);
|
||||
|
||||
}
|
||||
|
||||
// Deletes a world's own storage. The default storage can not be dropped.
|
||||
public static boolean dropWorldPlayerData(String world) {
|
||||
|
||||
return playerDataUtils.drop(world);
|
||||
|
||||
}
|
||||
|
||||
// Copies one storage into another. Count, 0 when nothing, -1 on failure.
|
||||
public static int copyWorldPlayerData(@Nullable String from, String to) {
|
||||
|
||||
return playerDataUtils.copy(from, to);
|
||||
|
||||
}
|
||||
|
||||
// Forces buffered player data writes to durable storage. False when unsupported
|
||||
// or it failed.
|
||||
public static boolean flushPlayerData() {
|
||||
|
||||
return playerDataUtils != null && playerDataUtils.flush();
|
||||
|
||||
}
|
||||
|
||||
// Per player locks so an offline edit here and a MyWorlds save never interleave
|
||||
// their read-modify-writes.
|
||||
private static final ConcurrentHashMap<UUID, ReentrantLock> playerDataLocks = new ConcurrentHashMap<>();
|
||||
|
||||
// Hold this around any read-modify-write of one player's stored data, released
|
||||
// in a finally.
|
||||
public static void lockPlayerData(UUID uuid) {
|
||||
|
||||
playerDataLocks.computeIfAbsent(uuid, u -> new ReentrantLock()).lock();
|
||||
|
||||
}
|
||||
|
||||
public static void unlockPlayerData(UUID uuid) {
|
||||
|
||||
final ReentrantLock lock = playerDataLocks.get(uuid);
|
||||
if (lock != null && lock.isHeldByCurrentThread()) {
|
||||
|
||||
lock.unlock();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Offline inventory helpers on the default storage. Need Item-NBT-API.
|
||||
|
||||
public static boolean isPlayerInventoryApiAvailable() {
|
||||
|
||||
return playerDataUtils != null && playerDataUtils.isItemConversionAvailable();
|
||||
|
||||
}
|
||||
|
||||
private static void requireInventoryApi() {
|
||||
|
||||
if (!isPlayerInventoryApiAvailable()) {
|
||||
|
||||
throw new UnsupportedOperationException("Offline inventory helpers need TrueOG Purpur and Item-NBT-API");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 41 slots in getContents() order. All null without a stored profile.
|
||||
public static @Nullable ItemStack @NotNull [] getInventoryData(UUID uuid) {
|
||||
|
||||
final ListTag inventoryData = playerDataUtils.getPlayerData(uuid).getList("Inventory", Tag.TAG_COMPOUND);
|
||||
final ItemStack[] inventoryContents = new ItemStack[41];
|
||||
for (int i = 0; i < inventoryData.size(); i++) {
|
||||
|
||||
final CompoundTag slotTag = inventoryData.getCompound(i);
|
||||
final byte slot = slotTag.getByte("Slot");
|
||||
final ItemStack itemStack = CraftItemStack.asBukkitCopy(net.minecraft.world.item.ItemStack.of(slotTag));
|
||||
|
||||
if (slot >= 0 && slot <= 35) {
|
||||
|
||||
inventoryContents[slot] = itemStack;
|
||||
|
||||
} else if (slot >= 100 && slot <= 103) {
|
||||
|
||||
inventoryContents[slot - 64] = itemStack;
|
||||
|
||||
} else if (slot == -106) {
|
||||
|
||||
inventoryContents[40] = itemStack;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return inventoryContents;
|
||||
requireInventoryApi();
|
||||
final Object tag = playerDataUtils.get(null, uuid);
|
||||
return (tag == null) ? new ItemStack[41] : InventoryNbtCodec.read(tag);
|
||||
|
||||
}
|
||||
|
||||
public static void setInventoryData(UUID uuid, @Nullable ItemStack @NotNull [] items) {
|
||||
// Replaces the stored inventory. False if no profile or the write failed.
|
||||
public static boolean setInventoryData(UUID uuid, @Nullable ItemStack @NotNull [] items) {
|
||||
|
||||
final ListTag inventoryData = new ListTag();
|
||||
for (byte i = 0; i < items.length; i++) {
|
||||
requireInventoryApi();
|
||||
// Lock the whole read-modify-write so a concurrent save cannot land between get
|
||||
// and save
|
||||
lockPlayerData(uuid);
|
||||
try {
|
||||
|
||||
final ItemStack item = items[i];
|
||||
if (item == null) {
|
||||
final Object tag = playerDataUtils.get(null, uuid);
|
||||
if (tag == null) {
|
||||
|
||||
continue;
|
||||
logToConsole(PREFIX, "&cNo stored player data for " + uuid + ", inventory not written.");
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
final byte slot;
|
||||
if (i <= 35) {
|
||||
final boolean saved = playerDataUtils.save(null, uuid, InventoryNbtCodec.write(tag, items));
|
||||
if (!saved) {
|
||||
|
||||
slot = i;
|
||||
|
||||
} else if (i <= 39) {
|
||||
|
||||
slot = (byte) (i + 64);
|
||||
|
||||
} else if (i == 40) {
|
||||
|
||||
slot = -106;
|
||||
|
||||
} else {
|
||||
|
||||
continue;
|
||||
logToConsole(PREFIX, "&cThe server refused the player data write for " + uuid + ".");
|
||||
|
||||
}
|
||||
|
||||
CompoundTag slotTag = new CompoundTag();
|
||||
CraftItemStack.asNMSCopy(item).save(slotTag);
|
||||
slotTag.putByte("Slot", slot);
|
||||
return saved;
|
||||
|
||||
inventoryData.add(slotTag);
|
||||
} finally {
|
||||
|
||||
unlockPlayerData(uuid);
|
||||
|
||||
}
|
||||
|
||||
playerDataUtils.setInventoryData(uuid, inventoryData);
|
||||
|
||||
}
|
||||
|
||||
// Held hotbar slot of the stored profile, 0 without a stored profile.
|
||||
public static int getHeldItemSlot(UUID uuid) {
|
||||
|
||||
return playerDataUtils.getPlayerData(uuid).getInt("SelectedItemSlot");
|
||||
requireInventoryApi();
|
||||
final Object tag = playerDataUtils.get(null, uuid);
|
||||
return (tag == null) ? 0 : InventoryNbtCodec.heldItemSlot(tag);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// Authors: NotAlexNoyle, SKBotNL.
|
||||
package net.trueog.utilitiesog.utils;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import de.tr7zw.nbtapi.NBT;
|
||||
import de.tr7zw.nbtapi.NBTCompoundList;
|
||||
import de.tr7zw.nbtapi.NBTContainer;
|
||||
import de.tr7zw.nbtapi.iface.ReadWriteNBT;
|
||||
|
||||
// Profile Inventory list to and from Bukkit items, through Item-NBT-API.
|
||||
public final class InventoryNbtCodec {
|
||||
|
||||
// Kept apart from UtilitiesOG so that class loads without Item-NBT-API.
|
||||
private InventoryNbtCodec() {
|
||||
|
||||
}
|
||||
|
||||
// 41 slots in PlayerInventory.getContents() order
|
||||
public static ItemStack[] read(Object tag) {
|
||||
|
||||
final ItemStack[] inventoryContents = new ItemStack[41];
|
||||
final NBTContainer playerData = new NBTContainer(tag);
|
||||
for (ReadWriteNBT slotTag : playerData.getCompoundList("Inventory")) {
|
||||
|
||||
final int index = toContentsIndex(slotTag.getByte("Slot"));
|
||||
if (index >= 0) {
|
||||
|
||||
inventoryContents[index] = NBT.itemStackFromNBT(slotTag);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return inventoryContents;
|
||||
|
||||
}
|
||||
|
||||
// Replaces the Inventory list in place and returns the tag to save
|
||||
public static Object write(Object tag, ItemStack[] items) {
|
||||
|
||||
final NBTContainer playerData = new NBTContainer(tag);
|
||||
playerData.removeKey("Inventory");
|
||||
final NBTCompoundList inventoryData = playerData.getCompoundList("Inventory");
|
||||
for (int i = 0; i < items.length && i <= 40; i++) {
|
||||
|
||||
final ItemStack item = items[i];
|
||||
if (item == null || item.getType().isAir()) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
final ReadWriteNBT slotTag = inventoryData.addCompound(NBT.itemStackToNBT(item));
|
||||
slotTag.setByte("Slot", toNbtSlot(i));
|
||||
|
||||
}
|
||||
|
||||
return playerData.getCompound();
|
||||
|
||||
}
|
||||
|
||||
public static int heldItemSlot(Object tag) {
|
||||
|
||||
return new NBTContainer(tag).getInteger("SelectedItemSlot");
|
||||
|
||||
}
|
||||
|
||||
// Vanilla Slot byte to getContents() index, -1 for unexposed slots.
|
||||
private static int toContentsIndex(byte slot) {
|
||||
|
||||
if (slot >= 0 && slot <= 35) {
|
||||
|
||||
return slot;
|
||||
|
||||
} else if (slot >= 100 && slot <= 103) {
|
||||
|
||||
return slot - 64;
|
||||
|
||||
} else if (slot == -106) {
|
||||
|
||||
return 40;
|
||||
|
||||
}
|
||||
|
||||
return -1;
|
||||
|
||||
}
|
||||
|
||||
private static byte toNbtSlot(int index) {
|
||||
|
||||
if (index <= 35) {
|
||||
|
||||
return (byte) index;
|
||||
|
||||
} else if (index <= 39) {
|
||||
|
||||
return (byte) (index + 64);
|
||||
|
||||
}
|
||||
|
||||
return (byte) -106;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +1,293 @@
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// Authors: NotAlexNoyle, SKBotNL.
|
||||
package net.trueog.utilitiesog.utils;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.world.level.storage.PlayerDataStorage;
|
||||
import net.trueog.utilitiesog.Internal;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.craftbukkit.v1_19_R3.CraftServer;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class PlayerDataUtils {
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
private final PlayerDataStorage playerIo = ((CraftServer) Bukkit.getServer()).getServer().getPlayerList().playerIo;
|
||||
private Method getPlayerData;
|
||||
private Method saveCompoundTag;
|
||||
import net.trueog.utilitiesog.Internal;
|
||||
|
||||
// Reflection bridge to the fork's PlayerDataApi. Tags are raw NMS objects.
|
||||
public final class PlayerDataUtils {
|
||||
|
||||
private final Object api;
|
||||
private final Method getMethod;
|
||||
private final Method saveAllMethod;
|
||||
private final Method hasMethod;
|
||||
private final Method seenMethod;
|
||||
private final Method dropMethod;
|
||||
private final Method copyMethod;
|
||||
private final Method flushMethod;
|
||||
private final String unavailableReason;
|
||||
private final String levelName;
|
||||
|
||||
public PlayerDataUtils() {
|
||||
|
||||
final Logger logger = Internal.getPlugin().getLogger();
|
||||
Object boundApi = null;
|
||||
Method[] bound = new Method[6];
|
||||
Method flushLocal = null;
|
||||
String reason = null;
|
||||
try {
|
||||
|
||||
getPlayerData = playerIo.getClass().getDeclaredMethod("getPlayerData", UUID.class);
|
||||
saveCompoundTag = playerIo.getClass().getDeclaredMethod("saveCompoundTag", UUID.class, CompoundTag.class);
|
||||
final Object server = Bukkit.getServer();
|
||||
final Object playerList = server.getClass().getMethod("getHandle").invoke(server);
|
||||
final Object storage = findStorage(playerList);
|
||||
if (storage == null) {
|
||||
|
||||
reason = "Not running TrueOG Purpur jar";
|
||||
|
||||
} else {
|
||||
|
||||
boundApi = findApiMethod(storage.getClass()).invoke(storage);
|
||||
final Class<?> apiType = boundApi.getClass();
|
||||
bound[0] = apiType.getMethod("get", String.class, UUID.class);
|
||||
bound[1] = apiType.getMethod("saveAll", UUID.class, Map.class);
|
||||
bound[2] = apiType.getMethod("has", String.class, UUID.class);
|
||||
bound[3] = apiType.getMethod("seen", String.class);
|
||||
bound[4] = apiType.getMethod("drop", String.class);
|
||||
bound[5] = apiType.getMethod("copy", String.class, String.class);
|
||||
// Optional: older forks lack flush(), so its absence must not disable the whole
|
||||
// API
|
||||
try {
|
||||
|
||||
flushLocal = apiType.getMethod("flush");
|
||||
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (Throwable t) {
|
||||
|
||||
reason = "Failed to bind the TrueOG Purpur player data API";
|
||||
logger.log(Level.WARNING, reason, t);
|
||||
|
||||
}
|
||||
|
||||
final boolean ok = (reason == null);
|
||||
this.api = ok ? boundApi : null;
|
||||
this.getMethod = ok ? bound[0] : null;
|
||||
this.saveAllMethod = ok ? bound[1] : null;
|
||||
this.hasMethod = ok ? bound[2] : null;
|
||||
this.seenMethod = ok ? bound[3] : null;
|
||||
this.dropMethod = ok ? bound[4] : null;
|
||||
this.copyMethod = ok ? bound[5] : null;
|
||||
this.flushMethod = ok ? flushLocal : null;
|
||||
this.unavailableReason = reason;
|
||||
this.levelName = Bukkit.getWorlds().get(0).getName();
|
||||
if (reason != null) {
|
||||
|
||||
logger.warning(reason + ", player data APIs are unavailable");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Field names are obfuscated, so find the field whose type declares api().
|
||||
private static Object findStorage(Object playerList) throws IllegalAccessException {
|
||||
|
||||
for (Class<?> type = playerList.getClass(); type != null; type = type.getSuperclass()) {
|
||||
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
|
||||
if (Modifier.isStatic(field.getModifiers()) || findApiMethod(field.getType()) == null) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
field.setAccessible(true);
|
||||
return field.get(playerList);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
// Resolved on the class, so a hook subclass still binds.
|
||||
private static Method findApiMethod(Class<?> type) {
|
||||
|
||||
try {
|
||||
|
||||
final Method method = type.getMethod("api");
|
||||
return method.getReturnType().getSimpleName().equals("PlayerDataApi") ? method : null;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
|
||||
getPlayerData = null;
|
||||
saveCompoundTag = null;
|
||||
Internal.getPlugin().getLogger().warning("Not running TrueOG Purpur jar, player data APIs are unavailable");
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public CompoundTag getPlayerData(UUID uuid) {
|
||||
public boolean isAvailable() {
|
||||
|
||||
if (getPlayerData == null) {
|
||||
return api != null;
|
||||
|
||||
throw new UnsupportedOperationException("Not running TrueOG Purpur jar, player data APIs are unavailable");
|
||||
}
|
||||
|
||||
// Item-NBT-API is a hard dependency, so tags can always become Bukkit item
|
||||
// stacks when the fork API is bound.
|
||||
public boolean isItemConversionAvailable() {
|
||||
|
||||
return api != null;
|
||||
|
||||
}
|
||||
|
||||
private void requireAvailable() {
|
||||
|
||||
if (api == null) {
|
||||
|
||||
throw new UnsupportedOperationException(unavailableReason + ", player data APIs are unavailable");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// The main worlds never lose their storage: the level itself, its nether and
|
||||
// its end.
|
||||
private boolean isProtected(String world) {
|
||||
|
||||
return world != null && (world.equalsIgnoreCase(levelName) || world.equalsIgnoreCase(levelName + "_nether")
|
||||
|| world.equalsIgnoreCase(levelName + "_the_end"));
|
||||
|
||||
}
|
||||
|
||||
// The server's own level name maps to null here, the fork knows only null as
|
||||
// default.
|
||||
private String key(String world) {
|
||||
|
||||
return (world == null || world.equalsIgnoreCase(levelName)) ? null : world;
|
||||
|
||||
}
|
||||
|
||||
// Null world or the server's own level name is the default storage.
|
||||
public Object get(String world, UUID uuid) {
|
||||
|
||||
requireAvailable();
|
||||
return invoke(getMethod, key(world), uuid);
|
||||
|
||||
}
|
||||
|
||||
// A single write is a one entry batch, the fork only has saveAll.
|
||||
public boolean save(String world, UUID uuid, Object tag) {
|
||||
|
||||
return saveAll(uuid, Collections.singletonMap(world, tag));
|
||||
|
||||
}
|
||||
|
||||
// One atomic write across worlds. A null key is the default storage.
|
||||
public boolean saveAll(UUID uuid, Map<String, Object> perWorld) {
|
||||
|
||||
requireAvailable();
|
||||
final Map<String, Object> keyed = new HashMap<>(perWorld.size() * 2);
|
||||
for (Map.Entry<String, Object> entry : perWorld.entrySet()) {
|
||||
|
||||
keyed.put(key(entry.getKey()), entry.getValue());
|
||||
|
||||
}
|
||||
|
||||
if (keyed.size() != perWorld.size()) {
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"The batch names the default storage twice, as null and as the level name");
|
||||
|
||||
}
|
||||
|
||||
return Boolean.TRUE.equals(invoke(saveAllMethod, uuid, keyed));
|
||||
|
||||
}
|
||||
|
||||
public boolean has(String world, UUID uuid) {
|
||||
|
||||
requireAvailable();
|
||||
return Boolean.TRUE.equals(invoke(hasMethod, key(world), uuid));
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<UUID> seen(String world) {
|
||||
|
||||
requireAvailable();
|
||||
final Object result = invoke(seenMethod, key(world));
|
||||
return (result == null) ? Collections.<UUID>emptyList() : (List<UUID>) result;
|
||||
|
||||
}
|
||||
|
||||
// The default storage can not be dropped, so the level name is refused here.
|
||||
public boolean drop(String world) {
|
||||
|
||||
requireAvailable();
|
||||
if (world == null || isProtected(world)) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
return Boolean.TRUE.equals(invoke(dropMethod, world));
|
||||
|
||||
}
|
||||
|
||||
// The default storage can not be a copy target, -1 when the level name is
|
||||
// given.
|
||||
public int copy(String from, String to) {
|
||||
|
||||
requireAvailable();
|
||||
if (to == null || isProtected(to)) {
|
||||
|
||||
return -1;
|
||||
|
||||
}
|
||||
|
||||
final Object result = invoke(copyMethod, key(from), to);
|
||||
return (result == null) ? -1 : ((Integer) result).intValue();
|
||||
|
||||
}
|
||||
|
||||
// Forces buffered writes to durable storage. False when the fork is too old to
|
||||
// support it.
|
||||
public boolean flush() {
|
||||
|
||||
requireAvailable();
|
||||
return flushMethod != null && Boolean.TRUE.equals(invoke(flushMethod));
|
||||
|
||||
}
|
||||
|
||||
private Object invoke(Method method, Object... args) {
|
||||
|
||||
try {
|
||||
|
||||
return (CompoundTag) getPlayerData.invoke(playerIo, uuid);
|
||||
return method.invoke(api, args);
|
||||
|
||||
} catch (Exception e) {
|
||||
} catch (InvocationTargetException e) {
|
||||
|
||||
// The fork throws IllegalStateException when the storage failed, callers must
|
||||
// see that
|
||||
if (e.getCause() instanceof RuntimeException) {
|
||||
|
||||
throw (RuntimeException) e.getCause();
|
||||
|
||||
}
|
||||
|
||||
throw new RuntimeException("Player data API call " + method.getName() + " failed", e.getCause());
|
||||
|
||||
} catch (IllegalAccessException e) {
|
||||
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@@ -53,32 +295,4 @@ public class PlayerDataUtils {
|
||||
|
||||
}
|
||||
|
||||
private void saveCompoundTag(UUID uuid, CompoundTag tag) {
|
||||
|
||||
if (saveCompoundTag == null) {
|
||||
|
||||
throw new UnsupportedOperationException("Not running TrueOG Purpur jar, player data APIs are unavailable");
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
saveCompoundTag.invoke(playerIo, uuid, tag);
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
throw new RuntimeException(e);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setInventoryData(UUID uuid, ListTag inventoryData) {
|
||||
|
||||
final CompoundTag playerData = getPlayerData(uuid);
|
||||
playerData.put("Inventory", inventoryData);
|
||||
saveCompoundTag(uuid, playerData);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,4 +26,6 @@ RanksMenu: true
|
||||
# Should advancements be restricted to survival mode? Default: true.
|
||||
AdvancementsOnlyInSurvival: true
|
||||
# Registers stub redirect/denial commands (/wild, /rtp, /seed, /f, /faction, /guild, /kit, /ah, /auction, /info). Default: true.
|
||||
MissingCommands: true
|
||||
MissingCommands: true
|
||||
# Registers Utilities-OG's own MiniPlaceholders (servers_name, player_display_name). Default: true.
|
||||
RegisterPlaceholders: true
|
||||
@@ -3,7 +3,7 @@ authors: [ christianniehaus, NotAlexNoyle ]
|
||||
version: '${version}'
|
||||
api-version: '${apiVersion}'
|
||||
main: net.trueog.utilitiesog.UtilitiesOG
|
||||
depend: [ MiniPlaceholders, WorldGuard ]
|
||||
depend: [ MiniPlaceholders, WorldGuard, Item-NBT-API ]
|
||||
commands:
|
||||
utilities:
|
||||
usage: /utilites
|
||||
|
||||
Reference in new issue
Block a user