Overhaul API, add JitPack integration for developer API provision

feat/data-edit-commands
William 3 years ago
parent 5dbea87ccb
commit 023082e749

@ -0,0 +1,19 @@
#!/bin/bash
JV=$(java -version 2>&1 >/dev/null | head -1)
echo "$JV" | sed -E 's/^.*version "([^".]*)\.[^"]*".*$/\1/'
if [ "$JV" != 16 ]; then
case "$1" in
install)
echo "installing sdkman..."
curl -s "https://get.sdkman.io" | bash
source ~/.sdkman/bin/sdkman-init.sh
sdk install java 16.0.1-open
;;
use)
echo "must source ~/.sdkman/bin/sdkman-init.sh"
exit 1
;;
esac
fi

@ -2,5 +2,43 @@ dependencies {
implementation project(':common') implementation project(':common')
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT' compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
compileOnly 'org.jetbrains:annotations:20.1.0' compileOnly 'org.jetbrains:annotations:22.0.0'
}
publishing {
publications {
mavenJava(MavenPublication) {
shadow.component(it)
afterEvaluate {
artifact javadocsJar
}
}
}
repositories {
mavenLocal()
}
}
shadowJar {
classifier = null
relocate ':common', 'me.william278.husksync'
}
repositories {
mavenCentral()
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
}
task javadocs(type: Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
source = project(':common').sourceSets.main.allJava
source += project(':api').sourceSets.main.allJava
classpath = files(project(':common').sourceSets.main.compileClasspath)
classpath += files(project(':api').sourceSets.main.compileClasspath)
destinationDir = file("${buildDir}/docs/javadoc")
}
task javadocsJar(type: Jar, dependsOn: javadocs) {
classifier = 'javadoc'
from javadocs.destinationDir
} }

@ -0,0 +1,60 @@
package me.william278.husksync.bukkit.api;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.redis.RedisMessage;
import java.io.IOException;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* API method class for HuskSync. To access methods, use the {@link #getInstance()} entrypoint.
*/
public class HuskSyncAPI {
private HuskSyncAPI() {
}
private static HuskSyncAPI instance;
/**
* API entry point. Returns an instance of the {@link HuskSyncAPI}
*
* @return instance of the {@link HuskSyncAPI}
*/
public static HuskSyncAPI getInstance() {
if (instance == null) {
instance = new HuskSyncAPI();
}
return instance;
}
/**
* (INTERNAL) Map of API requests that are processed by the bukkit plugin that implements the API.
*/
public static HashMap<UUID, CompletableFuture<PlayerData>> apiRequests = new HashMap<>();
/**
* Returns a {@link CompletableFuture} that will fetch the {@link PlayerData} for a user given their {@link UUID}, which contains synchronised data that can then be deserialized into ItemStacks and other usable values using the {@link me.william278.husksync.bukkit.data.DataSerializer} class. If no data could be returned, such as if an invalid UUID is specified, the CompletableFuture will be cancelled. Note that this only returns the last cached data of the user; not necessarily the current state of their inventory if they are online.
*
* @param playerUUID The {@link UUID} of the player to get data for
* @return a {@link CompletableFuture} with the user's {@link PlayerData} accessible on completion
* @throws IOException If an exception occurs with serializing during processing of the request
*/
public CompletableFuture<PlayerData> getPlayerData(UUID playerUUID) throws IOException {
final UUID requestUUID = UUID.randomUUID();
CompletableFuture<PlayerData> playerDataCompletableFuture = new CompletableFuture<>();
playerDataCompletableFuture.whenComplete((playerData, throwable) -> apiRequests.remove(requestUUID));
// Request the data via the proxy
new RedisMessage(RedisMessage.MessageType.API_DATA_REQUEST,
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
playerUUID.toString(), requestUUID.toString()).send();
apiRequests.put(requestUUID, playerDataCompletableFuture);
return playerDataCompletableFuture;
}
}

@ -1,4 +1,4 @@
package me.william278.husksync.api.events; package me.william278.husksync.bukkit.api.events;
import me.william278.husksync.PlayerData; import me.william278.husksync.PlayerData;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -7,8 +7,7 @@ import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Represents an event that will be fired when a {@link Player} has finished * Represents an event that will be fired when a {@link Player} has finished being synchronised with the correct {@link PlayerData}.
* being synchronised with the correct {@link PlayerData}.
*/ */
public class SyncCompleteEvent extends PlayerEvent { public class SyncCompleteEvent extends PlayerEvent {

@ -1,17 +1,14 @@
package me.william278.husksync.api.events; package me.william278.husksync.bukkit.api.events;
import me.william278.husksync.PlayerData; import me.william278.husksync.PlayerData;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable; import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent; import org.bukkit.event.player.PlayerEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Represents an event that will be fired before a {@link Player} is about * Represents an event that will be fired before a {@link Player} is about to be synchronised with their {@link PlayerData}.
* to be synchronised with their {@link PlayerData}.
*/ */
public class SyncEvent extends PlayerEvent implements Cancellable { public class SyncEvent extends PlayerEvent implements Cancellable {
@ -26,6 +23,7 @@ public class SyncEvent extends PlayerEvent implements Cancellable {
/** /**
* Returns the {@link PlayerData} which has just been set on the {@link Player} * Returns the {@link PlayerData} which has just been set on the {@link Player}
*
* @return The {@link PlayerData} that has been set * @return The {@link PlayerData} that has been set
*/ */
public PlayerData getData() { public PlayerData getData() {
@ -34,6 +32,7 @@ public class SyncEvent extends PlayerEvent implements Cancellable {
/** /**
* Sets the {@link PlayerData} to be synchronised to this player * Sets the {@link PlayerData} to be synchronised to this player
*
* @param data The {@link PlayerData} to set to the player * @param data The {@link PlayerData} to set to the player
*/ */
public void setData(PlayerData data) { public void setData(PlayerData data) {
@ -50,8 +49,7 @@ public class SyncEvent extends PlayerEvent implements Cancellable {
} }
/** /**
* Gets the cancellation state of this event. A cancelled event will not * Gets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins
* be executed in the server, but will still pass to other plugins
* *
* @return true if this event is cancelled * @return true if this event is cancelled
*/ */
@ -61,8 +59,7 @@ public class SyncEvent extends PlayerEvent implements Cancellable {
} }
/** /**
* Sets the cancellation state of this event. A cancelled event will not * Sets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins.
* be executed in the server, but will still pass to other plugins.
* *
* @param cancel true if you wish to cancel this event * @param cancel true if you wish to cancel this event
*/ */

@ -19,6 +19,9 @@ import java.io.Serializable;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.*;
/**
* Class that contains static methods for serializing and deserializing data from {@link me.william278.husksync.PlayerData}
*/
public class DataSerializer { public class DataSerializer {
/** /**
@ -208,6 +211,13 @@ public class DataSerializer {
playerLocation.getYaw(), playerLocation.getPitch(), player.getWorld().getName(), player.getWorld().getEnvironment())); playerLocation.getYaw(), playerLocation.getPitch(), player.getWorld().getName(), player.getWorld().getEnvironment()));
} }
/**
* Deserializes a player's advancement data as serialized with {@link #getSerializedAdvancements(Player)} into {@link AdvancementRecordDate} data.
*
* @param serializedAdvancementData The serialized advancement data {@link String}
* @return The deserialized {@link AdvancementRecordDate} for the player
* @throws IOException If the deserialization fails
*/
@SuppressWarnings("unchecked") // Ignore the unchecked cast here @SuppressWarnings("unchecked") // Ignore the unchecked cast here
public static List<DataSerializer.AdvancementRecordDate> deserializeAdvancementData(String serializedAdvancementData) throws IOException { public static List<DataSerializer.AdvancementRecordDate> deserializeAdvancementData(String serializedAdvancementData) throws IOException {
if (serializedAdvancementData.isEmpty()) { if (serializedAdvancementData.isEmpty()) {
@ -216,6 +226,7 @@ public class DataSerializer {
try { try {
List<?> deserialize = (List<?>) RedisMessage.deserialize(serializedAdvancementData); List<?> deserialize = (List<?>) RedisMessage.deserialize(serializedAdvancementData);
// Migrate old AdvancementRecord into date format
if (!deserialize.isEmpty() && deserialize.get(0) instanceof AdvancementRecord) { if (!deserialize.isEmpty() && deserialize.get(0) instanceof AdvancementRecord) {
deserialize = ((List<AdvancementRecord>) deserialize).stream() deserialize = ((List<AdvancementRecord>) deserialize).stream()
.map(o -> new AdvancementRecordDate( .map(o -> new AdvancementRecordDate(
@ -230,6 +241,13 @@ public class DataSerializer {
} }
} }
/**
* Returns a serialized {@link String} of a player's advancements that can be deserialized with {@link #deserializeStatisticData(String)}
*
* @param player {@link Player} to serialize advancement data of
* @return The serialized advancement data as a {@link String}
* @throws IOException If the serialization fails
*/
public static String getSerializedAdvancements(Player player) throws IOException { public static String getSerializedAdvancements(Player player) throws IOException {
Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator(); Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
ArrayList<DataSerializer.AdvancementRecordDate> advancementData = new ArrayList<>(); ArrayList<DataSerializer.AdvancementRecordDate> advancementData = new ArrayList<>();
@ -247,6 +265,13 @@ public class DataSerializer {
return RedisMessage.serialize(advancementData); return RedisMessage.serialize(advancementData);
} }
/**
* Deserializes a player's statistic data as serialized with {@link #getSerializedStatisticData(Player)} into {@link StatisticData}.
*
* @param serializedStatisticData The serialized statistic data {@link String}
* @return The deserialized {@link StatisticData} for the player
* @throws IOException If the deserialization fails
*/
public static DataSerializer.StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException { public static DataSerializer.StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException {
if (serializedStatisticData.isEmpty()) { if (serializedStatisticData.isEmpty()) {
return new DataSerializer.StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); return new DataSerializer.StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
@ -258,6 +283,13 @@ public class DataSerializer {
} }
} }
/**
* Returns a serialized {@link String} of a player's statistic data that can be deserialized with {@link #deserializeStatisticData(String)}
*
* @param player {@link Player} to serialize statistic data of
* @return The serialized statistic data as a {@link String}
* @throws IOException If the serialization fails
*/
public static String getSerializedStatisticData(Player player) throws IOException { public static String getSerializedStatisticData(Player player) throws IOException {
HashMap<Statistic, Integer> untypedStatisticValues = new HashMap<>(); HashMap<Statistic, Integer> untypedStatisticValues = new HashMap<>();
HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues = new HashMap<>(); HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues = new HashMap<>();
@ -294,14 +326,27 @@ public class DataSerializer {
return RedisMessage.serialize(statisticData); return RedisMessage.serialize(statisticData);
} }
/**
* A record used to store data for a player's location
*/
public record PlayerLocation(double x, double y, double z, float yaw, float pitch, public record PlayerLocation(double x, double y, double z, float yaw, float pitch,
String worldName, World.Environment environment) implements Serializable { String worldName, World.Environment environment) implements Serializable {
} }
/**
* A record used to store data for advancement synchronisation
*
* @deprecated Old format - Use {@link AdvancementRecordDate} instead
*/
@Deprecated
@SuppressWarnings("DeprecatedIsStillUsed") // Suppress deprecation warnings here (still used for backwards compatibility)
public record AdvancementRecord(String advancementKey, public record AdvancementRecord(String advancementKey,
ArrayList<String> awardedAdvancementCriteria) implements Serializable { ArrayList<String> awardedAdvancementCriteria) implements Serializable {
} }
/**
* A record used to store data for native advancement synchronisation, tracking advancement date progress
*/
public record AdvancementRecordDate(String key, Map<String, Date> criteriaMap) implements Serializable { public record AdvancementRecordDate(String key, Map<String, Date> criteriaMap) implements Serializable {
AdvancementRecordDate(String key, List<String> criteriaList) { AdvancementRecordDate(String key, List<String> criteriaList) {
this(key, new HashMap<>() {{ this(key, new HashMap<>() {{
@ -310,6 +355,9 @@ public class DataSerializer {
} }
} }
/**
* A record used to store data for a player's statistics
*/
public record StatisticData(HashMap<Statistic, Integer> untypedStatisticValues, public record StatisticData(HashMap<Statistic, Integer> untypedStatisticValues,
HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues, HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues,
HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues, HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues,

@ -9,6 +9,7 @@ dependencies {
compileOnly 'net.craftersland.data:bridge:4.0.1' compileOnly 'net.craftersland.data:bridge:4.0.1'
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT' compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
compileOnly 'org.jetbrains:annotations:22.0.0'
} }
shadowJar { shadowJar {

@ -17,6 +17,7 @@ import org.bukkit.scheduler.BukkitTask;
import java.io.IOException; import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level; import java.util.logging.Level;
public final class HuskSyncBukkit extends JavaPlugin { public final class HuskSyncBukkit extends JavaPlugin {

@ -2,16 +2,17 @@ package me.william278.husksync.bukkit.listener;
import de.themoep.minedown.MineDown; import de.themoep.minedown.MineDown;
import me.william278.husksync.HuskSyncBukkit; import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.util.MessageManager;
import me.william278.husksync.PlayerData; import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings; import me.william278.husksync.Settings;
import me.william278.husksync.bukkit.api.HuskSyncAPI;
import me.william278.husksync.bukkit.config.ConfigLoader; import me.william278.husksync.bukkit.config.ConfigLoader;
import me.william278.husksync.bukkit.data.DataViewer; import me.william278.husksync.bukkit.data.DataViewer;
import me.william278.husksync.bukkit.util.PlayerSetter;
import me.william278.husksync.bukkit.migrator.MPDBDeserializer; import me.william278.husksync.bukkit.migrator.MPDBDeserializer;
import me.william278.husksync.bukkit.util.PlayerSetter;
import me.william278.husksync.migrator.MPDBPlayerData; import me.william278.husksync.migrator.MPDBPlayerData;
import me.william278.husksync.redis.RedisListener; import me.william278.husksync.redis.RedisListener;
import me.william278.husksync.redis.RedisMessage; import me.william278.husksync.redis.RedisMessage;
import me.william278.husksync.util.MessageManager;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -107,6 +108,25 @@ public class BukkitRedisListener extends RedisListener {
} }
}); });
} }
case API_DATA_RETURN -> {
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
if (HuskSyncAPI.apiRequests.containsKey(requestUUID)) {
try {
final PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
HuskSyncAPI.apiRequests.get(requestUUID).complete(data);
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to serialize returned API-requested player data");
}
}
}
case API_DATA_CANCEL -> {
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
// Cancel requests if no data could be found on the proxy
if (HuskSyncAPI.apiRequests.containsKey(requestUUID)) {
HuskSyncAPI.apiRequests.get(requestUUID).cancel(true);
}
}
case RELOAD_CONFIG -> { case RELOAD_CONFIG -> {
plugin.reloadConfig(); plugin.reloadConfig();
ConfigLoader.loadSettings(plugin.getConfig()); ConfigLoader.loadSettings(plugin.getConfig());

@ -3,8 +3,8 @@ package me.william278.husksync.bukkit.util;
import me.william278.husksync.HuskSyncBukkit; import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.PlayerData; import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings; import me.william278.husksync.Settings;
import me.william278.husksync.api.events.SyncCompleteEvent; import me.william278.husksync.bukkit.api.events.SyncCompleteEvent;
import me.william278.husksync.api.events.SyncEvent; import me.william278.husksync.bukkit.api.events.SyncEvent;
import me.william278.husksync.bukkit.data.DataSerializer; import me.william278.husksync.bukkit.data.DataSerializer;
import me.william278.husksync.bukkit.util.nms.AdvancementUtils; import me.william278.husksync.bukkit.util.nms.AdvancementUtils;
import me.william278.husksync.redis.RedisMessage; import me.william278.husksync.redis.RedisMessage;
@ -315,7 +315,7 @@ public class PlayerSetter {
* Update a player's advancements and progress to match the advancementData * Update a player's advancements and progress to match the advancementData
* *
* @param player The player to set the advancements of * @param player The player to set the advancements of
* @param advancementData The ArrayList of {@link DataSerializer.AdvancementRecord}s to set * @param advancementData The ArrayList of {@link DataSerializer.AdvancementRecordDate}s to set
*/ */
private static void setPlayerAdvancements(Player player, List<DataSerializer.AdvancementRecordDate> advancementData, PlayerData data) { private static void setPlayerAdvancements(Player player, List<DataSerializer.AdvancementRecordDate> advancementData, PlayerData data) {
// Temporarily disable advancement announcing if needed // Temporarily disable advancement announcing if needed

@ -197,6 +197,30 @@ public class BungeeRedisListener extends RedisListener {
HuskSyncBungeeCord.dataManager)); HuskSyncBungeeCord.dataManager));
} }
} }
case API_DATA_REQUEST -> {
final UUID playerUUID = UUID.fromString(message.getMessageDataElements()[0]);
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[1]);
try {
final PlayerData data = getPlayerCachedData(playerUUID, message.getMessageTarget().targetClusterId());
if (data == null) {
new RedisMessage(RedisMessage.MessageType.API_DATA_CANCEL,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
requestUUID.toString())
.send();
} else {
// Send the reply alongside the request UUID, serializing the requested message data
new RedisMessage(RedisMessage.MessageType.API_DATA_RETURN,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
requestUUID.toString(),
RedisMessage.serialize(data))
.send();
}
} catch (IOException e) {
plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize PlayerData requested via the API");
}
}
} }
} }

@ -104,6 +104,21 @@ public class RedisMessage {
*/ */
PLAYER_DATA_SET, PLAYER_DATA_SET,
/**
* Sent by Bukkit servers to proxy to request {@link PlayerData} from the proxy via the API
*/
API_DATA_REQUEST,
/**
* Sent by the Proxy to fulfill an {@code MessageType.API_DATA_REQUEST}, containing the latest {@link PlayerData} for the requested UUID
*/
API_DATA_RETURN,
/**
* Sent by the Proxy to cancel an {@code MessageType.API_DATA_REQUEST} if no data can be returned.
*/
API_DATA_CANCEL,
/** /**
* Sent by the proxy to a Bukkit server to have them request data on join; contains no data otherwise * Sent by the proxy to a Bukkit server to have them request data on join; contains no data otherwise
*/ */

@ -0,0 +1,12 @@
# This file ensures jitpack builds HuskSync correctly by setting the JDK to 16
jdk:
- 'openjdk16'
before_install:
- 'git clone https://github.com/WiIIiam278/HuskSync.git --recurse-submodules'
- 'chmod +x gradlew'
- 'chmod +x ./.jitpack/ensure-java-16'
- 'bash ./.jitpack/ensure-java-16 install'
install:
- 'if ! ./.jitpack/ensure-java-16 use; then source ~/.sdkman/bin/sdkman-init.sh; fi'
- 'java -version'
- './gradlew api:clean api:publishToMavenLocal'

@ -7,8 +7,8 @@ pluginManagement {
rootProject.name = 'HuskSync' rootProject.name = 'HuskSync'
include 'common' include 'common'
include 'api'
include 'bukkit' include 'bukkit'
include 'bungeecord' include 'bungeecord'
include 'velocity' include 'velocity'
include 'api'
include 'plugin' include 'plugin'

@ -190,6 +190,30 @@ public class VelocityRedisListener extends RedisListener {
migrator.loadIncomingData(migrator.incomingPlayerData, HuskSyncVelocity.dataManager); migrator.loadIncomingData(migrator.incomingPlayerData, HuskSyncVelocity.dataManager);
} }
} }
case API_DATA_REQUEST -> {
final UUID playerUUID = UUID.fromString(message.getMessageDataElements()[0]);
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[1]);
try {
final PlayerData data = getPlayerCachedData(playerUUID, message.getMessageTarget().targetClusterId());
if (data == null) {
new RedisMessage(RedisMessage.MessageType.API_DATA_CANCEL,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
requestUUID.toString())
.send();
} else {
// Send the reply alongside the request UUID, serializing the requested message data
new RedisMessage(RedisMessage.MessageType.API_DATA_RETURN,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
requestUUID.toString(),
RedisMessage.serialize(data))
.send();
}
} catch (IOException e) {
plugin.getVelocityLogger().log(Level.SEVERE, "Failed to serialize PlayerData requested via the API");
}
}
} }
} }

Loading…
Cancel
Save