9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-28 11:09:11 +00:00

Start 2.0 rewrite

Use redis key caching, remove need for proxy plugin
Make platform independent to allow porting to other platforms
This commit is contained in:
William
2022-07-02 00:17:51 +01:00
parent 633847a254
commit 9471e0cbff
91 changed files with 2117 additions and 6639 deletions

View File

@@ -1,163 +0,0 @@
package net.william278.husksync;
import net.william278.husksync.Settings;
import net.william278.husksync.bukkit.util.BukkitUpdateChecker;
import net.william278.husksync.bukkit.util.PlayerSetter;
import net.william278.husksync.bukkit.config.ConfigLoader;
import net.william278.husksync.bukkit.data.BukkitDataCache;
import net.william278.husksync.bukkit.listener.BukkitRedisListener;
import net.william278.husksync.bukkit.listener.BukkitEventListener;
import net.william278.husksync.bukkit.migrator.MPDBDeserializer;
import net.william278.husksync.redis.RedisMessage;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Level;
public final class HuskSyncBukkit extends JavaPlugin {
// Bukkit bStats ID (Different to BungeeCord)
private static final int METRICS_ID = 13140;
private static HuskSyncBukkit instance;
public static HuskSyncBukkit getInstance() {
return instance;
}
public static BukkitDataCache bukkitCache;
public static BukkitRedisListener redisListener;
// Used for establishing a handshake with redis
public static UUID serverUUID;
// Has a handshake been established with the Bungee?
public static boolean handshakeCompleted = false;
// The handshake task to execute
private static BukkitTask handshakeTask;
// Whether MySqlPlayerDataBridge is installed
public static boolean isMySqlPlayerDataBridgeInstalled;
// Establish the handshake with the proxy
public static void establishRedisHandshake() {
serverUUID = UUID.randomUUID();
getInstance().getLogger().log(Level.INFO, "Executing handshake with Proxy server...");
final int[] attempts = {0}; // How many attempts to establish communication have been made
handshakeTask = Bukkit.getScheduler().runTaskTimerAsynchronously(getInstance(), () -> {
if (handshakeCompleted) {
handshakeTask.cancel();
return;
}
try {
new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
serverUUID.toString(),
Boolean.toString(isMySqlPlayerDataBridgeInstalled),
Bukkit.getName(),
getInstance().getDescription().getVersion())
.send();
attempts[0]++;
if (attempts[0] == 10) {
getInstance().getLogger().log(Level.WARNING, "Failed to complete handshake with the Proxy server; Please make sure your Proxy server is online and has HuskSync installed in its' /plugins/ folder. HuskSync will continue to try and establish a connection.");
}
} catch (IOException e) {
getInstance().getLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake establishment", e);
}
}, 0, 60);
}
private void closeRedisHandshake() {
if (!handshakeCompleted) return;
try {
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
serverUUID.toString(),
Bukkit.getName()).send();
} catch (IOException e) {
getInstance().getLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
}
}
@Override
public void onLoad() {
instance = this;
}
@Override
public void onEnable() {
// Plugin startup logic
// Load the config file
getConfig().options().copyDefaults(true);
saveDefaultConfig();
saveConfig();
reloadConfig();
ConfigLoader.loadSettings(getConfig());
// Do update checker
if (Settings.automaticUpdateChecks) {
new BukkitUpdateChecker().logToConsole();
}
// Check if MySqlPlayerDataBridge is installed
Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
if (mySqlPlayerDataBridge != null) {
isMySqlPlayerDataBridgeInstalled = mySqlPlayerDataBridge.isEnabled();
MPDBDeserializer.setMySqlPlayerDataBridge();
getLogger().info("MySQLPlayerDataBridge detected! Disabled data synchronisation to prevent data loss. To perform a migration, run \"husksync migrate\" in your Proxy (Bungeecord, Waterfall, etc) server console.");
}
// Initialize last data update UUID cache
bukkitCache = new BukkitDataCache();
// Initialize event listener
getServer().getPluginManager().registerEvents(new BukkitEventListener(), this);
// Initialize the redis listener
redisListener = new BukkitRedisListener();
// Ensure redis is connected; establish a handshake
establishRedisHandshake();
// Initialize bStats metrics
try {
new Metrics(this, METRICS_ID);
} catch (Exception e) {
getLogger().info("Skipped metrics initialization");
}
// Log to console
getLogger().info("Enabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
}
@Override
public void onDisable() {
// Update player data for disconnecting players
if (HuskSyncBukkit.handshakeCompleted && !HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled && Bukkit.getOnlinePlayers().size() > 0) {
getLogger().info("Saving data for remaining online players...");
for (Player player : Bukkit.getOnlinePlayers()) {
PlayerSetter.updatePlayerData(player, false);
// Clear player inventory and ender chest
player.getInventory().clear();
player.getEnderChest().clear();
}
getLogger().info("Data save complete!");
}
// Send termination handshake to proxy
closeRedisHandshake();
// Plugin shutdown logic
getLogger().info("Disabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
}
}

View File

@@ -1,34 +0,0 @@
package net.william278.husksync.bukkit.config;
import net.william278.husksync.Settings;
import org.bukkit.configuration.file.FileConfiguration;
public class ConfigLoader {
public static void loadSettings(FileConfiguration config) throws IllegalArgumentException {
Settings.serverType = Settings.ServerType.BUKKIT;
Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true);
Settings.cluster = config.getString("cluster_id", "main");
Settings.redisHost = config.getString("redis_settings.host", "localhost");
Settings.redisPort = config.getInt("redis_settings.port", 6379);
Settings.redisPassword = config.getString("redis_settings.password", "");
Settings.redisSSL = config.getBoolean("redis_settings.use_ssl", false);
Settings.syncInventories = config.getBoolean("synchronisation_settings.inventories", true);
Settings.syncEnderChests = config.getBoolean("synchronisation_settings.ender_chests", true);
Settings.syncHealth = config.getBoolean("synchronisation_settings.health", true);
Settings.syncHunger = config.getBoolean("synchronisation_settings.hunger", true);
Settings.syncExperience = config.getBoolean("synchronisation_settings.experience", true);
Settings.syncPotionEffects = config.getBoolean("synchronisation_settings.potion_effects", true);
Settings.syncStatistics = config.getBoolean("synchronisation_settings.statistics", true);
Settings.syncGameMode = config.getBoolean("synchronisation_settings.game_mode", true);
Settings.syncAdvancements = config.getBoolean("synchronisation_settings.advancements", true);
Settings.syncLocation = config.getBoolean("synchronisation_settings.location", false);
Settings.syncFlight = config.getBoolean("synchronisation_settings.flight", false);
Settings.useNativeImplementation = config.getBoolean("native_advancement_synchronization", false);
Settings.saveOnWorldSave = config.getBoolean("save_on_world_save", true);
Settings.synchronizationTimeoutRetryDelay = config.getLong("synchronization_timeout_retry_delay", 15L);
}
}

View File

@@ -1,74 +0,0 @@
package net.william278.husksync.bukkit.data;
import java.util.HashMap;
import java.util.HashSet;
import java.util.UUID;
public class BukkitDataCache {
/**
* Map of Player UUIDs to request on join
*/
private static HashSet<UUID> requestOnJoin;
public boolean isPlayerRequestingOnJoin(UUID uuid) {
return requestOnJoin.contains(uuid);
}
public void setRequestOnJoin(UUID uuid) {
requestOnJoin.add(uuid);
}
public void removeRequestOnJoin(UUID uuid) {
requestOnJoin.remove(uuid);
}
/**
* Map of Player UUIDs whose data has not been set yet
*/
private static HashSet<UUID> awaitingDataFetch;
public boolean isAwaitingDataFetch(UUID uuid) {
return awaitingDataFetch.contains(uuid);
}
public void setAwaitingDataFetch(UUID uuid) {
awaitingDataFetch.add(uuid);
}
public void removeAwaitingDataFetch(UUID uuid) {
awaitingDataFetch.remove(uuid);
}
public HashSet<UUID> getAwaitingDataFetch() {
return awaitingDataFetch;
}
/**
* Map of data being viewed by players
*/
private static HashMap<UUID, DataViewer.DataView> viewingPlayerData;
public void setViewing(UUID uuid, DataViewer.DataView dataView) {
viewingPlayerData.put(uuid, dataView);
}
public void removeViewing(UUID uuid) {
viewingPlayerData.remove(uuid);
}
public boolean isViewing(UUID uuid) {
return viewingPlayerData.containsKey(uuid);
}
public DataViewer.DataView getViewing(UUID uuid) {
return viewingPlayerData.get(uuid);
}
// Cache object
public BukkitDataCache() {
requestOnJoin = new HashSet<>();
viewingPlayerData = new HashMap<>();
awaitingDataFetch = new HashSet<>();
}
}

View File

@@ -1,327 +0,0 @@
package net.william278.husksync.bukkit.data;
import net.william278.husksync.redis.RedisMessage;
import org.bukkit.*;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
/**
* Class that contains static methods for serializing and deserializing data from {@link net.william278.husksync.PlayerData}
*/
public class DataSerializer {
/**
* Returns a serialized array of {@link ItemStack}s
*
* @param inventoryContents The contents of the inventory
* @return The serialized inventory contents
*/
public static String serializeInventory(ItemStack[] inventoryContents) {
// Return an empty string if there is no inventory item data to serialize
if (inventoryContents.length == 0) {
return "";
}
// Create an output stream that will be encoded into base 64
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
// Define the length of the inventory array to serialize
bukkitOutputStream.writeInt(inventoryContents.length);
// Write each serialize each ItemStack to the output stream
for (ItemStack inventoryItem : inventoryContents) {
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
}
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
} catch (IOException e) {
throw new IllegalArgumentException("Failed to serialize item stack data");
}
}
/**
* Returns an array of ItemStacks from serialized inventory data. Note: empty slots will be represented by {@code null}
*
* @param inventoryData The serialized {@link ItemStack[]} array
* @return The inventory contents as an array of {@link ItemStack}s
* @throws IOException If the deserialization fails reading data from the InputStream
* @throws ClassNotFoundException If the deserialization class cannot be found
*/
public static ItemStack[] deserializeInventory(String inventoryData) throws IOException, ClassNotFoundException {
// Return empty array if there is no inventory data (set the player as having an empty inventory)
if (inventoryData.isEmpty()) {
return new ItemStack[0];
}
// Create a byte input stream to read the serialized data
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(inventoryData))) {
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
// Read the length of the Bukkit input stream and set the length of the array to this value
ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
// Set the ItemStacks in the array from deserialized ItemStack data
int slotIndex = 0;
for (ItemStack ignored : inventoryContents) {
inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject());
slotIndex++;
}
// Return the finished, serialized inventory contents
return inventoryContents;
}
}
}
/**
* Returns the serialized version of an {@link ItemStack} as a string to object Map
*
* @param item The {@link ItemStack} to serialize
* @return The serialized {@link ItemStack}
*/
private static Map<String, Object> serializeItemStack(ItemStack item) {
return item != null ? item.serialize() : null;
}
/**
* Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream}
*
* @param serializedItemStack The serialized item stack; a String-Object map
* @return The deserialized {@link ItemStack}
*/
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
private static ItemStack deserializeItemStack(Object serializedItemStack) {
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
}
/**
* Returns a serialized array of {@link PotionEffect}s
*
* @param potionEffects The potion effect array
* @return The serialized potion effects
*/
public static String serializePotionEffects(PotionEffect[] potionEffects) {
// Return an empty string if there are no effects to serialize
if (potionEffects.length == 0) {
return "";
}
// Create an output stream that will be encoded into base 64
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
// Define the length of the potion effect array to serialize
bukkitOutputStream.writeInt(potionEffects.length);
// Write each serialize each PotionEffect to the output stream
for (PotionEffect potionEffect : potionEffects) {
bukkitOutputStream.writeObject(serializePotionEffect(potionEffect));
}
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
} catch (IOException e) {
throw new IllegalArgumentException("Failed to serialize potion effect data");
}
}
/**
* Returns an array of ItemStacks from serialized potion effect data
*
* @param potionEffectData The serialized {@link PotionEffect[]} array
* @return The {@link PotionEffect}s
* @throws IOException If the deserialization fails reading data from the InputStream
* @throws ClassNotFoundException If the deserialization class cannot be found
*/
public static PotionEffect[] deserializePotionEffects(String potionEffectData) throws IOException, ClassNotFoundException {
// Return empty array if there is no potion effect data (don't apply any effects to the player)
if (potionEffectData.isEmpty()) {
return new PotionEffect[0];
}
// Create a byte input stream to read the serialized data
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) {
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
// Read the length of the Bukkit input stream and set the length of the array to this value
PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()];
// Set the potion effects in the array from deserialized PotionEffect data
int potionIndex = 0;
for (PotionEffect ignored : potionEffects) {
potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject());
potionIndex++;
}
// Return the finished, serialized potion effect array
return potionEffects;
}
}
}
/**
* Returns the serialized version of an {@link ItemStack} as a string to object Map
*
* @param potionEffect The {@link ItemStack} to serialize
* @return The serialized {@link ItemStack}
*/
private static Map<String, Object> serializePotionEffect(PotionEffect potionEffect) {
return potionEffect != null ? potionEffect.serialize() : null;
}
/**
* Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream}
*
* @param serializedPotionEffect The serialized potion effect; a String-Object map
* @return The deserialized {@link PotionEffect}
*/
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
private static PotionEffect deserializePotionEffect(Object serializedPotionEffect) {
return serializedPotionEffect != null ? new PotionEffect((Map<String, Object>) serializedPotionEffect) : null;
}
public static me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation deserializePlayerLocationData(String serializedLocationData) throws IOException {
if (serializedLocationData.isEmpty()) {
return null;
}
try {
return (me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation) RedisMessage.deserialize(serializedLocationData);
} catch (ClassNotFoundException e) {
throw new IOException("Unable to decode class type.", e);
}
}
public static String getSerializedLocation(Player player) throws IOException {
final Location playerLocation = player.getLocation();
return RedisMessage.serialize(new me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation(playerLocation.getX(), playerLocation.getY(), playerLocation.getZ(),
playerLocation.getYaw(), playerLocation.getPitch(), player.getWorld().getName(), player.getWorld().getEnvironment()));
}
/**
* Deserializes a player's advancement data as serialized with {@link #getSerializedAdvancements(Player)} into {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate} data.
*
* @param serializedAdvancementData The serialized advancement data {@link String}
* @return The deserialized {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate} for the player
* @throws IOException If the deserialization fails
*/
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
public static List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> deserializeAdvancementData(String serializedAdvancementData) throws IOException {
if (serializedAdvancementData.isEmpty()) {
return new ArrayList<>();
}
try {
List<?> deserialize = (List<?>) RedisMessage.deserialize(serializedAdvancementData);
// Migrate old AdvancementRecord into date format
if (!deserialize.isEmpty() && deserialize.get(0) instanceof me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecord) {
deserialize = ((List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecord>) deserialize).stream()
.map(o -> new me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate(
o.advancementKey(),
o.awardedAdvancementCriteria()
)).toList();
}
return (List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate>) deserialize;
} catch (ClassNotFoundException e) {
throw new IOException("Unable to decode class type.", e);
}
}
/**
* 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 {
Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
ArrayList<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementData = new ArrayList<>();
while (serverAdvancements.hasNext()) {
final AdvancementProgress progress = player.getAdvancementProgress(serverAdvancements.next());
final NamespacedKey advancementKey = progress.getAdvancement().getKey();
final Map<String, Date> awardedCriteria = new HashMap<>();
progress.getAwardedCriteria().forEach(s -> awardedCriteria.put(s, progress.getDateAwarded(s)));
advancementData.add(new me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate(advancementKey.getNamespace() + ":" + advancementKey.getKey(), awardedCriteria));
}
return RedisMessage.serialize(advancementData);
}
/**
* Deserializes a player's statistic data as serialized with {@link #getSerializedStatisticData(Player)} into {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData}.
*
* @param serializedStatisticData The serialized statistic data {@link String}
* @return The deserialized {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData} for the player
* @throws IOException If the deserialization fails
*/
public static me.william278.husksync.bukkit.data.DataSerializer.StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException {
if (serializedStatisticData.isEmpty()) {
return new me.william278.husksync.bukkit.data.DataSerializer.StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
}
try {
return (me.william278.husksync.bukkit.data.DataSerializer.StatisticData) RedisMessage.deserialize(serializedStatisticData);
} catch (ClassNotFoundException e) {
throw new IOException("Unable to decode class type.", e);
}
}
/**
* 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 {
HashMap<Statistic, Integer> untypedStatisticValues = new HashMap<>();
HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues = new HashMap<>();
HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues = new HashMap<>();
HashMap<Statistic, HashMap<EntityType, Integer>> entityStatisticValues = new HashMap<>();
for (Statistic statistic : Statistic.values()) {
switch (statistic.getType()) {
case ITEM -> {
HashMap<Material, Integer> itemValues = new HashMap<>();
for (Material itemMaterial : Arrays.stream(Material.values()).filter(Material::isItem).toList()) {
itemValues.put(itemMaterial, player.getStatistic(statistic, itemMaterial));
}
itemStatisticValues.put(statistic, itemValues);
}
case BLOCK -> {
HashMap<Material, Integer> blockValues = new HashMap<>();
for (Material blockMaterial : Arrays.stream(Material.values()).filter(Material::isBlock).toList()) {
blockValues.put(blockMaterial, player.getStatistic(statistic, blockMaterial));
}
blockStatisticValues.put(statistic, blockValues);
}
case ENTITY -> {
HashMap<EntityType, Integer> entityValues = new HashMap<>();
for (EntityType type : Arrays.stream(EntityType.values()).filter(EntityType::isAlive).toList()) {
entityValues.put(type, player.getStatistic(statistic, type));
}
entityStatisticValues.put(statistic, entityValues);
}
case UNTYPED -> untypedStatisticValues.put(statistic, player.getStatistic(statistic));
}
}
me.william278.husksync.bukkit.data.DataSerializer.StatisticData statisticData = new me.william278.husksync.bukkit.data.DataSerializer.StatisticData(untypedStatisticValues, blockStatisticValues, itemStatisticValues, entityStatisticValues);
return RedisMessage.serialize(statisticData);
}
}

View File

@@ -1,115 +0,0 @@
package net.william278.husksync.bukkit.data;
import net.william278.husksync.HuskSyncBukkit;
import net.william278.husksync.PlayerData;
import net.william278.husksync.Settings;
import net.william278.husksync.bukkit.util.PlayerSetter;
import net.william278.husksync.redis.RedisMessage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import java.io.IOException;
/**
* Class used for managing viewing inventories using inventory-see command
*/
public class DataViewer {
/**
* Show a viewer's data to a viewer
*
* @param viewer The viewing {@link Player} who will see the data
* @param data The {@link DataView} to show the viewer
* @throws IOException If an exception occurred deserializing item data
*/
public static void showData(Player viewer, DataView data) throws IOException, ClassNotFoundException {
// Show an inventory with the viewer's inventory and equipment
viewer.closeInventory();
viewer.openInventory(createInventory(viewer, data));
// Set the viewer as viewing
HuskSyncBukkit.bukkitCache.setViewing(viewer.getUniqueId(), data);
}
/**
* Handles what happens after a data viewer finishes viewing data
*
* @param viewer The viewing {@link Player} who was looking at data
* @param inventory The {@link Inventory} that was being viewed
* @throws IOException If an exception occurred serializing item data
*/
public static void stopShowing(Player viewer, Inventory inventory) throws IOException {
// Get the DataView the player was looking at
DataView dataView = HuskSyncBukkit.bukkitCache.getViewing(viewer.getUniqueId());
// Set the player as no longer viewing an inventory
HuskSyncBukkit.bukkitCache.removeViewing(viewer.getUniqueId());
// Get and update the PlayerData with the new item data
PlayerData playerData = dataView.playerData();
String serializedItemData = DataSerializer.serializeInventory(inventory.getContents());
switch (dataView.inventoryType()) {
case INVENTORY -> playerData.setSerializedInventory(serializedItemData);
case ENDER_CHEST -> playerData.setSerializedEnderChest(serializedItemData);
}
// Send a redis message with the updated data after the viewing
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
RedisMessage.serialize(playerData), Boolean.toString(true))
.send();
}
/**
* Creates the inventory object that the viewer will see
*
* @param viewer The {@link Player} who will view the data
* @param data The {@link DataView} data to view
* @return The {@link Inventory} that the viewer will see
* @throws IOException If an exception occurred deserializing item data
*/
private static Inventory createInventory(Player viewer, DataView data) throws IOException, ClassNotFoundException {
Inventory inventory = switch (data.inventoryType) {
case INVENTORY -> Bukkit.createInventory(viewer, 45, data.ownerName + "'s Inventory");
case ENDER_CHEST -> Bukkit.createInventory(viewer, 27, data.ownerName + "'s Ender Chest");
};
PlayerSetter.setInventory(inventory, data.getDeserializedData());
return inventory;
}
/**
* Represents Player Data being viewed by a {@link Player}
*/
public record DataView(PlayerData playerData, String ownerName, InventoryType inventoryType) {
/**
* What kind of item data is being viewed
*/
public enum InventoryType {
/**
* A player's inventory
*/
INVENTORY,
/**
* A player's ender chest
*/
ENDER_CHEST
}
/**
* Gets the deserialized data currently being viewed
*
* @return The deserialized item data, as an {@link ItemStack[]} array
* @throws IOException If an exception occurred deserializing item data
*/
public ItemStack[] getDeserializedData() throws IOException, ClassNotFoundException {
return switch (inventoryType) {
case INVENTORY -> DataSerializer.deserializeInventory(playerData.getSerializedInventory());
case ENDER_CHEST -> DataSerializer.deserializeInventory(playerData.getSerializedEnderChest());
};
}
}
}

View File

@@ -1,38 +0,0 @@
package net.william278.husksync.bukkit.events;
import net.william278.husksync.PlayerData;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
/**
* Represents an event that will be fired when a {@link Player} has finished being synchronised with the correct {@link PlayerData}.
*/
public class SyncCompleteEvent extends PlayerEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final PlayerData data;
public SyncCompleteEvent(Player player, PlayerData data) {
super(player);
this.data = data;
}
/**
* Returns the {@link PlayerData} which has just been set on the {@link Player}
* @return The {@link PlayerData} that has been set
*/
public PlayerData getData() {
return data;
}
@Override
public @NotNull HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -1,70 +0,0 @@
package net.william278.husksync.bukkit.events;
import net.william278.husksync.PlayerData;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
/**
* Represents an event that will be fired before a {@link Player} is about to be synchronised with their {@link PlayerData}.
*/
public class SyncEvent extends PlayerEvent implements Cancellable {
private boolean cancelled;
private static final HandlerList HANDLER_LIST = new HandlerList();
private PlayerData data;
public SyncEvent(Player player, PlayerData data) {
super(player);
this.data = data;
}
/**
* Returns the {@link PlayerData} which has just been set on the {@link Player}
*
* @return The {@link PlayerData} that has been set
*/
public PlayerData getData() {
return data;
}
/**
* Sets the {@link PlayerData} to be synchronised to this player
*
* @param data The {@link PlayerData} to set to the player
*/
public void setData(PlayerData data) {
this.data = data;
}
@Override
public @NotNull HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
/**
* Gets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins
*
* @return true if this event is cancelled
*/
@Override
public boolean isCancelled() {
return cancelled;
}
/**
* Sets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins.
*
* @param cancel true if you wish to cancel this event
*/
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@@ -1,166 +0,0 @@
package net.william278.husksync.bukkit.listener;
import net.william278.husksync.HuskSyncBukkit;
import net.william278.husksync.Settings;
import net.william278.husksync.bukkit.data.DataViewer;
import net.william278.husksync.bukkit.util.PlayerSetter;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.*;
import org.bukkit.event.world.WorldSaveEvent;
import java.io.IOException;
import java.util.logging.Level;
public class BukkitEventListener implements Listener {
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerQuit(PlayerQuitEvent event) {
// When a player leaves a Bukkit server
final Player player = event.getPlayer();
// If the player was awaiting data fetch, remove them and prevent data from being overwritten
if (HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
return;
}
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled)
return; // If the plugin has not been initialized correctly
// Update the player's data
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// Update data to proxy
PlayerSetter.updatePlayerData(player, true);
// Clear player inventory and ender chest
player.getInventory().clear();
player.getEnderChest().clear();
});
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerJoin(PlayerJoinEvent event) {
if (!plugin.isEnabled()) return; // If the plugin has not been initialized correctly
// When a player joins a Bukkit server
final Player player = event.getPlayer();
// Mark the player as awaiting data fetch
HuskSyncBukkit.bukkitCache.setAwaitingDataFetch(player.getUniqueId());
if (!HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) {
return; // If the data handshake has not been completed yet (or MySqlPlayerDataBridge is installed)
}
// Send a redis message requesting the player data (if they need to)
if (HuskSyncBukkit.bukkitCache.isPlayerRequestingOnJoin(player.getUniqueId())) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
PlayerSetter.requestPlayerData(player.getUniqueId());
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
}
});
} else {
// If the player's data wasn't set after the synchronization timeout retry delay ticks, ensure it will be
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
if (player.isOnline()) {
try {
if (HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
PlayerSetter.requestPlayerData(player.getUniqueId());
}
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
}
}
}, Settings.synchronizationTimeoutRetryDelay);
}
}
@EventHandler
public void onInventoryClose(InventoryCloseEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId()))
return; // If the plugin has not been initialized correctly
// When a player closes an Inventory
final Player player = (Player) event.getPlayer();
// Handle a player who has finished viewing a player's item data
if (HuskSyncBukkit.bukkitCache.isViewing(player.getUniqueId())) {
try {
DataViewer.stopShowing(player, event.getInventory());
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to serialize updated item data", e);
}
}
}
/*
* Events to cancel if the player has not been set yet
*/
@EventHandler(priority = EventPriority.HIGHEST)
public void onDropItem(PlayerDropItemEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPickupItem(EntityPickupItemEvent event) {
if (event.getEntity() instanceof Player player) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerInteract(PlayerInteractEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onBlockPlace(BlockPlaceEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onBlockBreak(BlockBreakEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onInventoryOpen(InventoryOpenEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
@EventHandler(priority = EventPriority.NORMAL)
public void onWorldSave(WorldSaveEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted) {
return;
}
for (Player playerInWorld : event.getWorld().getPlayers()) {
PlayerSetter.updatePlayerData(playerInWorld, false);
}
}
}

View File

@@ -1,221 +0,0 @@
package net.william278.husksync.bukkit.listener;
import de.themoep.minedown.MineDown;
import net.william278.husksync.HuskSyncBukkit;
import net.william278.husksync.PlayerData;
import net.william278.husksync.Settings;
import net.william278.husksync.bukkit.config.ConfigLoader;
import net.william278.husksync.bukkit.data.DataViewer;
import net.william278.husksync.bukkit.migrator.MPDBDeserializer;
import net.william278.husksync.bukkit.util.PlayerSetter;
import net.william278.husksync.migrator.MPDBPlayerData;
import net.william278.husksync.redis.RedisListener;
import net.william278.husksync.redis.RedisMessage;
import net.william278.husksync.util.MessageManager;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.IOException;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
public class BukkitRedisListener extends RedisListener {
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
public static HashMap<UUID, CompletableFuture<PlayerData>> apiRequests = new HashMap<>();
// Initialize the listener on the bukkit server
public BukkitRedisListener() {
super();
listen();
}
/**
* Handle an incoming {@link RedisMessage}
*
* @param message The {@link RedisMessage} to handle
*/
@Override
public void handleMessage(RedisMessage message) {
// Ignore messages for proxy servers
if (!message.getMessageTarget().targetServerType().equals(Settings.ServerType.BUKKIT)) {
return;
}
// Ignore messages if the plugin is disabled
if (!plugin.isEnabled()) {
return;
}
// Ignore messages for other clusters if applicable
final String targetClusterId = message.getMessageTarget().targetClusterId();
if (targetClusterId != null) {
if (!targetClusterId.equalsIgnoreCase(Settings.cluster)) {
return;
}
}
// Handle the incoming redis message; either for a specific player or the system
if (message.getMessageTarget().targetPlayerUUID() == null) {
switch (message.getMessageType()) {
case REQUEST_DATA_ON_JOIN -> {
UUID playerUUID = UUID.fromString(message.getMessageDataElements()[1]);
switch (RedisMessage.RequestOnJoinUpdateType.valueOf(message.getMessageDataElements()[0])) {
case ADD_REQUESTER -> HuskSyncBukkit.bukkitCache.setRequestOnJoin(playerUUID);
case REMOVE_REQUESTER -> HuskSyncBukkit.bukkitCache.removeRequestOnJoin(playerUUID);
}
}
case CONNECTION_HANDSHAKE -> {
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
String proxyBrand = message.getMessageDataElements()[1];
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
HuskSyncBukkit.handshakeCompleted = true;
log(Level.INFO, "Completed handshake with " + proxyBrand + " proxy (" + serverUUID + ")");
// If there are any players awaiting a data update, request it
for (UUID uuid : HuskSyncBukkit.bukkitCache.getAwaitingDataFetch()) {
try {
PlayerSetter.requestPlayerData(uuid);
} catch (IOException e) {
log(Level.SEVERE, "Failed to serialize handshake message data");
}
}
}
}
case TERMINATE_HANDSHAKE -> {
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
String proxyBrand = message.getMessageDataElements()[1];
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
HuskSyncBukkit.handshakeCompleted = false;
log(Level.WARNING, proxyBrand + " proxy has terminated communications; attempting to re-establish (" + serverUUID + ")");
// Attempt to re-establish communications via another handshake
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, HuskSyncBukkit::establishRedisHandshake, 20);
}
}
case DECODE_MPDB_DATA -> {
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
String encodedData = message.getMessageDataElements()[1];
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
try {
MPDBPlayerData data = (MPDBPlayerData) RedisMessage.deserialize(encodedData);
new RedisMessage(RedisMessage.MessageType.DECODED_MPDB_DATA_SET,
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
RedisMessage.serialize(MPDBDeserializer.convertMPDBData(data)),
data.playerName)
.send();
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to serialize encoded MPDB data");
}
}
});
}
case API_DATA_RETURN -> {
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
if (apiRequests.containsKey(requestUUID)) {
try {
final PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
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 (apiRequests.containsKey(requestUUID)) {
apiRequests.get(requestUUID).cancel(true);
}
}
case RELOAD_CONFIG -> {
plugin.reloadConfig();
ConfigLoader.loadSettings(plugin.getConfig());
}
}
} else {
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getUniqueId().equals(message.getMessageTarget().targetPlayerUUID())) {
switch (message.getMessageType()) {
case PLAYER_DATA_SET -> {
if (HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) return;
try {
// Deserialize the received PlayerData
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
// Set the player's data
PlayerSetter.setPlayerFrom(player, data);
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize PlayerData when handling data from the proxy");
e.printStackTrace();
}
}
case SEND_PLUGIN_INFORMATION -> {
String proxyBrand = message.getMessageDataElements()[0];
String proxyVersion = message.getMessageDataElements()[1];
assert plugin.getDescription().getDescription() != null;
player.spigot().sendMessage(new MineDown(MessageManager.PLUGIN_INFORMATION.toString()
.replaceAll("%plugin_description%", plugin.getDescription().getDescription())
.replaceAll("%proxy_brand%", proxyBrand)
.replaceAll("%proxy_version%", proxyVersion)
.replaceAll("%bukkit_brand%", Bukkit.getName())
.replaceAll("%bukkit_version%", plugin.getDescription().getVersion()))
.toComponent());
}
case OPEN_INVENTORY -> {
// Get the name of the inventory owner
String inventoryOwnerName = message.getMessageDataElements()[0];
// Synchronously do inventory setting, etc
Bukkit.getScheduler().runTask(plugin, () -> {
try {
// Get that player's data
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
// Show the data to the player
DataViewer.showData(player, new DataViewer.DataView(data, inventoryOwnerName, DataViewer.DataView.InventoryType.INVENTORY));
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize PlayerData when handling inventory-see data from the proxy");
e.printStackTrace();
}
});
}
case OPEN_ENDER_CHEST -> {
// Get the name of the inventory owner
String enderChestOwnerName = message.getMessageDataElements()[0];
// Synchronously do inventory setting, etc
Bukkit.getScheduler().runTask(plugin, () -> {
try {
// Get that player's data
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
// Show the data to the player
DataViewer.showData(player, new DataViewer.DataView(data, enderChestOwnerName, DataViewer.DataView.InventoryType.ENDER_CHEST));
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize PlayerData when handling ender chest-see data from the proxy");
e.printStackTrace();
}
});
}
}
return;
}
}
}
}
/**
* Log to console
*
* @param level The {@link Level} to log
* @param message Message to log
*/
@Override
public void log(Level level, String message) {
plugin.getLogger().log(level, message);
}
}

View File

@@ -1,87 +0,0 @@
package net.william278.husksync.bukkit.migrator;
import net.william278.husksync.HuskSyncBukkit;
import net.william278.husksync.PlayerData;
import net.william278.husksync.bukkit.data.DataSerializer;
import net.william278.husksync.bukkit.util.PlayerSetter;
import net.william278.husksync.migrator.MPDBPlayerData;
import net.william278.mpdbconverter.MPDBConverter;
import org.bukkit.Bukkit;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import java.util.logging.Level;
public class MPDBDeserializer {
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
// Instance of MySqlPlayerDataBridge
private static MPDBConverter mpdbConverter;
public static void setMySqlPlayerDataBridge() {
Plugin mpdbPlugin = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
assert mpdbPlugin != null;
mpdbConverter = MPDBConverter.getInstance(mpdbPlugin);
}
/**
* Convert MySqlPlayerDataBridge ({@link MPDBPlayerData}) data to HuskSync's {@link PlayerData}
*
* @param mpdbPlayerData The {@link MPDBPlayerData} to convert
* @return The converted {@link PlayerData}
*/
public static PlayerData convertMPDBData(MPDBPlayerData mpdbPlayerData) {
PlayerData playerData = PlayerData.DEFAULT_PLAYER_DATA(mpdbPlayerData.playerUUID);
playerData.useDefaultData = false;
if (!HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) {
plugin.getLogger().log(Level.SEVERE, "MySqlPlayerDataBridge is not installed, failed to serialize data!");
return null;
}
// Convert the data
try {
// Set inventory contents
Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
if (!mpdbPlayerData.inventoryData.isEmpty() && !mpdbPlayerData.inventoryData.equalsIgnoreCase("none")) {
PlayerSetter.setInventory(inventory, mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.inventoryData));
}
// Set armor (if there is data; MPDB stores empty data with literally the word "none". Obviously.)
int armorSlot = 36;
if (!mpdbPlayerData.armorData.isEmpty() && !mpdbPlayerData.armorData.equalsIgnoreCase("none")) {
ItemStack[] armorItems = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.armorData);
for (ItemStack armorPiece : armorItems) {
if (armorPiece != null) {
inventory.setItem(armorSlot, armorPiece);
}
armorSlot++;
}
}
// Now apply the contents and clear the temporary inventory variable
playerData.setSerializedInventory(DataSerializer.serializeInventory(inventory.getContents()));
// Set ender chest (again, if there is data)
ItemStack[] enderChestData;
if (!mpdbPlayerData.enderChestData.isEmpty() && !mpdbPlayerData.enderChestData.equalsIgnoreCase("none")) {
enderChestData = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.enderChestData);
} else {
enderChestData = new ItemStack[0];
}
playerData.setSerializedEnderChest(DataSerializer.serializeInventory(enderChestData));
// Set experience
playerData.setExpLevel(mpdbPlayerData.expLevel);
playerData.setExpProgress(mpdbPlayerData.expProgress);
playerData.setTotalExperience(mpdbPlayerData.totalExperience);
} catch (Exception e) {
plugin.getLogger().log(Level.WARNING, "Failed to convert MPDB data to HuskSync's format!");
e.printStackTrace();
}
return playerData;
}
}

View File

@@ -1,20 +0,0 @@
package net.william278.husksync.bukkit.util;
import net.william278.husksync.HuskSyncBukkit;
import net.william278.husksync.util.UpdateChecker;
import java.util.logging.Level;
public class BukkitUpdateChecker extends UpdateChecker {
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
public BukkitUpdateChecker() {
super(plugin.getDescription().getVersion());
}
@Override
public void log(Level level, String message) {
plugin.getLogger().log(level, message);
}
}

View File

@@ -1,479 +0,0 @@
package net.william278.husksync.bukkit.util;
import net.william278.husksync.HuskSyncBukkit;
import net.william278.husksync.PlayerData;
import net.william278.husksync.Settings;
import net.william278.husksync.bukkit.events.SyncCompleteEvent;
import net.william278.husksync.bukkit.events.SyncEvent;
import net.william278.husksync.bukkit.data.DataSerializer;
import net.william278.husksync.bukkit.util.nms.AdvancementUtils;
import net.william278.husksync.redis.RedisMessage;
import org.bukkit.*;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
public class PlayerSetter {
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
/**
* Returns the new serialized PlayerData for a player.
*
* @param player The {@link Player} to get the new serialized PlayerData for
* @return The {@link PlayerData}, serialized as a {@link String}
* @throws IOException If the serialization fails
*/
private static String getNewSerializedPlayerData(Player player) throws IOException {
final double maxHealth = getMaxHealth(player); // Get the player's max health (used to determine health as well)
return RedisMessage.serialize(new PlayerData(player.getUniqueId(),
DataSerializer.serializeInventory(player.getInventory().getContents()),
DataSerializer.serializeInventory(player.getEnderChest().getContents()),
Math.min(player.getHealth(), maxHealth),
maxHealth,
player.isHealthScaled() ? player.getHealthScale() : 0D,
player.getFoodLevel(),
player.getSaturation(),
player.getExhaustion(),
player.getInventory().getHeldItemSlot(),
DataSerializer.serializePotionEffects(getPlayerPotionEffects(player)),
player.getTotalExperience(),
player.getLevel(),
player.getExp(),
player.getGameMode().toString(),
DataSerializer.getSerializedStatisticData(player),
player.isFlying(),
DataSerializer.getSerializedAdvancements(player),
DataSerializer.getSerializedLocation(player)));
}
/**
* Returns a {@link Player}'s maximum health, minus any health boost effects
*
* @param player The {@link Player} to get the maximum health of
* @return The {@link Player}'s max health
*/
private static double getMaxHealth(Player player) {
double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
// If the player has additional health bonuses from synchronised potion effects, subtract these from this number as they are synchronised separately
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20D) {
PotionEffect healthBoostEffect = player.getPotionEffect(PotionEffectType.HEALTH_BOOST);
assert healthBoostEffect != null;
double healthBoostBonus = 4 * (healthBoostEffect.getAmplifier() + 1);
maxHealth -= healthBoostBonus;
}
return maxHealth;
}
/**
* Returns a {@link Player}'s active potion effects in a {@link PotionEffect} array
*
* @param player The {@link Player} to get the effects of
* @return The {@link PotionEffect} array
*/
private static PotionEffect[] getPlayerPotionEffects(Player player) {
PotionEffect[] potionEffects = new PotionEffect[player.getActivePotionEffects().size()];
int arrayIndex = 0;
for (PotionEffect effect : player.getActivePotionEffects()) {
potionEffects[arrayIndex] = effect;
arrayIndex++;
}
return potionEffects;
}
/**
* Update a {@link Player}'s data, sending it to the proxy
*
* @param player {@link Player} to send data to proxy
* @param bounceBack whether the plugin should bounce-back the updated data to the player (used for server switching)
*/
public static void updatePlayerData(Player player, boolean bounceBack) {
// Send a redis message with the player's last updated PlayerData version UUID and their new PlayerData
try {
final String serializedPlayerData = getNewSerializedPlayerData(player);
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
serializedPlayerData, Boolean.toString(bounceBack)).send();
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
}
}
/**
* Request a {@link Player}'s data from the proxy
*
* @param playerUUID The {@link UUID} of the {@link Player} to fetch PlayerData from
* @throws IOException If the request Redis message data fails to serialize
*/
public static void requestPlayerData(UUID playerUUID) throws IOException {
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REQUEST,
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
playerUUID.toString()).send();
}
/**
* Set a player from their PlayerData, based on settings
*
* @param player The {@link Player} to set
* @param dataToSet The {@link PlayerData} to assign to the player
*/
public static void setPlayerFrom(Player player, PlayerData dataToSet) {
Bukkit.getScheduler().runTask(plugin, () -> {
// Handle the SyncEvent
SyncEvent syncEvent = new SyncEvent(player, dataToSet);
Bukkit.getPluginManager().callEvent(syncEvent);
final PlayerData data = syncEvent.getData();
if (syncEvent.isCancelled()) {
return;
}
// If the data is flagged as being default data, skip setting
if (data.useDefaultData) {
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
return;
}
// Clear player
player.getInventory().clear();
player.getEnderChest().clear();
player.setExp(0);
player.setLevel(0);
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
// Set the player's data from the PlayerData
try {
if (Settings.syncAdvancements) {
List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementRecords
= DataSerializer.deserializeAdvancementData(data.getSerializedAdvancements());
if (Settings.useNativeImplementation) {
try {
nativeSyncPlayerAdvancements(player, advancementRecords);
} catch (Exception e) {
plugin.getLogger().log(Level.WARNING,
"Your server does not support a native implementation of achievements synchronization");
plugin.getLogger().log(Level.WARNING,
"Your server version is {0}. Please disable using native implementation!", Bukkit.getVersion());
Settings.useNativeImplementation = false;
setPlayerAdvancements(player, advancementRecords, data);
plugin.getLogger().log(Level.SEVERE, e.getMessage(), e);
}
} else {
setPlayerAdvancements(player, advancementRecords, data);
}
}
if (Settings.syncInventories) {
setPlayerInventory(player, DataSerializer.deserializeInventory(data.getSerializedInventory()));
player.getInventory().setHeldItemSlot(data.getSelectedSlot());
}
if (Settings.syncEnderChests) {
setPlayerEnderChest(player, DataSerializer.deserializeInventory(data.getSerializedEnderChest()));
}
if (Settings.syncHealth) {
setPlayerHealth(player, data.getHealth(), data.getMaxHealth(), data.getHealthScale());
}
if (Settings.syncHunger) {
player.setFoodLevel(data.getHunger());
player.setSaturation(data.getSaturation());
player.setExhaustion(data.getSaturationExhaustion());
}
if (Settings.syncExperience) {
// This is also handled when syncing advancements to ensure its correct
setPlayerExperience(player, data);
}
if (Settings.syncPotionEffects) {
setPlayerPotionEffects(player, DataSerializer.deserializePotionEffects(data.getSerializedEffectData()));
}
if (Settings.syncStatistics) {
setPlayerStatistics(player, DataSerializer.deserializeStatisticData(data.getSerializedStatistics()));
}
if (Settings.syncGameMode) {
player.setGameMode(GameMode.valueOf(data.getGameMode()));
}
if (Settings.syncLocation) {
setPlayerLocation(player, DataSerializer.deserializePlayerLocationData(data.getSerializedLocation()));
}
if (Settings.syncFlight) {
if (data.isFlying()) {
player.setAllowFlight(true);
}
player.setFlying(player.getAllowFlight() && data.isFlying());
}
// Handle the SyncCompleteEvent
Bukkit.getPluginManager().callEvent(new SyncCompleteEvent(player, data));
} catch (IOException | ClassNotFoundException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to deserialize PlayerData", e);
}
});
}
/**
* Sets a player's ender chest from a set of {@link ItemStack}s
*
* @param player The player to set the inventory of
* @param items The array of {@link ItemStack}s to set
*/
private static void setPlayerEnderChest(Player player, ItemStack[] items) {
setInventory(player.getEnderChest(), items);
}
/**
* Sets a player's inventory from a set of {@link ItemStack}s
*
* @param player The player to set the inventory of
* @param items The array of {@link ItemStack}s to set
*/
private static void setPlayerInventory(Player player, ItemStack[] items) {
setInventory(player.getInventory(), items);
}
/**
* Sets an inventory's contents from an array of {@link ItemStack}s
*
* @param inventory The inventory to set
* @param items The {@link ItemStack}s to fill it with
*/
public static void setInventory(Inventory inventory, ItemStack[] items) {
inventory.clear();
int index = 0;
for (ItemStack item : items) {
if (item != null) {
inventory.setItem(index, item);
}
index++;
}
}
/**
* Set a player's current potion effects from a set of {@link PotionEffect[]}
*
* @param player The player to set the potion effects of
* @param effects The array of {@link PotionEffect}s to set
*/
private static void setPlayerPotionEffects(Player player, PotionEffect[] effects) {
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
for (PotionEffect effect : effects) {
player.addPotionEffect(effect);
}
}
private static void nativeSyncPlayerAdvancements(final Player player, final List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementRecords) {
final Object playerAdvancements = AdvancementUtils.getPlayerAdvancements(player);
// Clear
AdvancementUtils.clearPlayerAdvancements(playerAdvancements);
AdvancementUtils.clearVisibleAdvancements(playerAdvancements);
advancementRecords.forEach(advancementRecord -> {
NamespacedKey namespacedKey = Objects.requireNonNull(
NamespacedKey.fromString(advancementRecord.key()),
"Invalid Namespaced key of " + advancementRecord.key()
);
Advancement bukkitAdvancement = Bukkit.getAdvancement(namespacedKey);
if (bukkitAdvancement == null) {
plugin.getLogger().log(Level.WARNING, "Ignored advancement '{0}' - it doesn't exist anymore?", namespacedKey);
return;
}
Object advancement = AdvancementUtils.getHandle(bukkitAdvancement);
Map<String, Date> criteriaList = advancementRecord.criteriaMap();
{
Map<String, Object> nativeCriteriaMap = new HashMap<>();
criteriaList.forEach((criteria, date) ->
nativeCriteriaMap.put(criteria, AdvancementUtils.newCriterionProgress(date))
);
Object nativeAdvancementProgress = AdvancementUtils.newAdvancementProgress(nativeCriteriaMap);
AdvancementUtils.startProgress(playerAdvancements, advancement, nativeAdvancementProgress);
}
});
AdvancementUtils.ensureAllVisible(playerAdvancements); // Set all completed advancement is visible
AdvancementUtils.markPlayerAdvancementsFirst(playerAdvancements); // Mark the sending of visible advancement as the first
}
/**
* Update a player's advancements and progress to match the advancementData
*
* @param player The player to set the advancements of
* @param advancementData The ArrayList of {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate}s to set
*/
private static void setPlayerAdvancements(Player player, List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementData, PlayerData data) {
// Temporarily disable advancement announcing if needed
boolean announceAdvancementUpdate = false;
if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
announceAdvancementUpdate = true;
}
final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate;
// Run async because advancement loading is very slow
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// Apply the advancements to the player
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
while (serverAdvancements.hasNext()) { // Iterate through all advancements
boolean correctExperienceCheck = false; // Determines whether the experience might have changed warranting an update
Advancement advancement = serverAdvancements.next();
AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
for (me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate record : advancementData) {
// If the advancement is one on the data
if (record.key().equals(advancement.getKey().getNamespace() + ":" + advancement.getKey().getKey())) {
// Award all criteria that the player does not have that they do on the cache
ArrayList<String> currentlyAwardedCriteria = new ArrayList<>(playerProgress.getAwardedCriteria());
for (String awardCriteria : record.criteriaMap().keySet()) {
if (!playerProgress.getAwardedCriteria().contains(awardCriteria)) {
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).awardCriteria(awardCriteria));
correctExperienceCheck = true;
}
currentlyAwardedCriteria.remove(awardCriteria);
}
// Revoke all criteria that the player does have but should not
for (String awardCriteria : currentlyAwardedCriteria) {
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).revokeCriteria(awardCriteria));
}
break;
}
}
// Update the player's experience in case the advancement changed that
if (correctExperienceCheck) {
if (Settings.syncExperience) {
setPlayerExperience(player, data);
}
}
}
// Re-enable announcing advancements (back on main thread again)
Bukkit.getScheduler().runTask(plugin, () -> {
if (finalAnnounceAdvancementUpdate) {
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
}
});
});
}
/**
* Set a player's statistics (in the Statistic menu)
*
* @param player The player to set the statistics of
* @param statisticData The {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData} to set
*/
private static void setPlayerStatistics(Player player, me.william278.husksync.bukkit.data.DataSerializer.StatisticData statisticData) {
// Set untyped statistics
for (Statistic statistic : statisticData.untypedStatisticValues().keySet()) {
player.setStatistic(statistic, statisticData.untypedStatisticValues().get(statistic));
}
// Set block statistics
for (Statistic statistic : statisticData.blockStatisticValues().keySet()) {
for (Material blockMaterial : statisticData.blockStatisticValues().get(statistic).keySet()) {
player.setStatistic(statistic, blockMaterial, statisticData.blockStatisticValues().get(statistic).get(blockMaterial));
}
}
// Set item statistics
for (Statistic statistic : statisticData.itemStatisticValues().keySet()) {
for (Material itemMaterial : statisticData.itemStatisticValues().get(statistic).keySet()) {
player.setStatistic(statistic, itemMaterial, statisticData.itemStatisticValues().get(statistic).get(itemMaterial));
}
}
// Set entity statistics
for (Statistic statistic : statisticData.entityStatisticValues().keySet()) {
for (EntityType entityType : statisticData.entityStatisticValues().get(statistic).keySet()) {
player.setStatistic(statistic, entityType, statisticData.entityStatisticValues().get(statistic).get(entityType));
}
}
}
/**
* Set a player's exp level, exp points & score
*
* @param player The {@link Player} to set
* @param data The {@link PlayerData} to set them
*/
private static void setPlayerExperience(Player player, PlayerData data) {
player.setTotalExperience(data.getTotalExperience());
player.setLevel(data.getExpLevel());
player.setExp(data.getExpProgress());
}
/**
* Set a player's location from {@link me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation} data
*
* @param player The {@link Player} to teleport
* @param location The {@link me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation}
*/
private static void setPlayerLocation(Player player, me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation location) {
// Don't teleport if the location is invalid
if (location == null) {
return;
}
// Determine the world; if the names match, use that
World world = Bukkit.getWorld(location.worldName());
if (world == null) {
// If the names don't match, find the corresponding world with the same dimension environment
for (World worldOnServer : Bukkit.getWorlds()) {
if (worldOnServer.getEnvironment().equals(location.environment())) {
world = worldOnServer;
}
}
// If that still fails, return
if (world == null) {
return;
}
}
// Teleport the player
player.teleport(new Location(world, location.x(), location.y(), location.z(), location.yaw(), location.pitch()));
}
/**
* Correctly set a {@link Player}'s health data
*
* @param player The {@link Player} to set
* @param health Health to set to the player
* @param maxHealth Max health to set to the player
* @param healthScale Health scaling to apply to the player
*/
private static void setPlayerHealth(Player player, double health, double maxHealth, double healthScale) {
// Set max health
if (maxHealth != 0D) {
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).setBaseValue(maxHealth);
}
// Set health
double currentHealth = player.getHealth();
if (health != currentHealth) player.setHealth(currentHealth > maxHealth ? maxHealth : health);
// Set health scaling if needed
if (healthScale != 0D) {
player.setHealthScale(healthScale);
} else {
player.setHealthScale(maxHealth);
}
player.setHealthScaled(healthScale != 0D);
}
}

View File

@@ -1,146 +0,0 @@
package net.william278.husksync.bukkit.util.nms;
import net.william278.husksync.util.ThrowSupplier;
import org.bukkit.advancement.Advancement;
import org.bukkit.entity.Player;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Map;
import java.util.Set;
public class AdvancementUtils {
public final static Class<?> PLAYER_ADVANCEMENT;
private final static Field PLAYER_ADVANCEMENTS_MAP;
private final static Field PLAYER_VISIBLE_SET;
private final static Field PLAYER_ADVANCEMENTS;
private final static Field CRITERIA_MAP;
private final static Field CRITERIA_DATE;
private final static Field IS_FIRST_PACKET;
private final static Method GET_HANDLE;
private final static Method START_PROGRESS;
private final static Method ENSURE_ALL_VISIBLE;
private final static Class<?> ADVANCEMENT_PROGRESS;
private final static Class<?> CRITERION_PROGRESS;
static {
Class<?> SERVER_PLAYER = MinecraftVersionUtils.getMinecraftClass("level.EntityPlayer");
PLAYER_ADVANCEMENTS = ThrowSupplier.get(() -> SERVER_PLAYER.getDeclaredField("cs"));
PLAYER_ADVANCEMENTS.setAccessible(true);
Class<?> CRAFT_ADVANCEMENT = MinecraftVersionUtils.getBukkitClass("advancement.CraftAdvancement");
GET_HANDLE = ThrowSupplier.get(() -> CRAFT_ADVANCEMENT.getDeclaredMethod("getHandle"));
ADVANCEMENT_PROGRESS = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.AdvancementProgress"));
CRITERIA_MAP = ThrowSupplier.get(() -> ADVANCEMENT_PROGRESS.getDeclaredField("a"));
CRITERIA_MAP.setAccessible(true);
CRITERION_PROGRESS = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.CriterionProgress"));
CRITERIA_DATE = ThrowSupplier.get(() -> CRITERION_PROGRESS.getDeclaredField("b"));
CRITERIA_DATE.setAccessible(true);
Class<?> ADVANCEMENT = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.Advancement"));
PLAYER_ADVANCEMENT = MinecraftVersionUtils.getMinecraftClass("AdvancementDataPlayer");
PLAYER_ADVANCEMENTS_MAP = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("h"));
PLAYER_ADVANCEMENTS_MAP.setAccessible(true);
PLAYER_VISIBLE_SET = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("i"));
PLAYER_VISIBLE_SET.setAccessible(true);
START_PROGRESS = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredMethod("a", ADVANCEMENT, ADVANCEMENT_PROGRESS));
START_PROGRESS.setAccessible(true);
ENSURE_ALL_VISIBLE = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredMethod("c"));
ENSURE_ALL_VISIBLE.setAccessible(true);
IS_FIRST_PACKET = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("n"));
IS_FIRST_PACKET.setAccessible(true);
}
public static void markPlayerAdvancementsFirst(final Object playerAdvancements) {
try {
IS_FIRST_PACKET.set(playerAdvancements, true);
} catch (IllegalAccessException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public static Object getPlayerAdvancements(Player player) {
Object nativePlayer = EntityUtils.getHandle(player);
try {
return PLAYER_ADVANCEMENTS.get(nativePlayer);
} catch (IllegalAccessException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public static void clearPlayerAdvancements(final Object playerAdvancement) {
try {
((Map<?, ?>) PLAYER_ADVANCEMENTS_MAP.get(playerAdvancement))
.clear();
} catch (IllegalAccessException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public static Object getHandle(Advancement advancement) {
try {
return GET_HANDLE.invoke(advancement);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public static Object newCriterionProgress(final Date date) {
try {
Object nativeCriterionProgress = CRITERION_PROGRESS.getDeclaredConstructor().newInstance();
CRITERIA_DATE.set(nativeCriterionProgress, date);
return nativeCriterionProgress;
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@SuppressWarnings("unchecked") // Suppress unchecked cast warnings here
public static Object newAdvancementProgress(final Map<String, Object> criteria) {
try {
Object nativeAdvancementProgress = ADVANCEMENT_PROGRESS.getDeclaredConstructor().newInstance();
final Map<String, Object> criteriaMap = (Map<String, Object>) CRITERIA_MAP.get(nativeAdvancementProgress);
criteriaMap.putAll(criteria);
return nativeAdvancementProgress;
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public static void startProgress(final Object playerAdvancements, final Object advancement, final Object advancementProgress) {
try {
START_PROGRESS.invoke(playerAdvancements, advancement, advancementProgress);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public static void ensureAllVisible(final Object playerAdvancements) {
try {
ENSURE_ALL_VISIBLE.invoke(playerAdvancements);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public static void clearVisibleAdvancements(final Object playerAdvancements) {
try {
((Set<?>) PLAYER_VISIBLE_SET.get(playerAdvancements))
.clear();
} catch (IllegalAccessException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}

View File

@@ -1,26 +0,0 @@
package net.william278.husksync.bukkit.util.nms;
import net.william278.husksync.util.ThrowSupplier;
import org.bukkit.entity.LivingEntity;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class EntityUtils {
private final static Method GET_HANDLE;
static {
final Class<?> CRAFT_ENTITY = MinecraftVersionUtils.getBukkitClass("entity.CraftEntity");
GET_HANDLE = ThrowSupplier.get(() -> CRAFT_ENTITY.getDeclaredMethod("getHandle"));
}
public static Object getHandle(LivingEntity livingEntity) throws RuntimeException {
try {
return GET_HANDLE.invoke(livingEntity);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}

View File

@@ -1,25 +0,0 @@
package net.william278.husksync.bukkit.util.nms;
import net.william278.husksync.util.ThrowSupplier;
import net.william278.husksync.util.VersionUtils;
import org.bukkit.Bukkit;
public class MinecraftVersionUtils {
public final static String CRAFTBUKKIT_PACKAGE_PATH = Bukkit.getServer().getClass().getPackage().getName();
public final static String PACKAGE_VERSION = CRAFTBUKKIT_PACKAGE_PATH.split("\\.")[3];
public final static VersionUtils.Version SERVER_VERSION
= VersionUtils.Version.of(Bukkit.getBukkitVersion().split("-")[0]);
public final static String MINECRAFT_PACKAGE = SERVER_VERSION.compareTo(VersionUtils.Version.of("1.17")) < 0 ?
"net.minecraft.server.".concat(PACKAGE_VERSION) : "net.minecraft.server";
public static Class<?> getBukkitClass(String path) {
return ThrowSupplier.get(() -> Class.forName(CRAFTBUKKIT_PACKAGE_PATH.concat(".").concat(path)));
}
public static Class<?> getMinecraftClass(String path) {
return ThrowSupplier.get(() -> Class.forName(MINECRAFT_PACKAGE.concat(".").concat(path)));
}
}