Add player data API and remove FastOfflinePlayer #5

Merged
SKBotNL merged 3 commits from main into main 2026-09-16 08:09:25 -04:00
9 changed files with 195 additions and 1167 deletions

No files matched your search

+42
View File
@@ -619,3 +619,45 @@ Java:
```java
UtilitiesOG.logToConsole("[MyPlugin-OG]", "&6This is a &*message with <green>color codes!");
```
**[ItemStack[]] getInventoryData(UUID uuid)**
Gets the inventory contents of an offline player using the TrueOG Purpur player data API.
Kotlin:
```kotlin
val inventoryData = UtilitiesOG.getInventoryData(uuid)
```
Java:
```java
ItemStack[] inventoryData = UtilitiesOG.getInventoryData(uuid);
```
**[void] setInventoryData(UUID uuid, ItemStack[] items)**
Sets the inventory contents of an offline player using the TrueOG Purpur player data API.
Kotlin:
```kotlin
UtilitiesOG.setInventoryData(uuid, inventoryData)
```
Java:
```java
UtilitiesOG.setInventoryData(uuid, inventoryData);
```
**[int] getHeldItemSlot(UUID uuid)**
Gets the held item slot of an offline player using the TrueOG Purpur player data API.
Kotlin:
```kotlin
val heldItemSlot = UtilitiesOG.getHeldItemSlot(uuid)
```
Java:
```java
int heldItemSlot = UtilitiesOG.getHeldItemSlot(uuid);
```
+4 -2
View File
@@ -6,6 +6,7 @@ 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.
@@ -49,14 +50,13 @@ repositories {
/* ---------------------- Java project deps ---------------------------- */
dependencies {
compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") // Declare Purpur API version to be packaged.
paperweightDevelopmentBundle("org.purpurmc.purpur:dev-bundle:1.19.4-R0.1-SNAPSHOT")
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.
compileOnly("net.luckperms:api:5.4") // Import LuckPerms API.
implementation(kotlin("stdlib")) // Import and package Kotlin standard library.
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") // Import and package Kotlin async library.
implementation("com.github.jasync-sql:jasync-postgresql:2.2.4") // Import and package Jasync API.
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3") // Add JUnit API to testing environment.
testImplementation("org.mockito:mockito-core:5.18.0") // Add Mockito API to testing environment.
testImplementation("com.github.seeseemelk:MockBukkit-v1.19:2.29.0") // Add MockBukkit API to testing environment.
@@ -79,6 +79,8 @@ tasks.shadowJar {
tasks.jar { archiveClassifier.set("part") } // Applies to root jarfile only.
tasks.assemble { dependsOn(tasks.reobfJar) }
tasks.build { dependsOn(tasks.spotlessApply, tasks.shadowJar) } // Build depends on spotless and shadow.
/* --------------------------- Javac opts ------------------------------- */
@@ -1,568 +0,0 @@
// This is free and unencumbered software released into the public domain.
// Authors: NotAlexNoyle.
package net.trueog.utilitiesog;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.Statistic;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.destroystokyo.paper.profile.PlayerProfile;
import com.github.jasync.sql.db.Connection;
// FAST async Postgres and cache based re-implementation of Bukkit's OfflinePlayer.
// Designed for 1.19.4, OfflinePlayer behavior may be different on other versions.
// TODO: Add in-game integration testing.
public class FastOfflinePlayer implements OfflinePlayer {
// Container for player UUID (always available).
private final UUID uuid;
// Container for player username (requires lookup).
private String cachedName;
// Container for safe concurrent updates to async futures.
private final AtomicReference<CompletableFuture<String>> lookup = new AtomicReference<>();
// Fake Postgres connection without using UtilitiesOG.java.
private static volatile Supplier<Connection> connectionSupplier;
// Test-only supplier hook.
static void overrideConnectionSupplierForTests(Supplier<Connection> supplier) {
// Enable the test supplier.
connectionSupplier = supplier;
}
// Supply a connection to jasync Postgres.
private Connection connection() {
// If a test supplier is available, do this...
final Supplier<Connection> s = connectionSupplier;
return s != null ? s.get() : Internal.getPostgres();
}
// Runs after each individual unit test.
static void resetConnectionSupplierForTests() {
// Reset the jasync Postgres connection supplier.
connectionSupplier = null;
}
// Class constructor.
public FastOfflinePlayer(UUID inputtedUUID) {
// Cache the provided UUID.
this.uuid = inputtedUUID;
}
// Internal constructor for deserialization.
private FastOfflinePlayer(@NotNull UUID uuid, @Nullable String username) {
// Pass the provided UUID.
this.uuid = uuid;
// Pass the username (will be set to 'usernameLookupError' if not found).
this.cachedName = username;
}
@Override
public boolean isOp() {
// TODO Auto-generated method stub.
return false;
}
@Override
public void setOp(boolean value) {
// TODO Auto-generated method stub.
}
// Fast re-implementation of OfflinePlayer.serialize() from Bukkit API.
@Override
public @NotNull Map<String, Object> serialize() {
// Construct a map to hold player data.
final Map<String, Object> map = new LinkedHashMap<>();
// Add the player data to the map.
map.put("name", getName());
// Return the combined serialized object.
return map;
}
// Fast re-implementation of OfflinePlayer.deserialize() from Bukkit API.
public static @NotNull FastOfflinePlayer deserialize(@NotNull UUID uuid, @NotNull Map<String, Object> values) {
// Extract the player name from the serialized map as a String.
final String name = (String) values.get("name");
// Return the UUID from cache.
return new FastOfflinePlayer(uuid, name);
}
@Override
public boolean isOnline() {
// TODO Auto-generated method stub.
return false;
}
// Fast re-implementation of OfflinePlayer.getName() from Bukkit API.
@Override
public @Nullable String getName() {
// If the name is already in the cache, do this...
if (cachedName != null) {
// Return the name from cache instead of querying the database.
return cachedName;
}
// Get existing lookup future if present.
CompletableFuture<String> future = lookup.get();
// If there is no existing lookup future, do this...
if (future == null) {
// Create new jasync Postgres query for username.
CompletableFuture<String> created = connection()
.sendPreparedStatement("SELECT username FROM offline_players WHERE uuid = $1 LIMIT 1;",
Collections.singletonList(uuid))
// On success, cache and return username.
.thenApply(res ->
{
// If the query returned rows, do this...
if (!res.getRows().isEmpty()) {
// Extract the "username" value from the first row.
final String name = res.getRows().get(0).getString("username");
// Store the retrieved username in the cache.
cachedName = name;
// Return the retrieved username.
return name;
}
// If no rows are found, return null.
return null;
})
// If there was an error with finding the name, do this...
.exceptionally(ex ->
{
// Set a placeholder name to avoid a crash.
cachedName = "usernameLookupError";
UtilitiesOG.logToConsole(Internal.getPrefix(),
"ERROR: FastOfflinePlayer getName() failed!" + ex.getMessage());
// Return the placeholder name.
return cachedName;
});
// Attempt to set the lookup future only if it is currently null.
if (!lookup.compareAndSet(null, created)) {
// If another thread already set the future, use the existing one instead.
created = lookup.get();
}
// Store created future.
future = created;
}
// Return cached name (may still be null until async query completes).
return cachedName;
}
// Non-cached version of getName() for testing
public @NotNull CompletableFuture<@Nullable String> getNameAsync() {
// If cached name exists, return it as completed future
if (cachedName != null) {
return CompletableFuture.completedFuture(cachedName);
}
// Ensure lookup starts by calling getName()
getName();
// Return lookup future
return lookup.get();
}
// Fast re-implementation OfflinePlayer.getUniqueId() from Bukkit API.
@Override
public @NotNull UUID getUniqueId() {
// Pass on UUID from constructor.
return uuid;
}
@Override
public @NotNull PlayerProfile getPlayerProfile() {
// TODO Auto-generated method stub.
return null;
}
@Override
public boolean isBanned() {
// TODO Auto-generated method stub.
return false;
}
@Override
public boolean isWhitelisted() {
// TODO Auto-generated method stub.
return false;
}
@Override
public void setWhitelisted(boolean value) {
// TODO Auto-generated method stub.
}
@Override
public @Nullable Player getPlayer() {
// TODO Auto-generated method stub.
return null;
}
@Override
public long getFirstPlayed() {
// TODO Auto-generated method stub.
return 0;
}
/**
* @deprecated The getLastPlayed API contract is ambiguous and the
* implementation may or may not return the correct value. Use
* {@link #getLastSeen()} instead.
*/
@Deprecated
@Override
public long getLastPlayed() {
// TODO Auto-generated method stub.
return getLastSeen();
}
@Override
public boolean hasPlayedBefore() {
// TODO Auto-generated method stub.
return false;
}
@Override
public @Nullable Location getBedSpawnLocation() {
// TODO Auto-generated method stub.
return null;
}
@Override
public long getLastLogin() {
// TODO Auto-generated method stub.
return 0;
}
@Override
public long getLastSeen() {
// TODO Auto-generated method stub.
return 0;
}
@Override
public void incrementStatistic(@NotNull Statistic statistic) throws IllegalArgumentException {
// TODO Auto-generated method stub.
}
@Override
public void decrementStatistic(@NotNull Statistic statistic) throws IllegalArgumentException {
// TODO Auto-generated method stub.
}
@Override
public void incrementStatistic(@NotNull Statistic statistic, int amount) throws IllegalArgumentException {
// TODO Auto-generated method stub.
}
@Override
public void decrementStatistic(@NotNull Statistic statistic, int amount) throws IllegalArgumentException {
// TODO Auto-generated method stub.
}
@Override
public void setStatistic(@NotNull Statistic statistic, int newValue) throws IllegalArgumentException {
// TODO Auto-generated method stub.
}
@Override
public int getStatistic(@NotNull Statistic statistic) throws IllegalArgumentException {
// TODO Auto-generated method stub.
return 0;
}
@Override
public void incrementStatistic(@NotNull Statistic statistic, @NotNull Material material)
throws IllegalArgumentException
{
// TODO Auto-generated method stub.
}
@Override
public void decrementStatistic(@NotNull Statistic statistic, @NotNull Material material)
throws IllegalArgumentException
{
// TODO Auto-generated method stub.
}
@Override
public int getStatistic(@NotNull Statistic statistic, @NotNull Material material) throws IllegalArgumentException {
// TODO Auto-generated method stub.
return 0;
}
@Override
public void incrementStatistic(@NotNull Statistic statistic, @NotNull Material material, int amount)
throws IllegalArgumentException
{
// TODO Auto-generated method stub.
}
@Override
public void decrementStatistic(@NotNull Statistic statistic, @NotNull Material material, int amount)
throws IllegalArgumentException
{
// TODO Auto-generated method stub.
}
@Override
public void setStatistic(@NotNull Statistic statistic, @NotNull Material material, int newValue)
throws IllegalArgumentException
{
// TODO Auto-generated method stub.
}
@Override
public void incrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType)
throws IllegalArgumentException
{
// TODO Auto-generated method stub.
}
@Override
public void decrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType)
throws IllegalArgumentException
{
// TODO Auto-generated method stub.
}
@Override
public int getStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType)
throws IllegalArgumentException
{
// TODO Auto-generated method stub.
return 0;
}
@Override
public void incrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType, int amount)
throws IllegalArgumentException
{
// TODO Auto-generated method stub.
}
@Override
public void decrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType, int amount) {
// TODO Auto-generated method stub.
}
@Override
public void setStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType, int newValue) {
// TODO Auto-generated method stub.
}
@Override
public @Nullable Location getLastDeathLocation() {
// TODO Auto-generated method stub.
return null;
}
@Override
public boolean getAllowFlight() {
// TODO Auto-generated method stub.
return false;
}
@Override
public void setAllowFlight(boolean flight) {
// TODO Auto-generated method stub.
}
@Override
public boolean isFlying() {
// TODO Auto-generated method stub.
return false;
}
@Override
public void setFlying(boolean value) {
// TODO Auto-generated method stub.
}
@Override
public void setFlySpeed(float value) throws IllegalArgumentException {
// TODO Auto-generated method stub.
}
@Override
public void setWalkSpeed(float value) throws IllegalArgumentException {
// TODO Auto-generated method stub.
}
@Override
public float getFlySpeed() {
// TODO Auto-generated method stub.
return 0;
}
@Override
public float getWalkSpeed() {
// TODO Auto-generated method stub.
return 0;
}
@Override
public @Nullable Location getLocation() {
// TODO Auto-generated method stub.
return null;
}
@Override
public boolean teleportOffline(@NotNull Location destination) {
// TODO Auto-generated method stub.
return false;
}
@Override
public boolean teleportOffline(@NotNull Location destination, @NotNull TeleportCause cause) {
// TODO Auto-generated method stub.
return false;
}
@Override
public @NotNull CompletableFuture<Boolean> teleportOfflineAsync(@NotNull Location destination) {
// TODO Auto-generated method stub.
return null;
}
@Override
public @NotNull CompletableFuture<Boolean> teleportOfflineAsync(@NotNull Location destination,
@NotNull TeleportCause cause)
{
// TODO Auto-generated method stub.
return null;
}
}
@@ -2,9 +2,6 @@
// Authors: NotAlexNoyle.
package net.trueog.utilitiesog;
import com.github.jasync.sql.db.pool.ConnectionPool;
import com.github.jasync.sql.db.postgresql.PostgreSQLConnection;
// Internal-only accessors for states owned by the main UtilitiesOG class.
public final class Internal {
@@ -26,13 +23,6 @@ public final class Internal {
}
// Shared jasync Postgres connection pool.
public static ConnectionPool<PostgreSQLConnection> getPostgres() {
return UtilitiesOG.getPluginPostgres();
}
// Three-argument functional interface used for the relational-with-args
// placeholder registration API.
@FunctionalInterface
@@ -9,14 +9,16 @@ 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.PlayerDataUtils;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import com.github.jasync.sql.db.pool.ConnectionPool;
import com.github.jasync.sql.db.postgresql.PostgreSQLConnection;
import com.github.jasync.sql.db.postgresql.PostgreSQLConnectionBuilder;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.luckperms.api.LuckPerms;
@@ -50,6 +52,8 @@ import net.trueog.utilitiesog.modules.RandomDropsModule;
import net.trueog.utilitiesog.utils.MessageFormat;
import net.trueog.utilitiesog.utils.PlaceholderUtils;
import net.trueog.utilitiesog.utils.TextUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
// Declare main plugin class.
public final class UtilitiesOG extends JavaPlugin {
@@ -61,8 +65,7 @@ public final class UtilitiesOG extends JavaPlugin {
// Declare live plugin instance to be initialized in onEnable().
private static UtilitiesOG instance;
// Declare live PostgreSQL connection pool to be initialized in onEnable().
private static ConnectionPool<PostgreSQLConnection> POOL;
private static PlayerDataUtils playerDataUtils;
// Package-private hooks consumed by net.trueog.utilitiesog.Internal.
static UtilitiesOG getPluginInstance() {
@@ -77,17 +80,13 @@ public final class UtilitiesOG extends JavaPlugin {
}
static ConnectionPool<PostgreSQLConnection> getPluginPostgres() {
return POOL;
}
@Override
public void onEnable() {
instance = this;
playerDataUtils = new PlayerDataUtils();
final File configFile = new File(this.getDataFolder(), "config.yml");
if (!configFile.exists()) {
@@ -95,8 +94,6 @@ public final class UtilitiesOG extends JavaPlugin {
}
POOL = initPsql();
if (this.getConfig().getBoolean("ChainArmor")) {
ChainArmorModule.Enable();
@@ -500,19 +497,62 @@ public final class UtilitiesOG extends JavaPlugin {
}
// Initialize postgres connection;
private ConnectionPool<PostgreSQLConnection> initPsql() {
public static @Nullable ItemStack @NotNull [] getInventoryData(UUID uuid) {
// Reads the values from config.yml;
final String baseUrl = getConfig().getString("postgresUrl"); // e.g. jdbc:postgresql://localhost:5432/diamond
final String user = getConfig().getString("postgresUser"); // e.g. postgres
final String password = getConfig().getString("postgresPassword"); // e.g. postgresPassword
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++) {
// Appends credentials to the URL (only if they are not already present)
final String jdbcUrl = "%s?user=%s&password=%s".formatted(baseUrl, user, password);
final CompoundTag slotTag = inventoryData.getCompound(i);
final int slot = slotTag.getByte("Slot") & 0xFF;
final String id = slotTag.getString("id");
final int count = slotTag.getByte("Count") & 0xFF;
final Material material = Material.matchMaterial(id);
if (material == null) {
// Create postgres connection.
return PostgreSQLConnectionBuilder.createConnectionPool(jdbcUrl);
continue;
}
if (slot < inventoryContents.length) {
inventoryContents[slot] = new ItemStack(material, count);
}
}
return inventoryContents;
}
public static void setInventoryData(UUID uuid, @Nullable ItemStack @NotNull [] items) {
final ListTag inventoryData = new ListTag();
for (int i = 0; i < items.length; i++) {
final ItemStack item = items[i];
if (item == null) {
continue;
}
final CompoundTag slotTag = new CompoundTag();
slotTag.putByte("Slot", (byte) i);
slotTag.putString("id", item.getType().getKey().toString());
slotTag.putByte("Count", (byte) item.getAmount());
inventoryData.add(slotTag);
}
playerDataUtils.setInventoryData(uuid, inventoryData);
}
public static int getHeldItemSlot(UUID uuid) {
return playerDataUtils.getPlayerData(uuid).getInt("SelectedItemSlot");
}
@@ -1,57 +0,0 @@
// This is free and unencumbered software released into the public domain.
// Authors: christianniehaus, NotAlexNoyle.
package net.trueog.utilitiesog.listeners;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.PlayerJoinEvent;
import com.github.jasync.sql.db.QueryResult;
import net.trueog.utilitiesog.Internal;
import net.trueog.utilitiesog.UtilitiesOG;
// A class for storing and caching FastOfflinePlayers during player join and player leave.
public class FastOfflinePlayerListener {
// When a player joins the server, do this...
@EventHandler(priority = EventPriority.NORMAL)
public void onPlayerJoin(PlayerJoinEvent event) {
// Get the player from the event.
final Player player = event.getPlayer();
// Add or update player data in the offline player database.
upsertOrUpdatePlayer(player);
// Inform the player of the result.
UtilitiesOG.trueogMessage(player, "Your data is cached!");
}
// Insert new user or update user metadata in one query depending on need.
private CompletableFuture<QueryResult> upsertOrUpdatePlayer(Player player) {
// Direct postgres query for inserting and/or updating a player.
final String sql = "INSERT INTO offline_players (uuid, username) " + "VALUES ($1, $2) "
+ "ON CONFLICT (uuid) DO UPDATE " + " SET username = EXCLUDED.username, "
+ " last_seen = NOW();";
// Add player UUID and player name key-value pair to database.
return query(sql, player.getUniqueId(), player.getName());
}
// Send an async query using jasync Postgres.
private CompletableFuture<QueryResult> query(String sql, Object... params) {
// Gift wrap around jasync’s sendPreparedStatement.
return Internal.getPostgres().sendPreparedStatement(sql, Arrays.asList(params));
}
}
@@ -0,0 +1,84 @@
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.Method;
import java.util.UUID;
public class PlayerDataUtils {
private final PlayerDataStorage playerIo = ((CraftServer) Bukkit.getServer()).getServer().getPlayerList().playerIo;
private Method getPlayerData;
private Method saveCompoundTag;
public PlayerDataUtils() {
try {
getPlayerData = playerIo.getClass().getDeclaredMethod("getPlayerData", UUID.class);
saveCompoundTag = playerIo.getClass().getDeclaredMethod("saveCompoundTag", UUID.class, CompoundTag.class);
} catch (NoSuchMethodException e) {
getPlayerData = null;
saveCompoundTag = null;
Internal.getPlugin().getLogger().warning("Not running TrueOG Purpur jar, player data APIs are unavailable");
}
}
public CompoundTag getPlayerData(UUID uuid) {
if (getPlayerData == null) {
throw new UnsupportedOperationException("Not running TrueOG Purpur jar, player data APIs are unavailable");
}
try {
return (CompoundTag) getPlayerData.invoke(playerIo, uuid);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
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);
}
}
+1 -8
View File
@@ -26,11 +26,4 @@ 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). Default: true.
MissingCommands: true
# PostgreSQL Setup for FastOfflinePlayer database.
postgresUrl: jdbc:postgresql://localhost:5432/fastofflineplayer
postgresUser: ^{POSTGRES_USER}
postgresPassword: ^{POSTGRES_PASSWORD}
postgresTable: fastofflineplayer
postgresLogTable: fastofflineplayer_log
postgresLogLimit: 500
MissingCommands: true
@@ -1,498 +0,0 @@
// This is free and unencumbered software released into the public domain.
// Authors: NotAlexNoyle.
package net.trueog.utilitiesog;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import com.github.jasync.sql.db.QueryResult;
import com.github.jasync.sql.db.ResultSet;
import com.github.jasync.sql.db.RowData;
import com.github.jasync.sql.db.pool.ConnectionPool;
import be.seeseemelk.mockbukkit.MockBukkit;
import be.seeseemelk.mockbukkit.ServerMock;
// Test cases for FastOfflinePlayer.java
class FastOfflinePlayerTest {
// Declare a test UUID.
final UUID testUUID = UUID.randomUUID();
// Declare a test username.
final String testName = "NotAlexNoyle";
// Declare a mock server container using MockBukkit.
private static ServerMock server;
// Runs before all tests.
@BeforeAll
static void setUp() {
// Initialize the MockBukkit server.
server = MockBukkit.mock();
}
// Runs after each test.
@AfterEach
void resetDb() {
// Ensure each test gets a clean supplier.
FastOfflinePlayer.resetConnectionSupplierForTests();
}
// Runs after all tests.
@AfterAll
static void tearDown() {
// Bring down MockBukkit server.
MockBukkit.unmock();
}
// TODO: Uncomment and finish
/*
* @Test void testIsOp() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testSetOp() {
*
* fail("Not yet implemented");
*
* }
*/
// FastOfflinePlayer.serialize() test.
@Test
void testSerialize() {
// Declare a test player.
final FastOfflinePlayer testSerializePlayer = FastOfflinePlayer.deserialize(testUUID, Map.of("name", testName));
// JUnit: Compare the expected map to the FastOfflinePlayer map.
assertEquals(Map.of("name", testName), testSerializePlayer.serialize());
}
// FastOfflinePlayer.deserialize() test.
@Test
void testDeserialize() {
// Compose a deserialization control group map.
final Map<String, Object> yaml = Map.of("name", testName);
// Declare a test player.
final FastOfflinePlayer testDeserializePlayer = FastOfflinePlayer.deserialize(testUUID, yaml);
// JUnit: Compare the expected UUID to the FastOfflinePlayer UUID.
assertEquals(testUUID, testDeserializePlayer.getUniqueId());
// JUnit: Compare the expected name to the FastOfflinePlayer name.
assertEquals(testName, testDeserializePlayer.getName());
}
// TODO: Uncomment and finish
/*
* @Test void testIsOnline() {
*
* fail("Not yet implemented");
*
* }
*/
// FastOfflinePlayer.getName() test.
@Test
void testGetName() {
// Declare a jasync Postgres connection pool.
final ConnectionPool<?> pool = mockPoolReturning(
mockQueryResult(mockResultSet(mockRow(Map.of("username", testName)))));
installMockDb(pool);
// Declare a test player.
final FastOfflinePlayer testGetNamePlayer = new FastOfflinePlayer(UUID.randomUUID());
// JUnit: Compare the expected name to the actual name from database.
assertEquals(testName, testGetNamePlayer.getNameAsync().join());
// JUnit: Compare the expected name to the actual name from cache.
assertEquals(testName, testGetNamePlayer.getName());
// Verify that multiple queries are not overlapping.
verify(pool, times(1)).sendPreparedStatement(anyString(), anyList());
}
// FastOfflinePlayer.getUniqueID() test.
@Test
void testGetUniqueId() {
// Declare a test player.
final FastOfflinePlayer uuidTestPlayer = new FastOfflinePlayer(testUUID);
// JUnit: Compare the expected UUID to the FastOfflinePlayer UUID.
assertEquals(testUUID, uuidTestPlayer.getUniqueId());
}
// TODO: Uncomment and finish
/*
* @Test void testGetPlayerProfile() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testIsBanned() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testIsWhitelisted() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testSetWhitelisted() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetPlayer() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetFirstPlayed() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetLastPlayed() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testHasPlayedBefore() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetBedSpawnLocation() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetLastLogin() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetLastSeen() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testIncrementStatisticStatistic() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testDecrementStatisticStatistic() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testIncrementStatisticStatisticInt() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testDecrementStatisticStatisticInt() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testSetStatisticStatisticInt() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetStatisticStatistic() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testIncrementStatisticStatisticMaterial() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testDecrementStatisticStatisticMaterial() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetStatisticStatisticMaterial() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testIncrementStatisticStatisticMaterialInt() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testDecrementStatisticStatisticMaterialInt() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testSetStatisticStatisticMaterialInt() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testIncrementStatisticStatisticEntityType() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testDecrementStatisticStatisticEntityType() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetStatisticStatisticEntityType() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testIncrementStatisticStatisticEntityTypeInt() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testDecrementStatisticStatisticEntityTypeInt() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testSetStatisticStatisticEntityTypeInt() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetLastDeathLocation() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetAllowFlight() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testSetAllowFlight() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testIsFlying() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testSetFlying() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testSetFlySpeed() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testSetWalkSpeed() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetFlySpeed() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetWalkSpeed() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testGetLocation() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testTeleportOfflineLocation() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testTeleportOfflineLocationTeleportCause() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testTeleportOfflineAsyncLocation() {
*
* fail("Not yet implemented");
*
* }
*
* @Test void testTeleportOfflineAsyncLocationTeleportCause() {
*
* fail("Not yet implemented");
*
* }
*/
// Declare a fake jasync Postgres row for tests to use.
private static RowData mockRow(Map<String, Object> cols) {
// Declare a Mockito mock for a single jasync Postgres row.
final RowData row = mock(RowData.class);
// When getString(columnName) is called, look up the value from the map.
when(row.getString(anyString())).thenAnswer(inv -> {
// If absent, return null; otherwise, coerce to String to match the API.
final Object v = cols.get(inv.getArgument(0));
return v == null ? null : String.valueOf(v);
});
// Return the configured mock row.
return row;
}
// Declare a fake jasync Postgres result set for tests to use.
private static ResultSet mockResultSet(RowData... rowArray) {
// Create a Mockito mock for a jasync Postgres ResultSet.
final ResultSet rs = mock(ResultSet.class);
// Mock a result set. isEmpty() should reflect whether any rows have been passed
// in.
when(rs.isEmpty()).thenReturn(rowArray.length == 0);
// Loop that runs once for each result row.
for (int i = 0; i < rowArray.length; i++) {
// Stub indexed access to return the corresponding RowData from the array.
when(rs.get(i)).thenReturn(rowArray[i]);
}
// Return the configured mock result set.
return rs;
}
// Declare a fake jasync Postgres query result for tests to use.
private static QueryResult mockQueryResult(ResultSet rows) {
// Create a Mockito mock for a jasync Postgres QueryResult that wraps a given
// ResultSet.
final QueryResult qr = mock(QueryResult.class);
// Mock a query result.
when(qr.getRows()).thenReturn(rows);
// Return the mocked query result.
return qr;
}
// Declare a fake jasync Postgres ConnectionPool for tests to use.
private static ConnectionPool<?> mockPoolReturning(QueryResult result) {
// Create a Mockito mock for a jasync Postgres ConnectionPool.
final ConnectionPool<?> pool = mock(ConnectionPool.class);
// Mock a ConnectionPool.
when(pool.sendPreparedStatement(anyString(), anyList())).thenReturn(CompletableFuture.completedFuture(result));
// Return the mocked mock pool.
return pool;
}
// Initialize the mocked jasync Postgres database.
private static void installMockDb(ConnectionPool<?> pool) {
// Install the mocked pool into FastOfflinePlayer via its test seam so code
// under test uses this pool instead of touching UtilitiesOG. This prevents
// missing class errors due to not all UtilitiesOG.java calls being on the test
// class path.
FastOfflinePlayer.overrideConnectionSupplierForTests(() -> pool);
}
}