mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-28 11:09:11 +00:00
Refactor package to net.william278; update dependencies & stop shading internal modules
This commit is contained in:
159
bukkit/src/main/java/net/william278/husksync/HuskSyncBukkit.java
Normal file
159
bukkit/src/main/java/net/william278/husksync/HuskSyncBukkit.java
Normal file
@@ -0,0 +1,159 @@
|
||||
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);
|
||||
}
|
||||
getLogger().info("Data save complete!");
|
||||
}
|
||||
|
||||
|
||||
// Send termination handshake to proxy
|
||||
closeRedisHandshake();
|
||||
|
||||
// Plugin shutdown logic
|
||||
getLogger().info("Disabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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.synchronizationTimeoutRetryDelay = config.getLong("synchronization_timeout_retry_delay", 15L);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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))
|
||||
.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());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
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 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, () -> PlayerSetter.updatePlayerData(player));
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
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.api.HuskSyncAPI;
|
||||
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.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitRedisListener extends RedisListener {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
// 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 (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 -> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package net.william278.husksync.bukkit.migrator;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.husksync.migrator.MPDBPlayerData;
|
||||
import net.craftersland.data.bridge.PD;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MPDBDeserializer {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
// Instance of MySqlPlayerDataBridge
|
||||
private static PD mySqlPlayerDataBridge;
|
||||
|
||||
public static void setMySqlPlayerDataBridge() {
|
||||
mySqlPlayerDataBridge = (PD) Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, getItemStackArrayFromMPDBBase64String(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 = getItemStackArrayFromMPDBBase64String(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 = getItemStackArrayFromMPDBBase64String(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ItemStack array from a decoded base 64 string in MySQLPlayerDataBridge's format
|
||||
*
|
||||
* @param data The encoded ItemStack[] string from MySQLPlayerDataBridge
|
||||
* @return The {@link ItemStack[]} array
|
||||
* @throws InvocationTargetException If an error occurs during decoding
|
||||
* @throws IllegalAccessException If an error occurs during decoding
|
||||
*/
|
||||
public static ItemStack[] getItemStackArrayFromMPDBBase64String(String data) throws InvocationTargetException, IllegalAccessException {
|
||||
if (data.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
return mySqlPlayerDataBridge.getItemStackSerializer().fromBase64(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
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.api.events.SyncCompleteEvent;
|
||||
import net.william278.husksync.bukkit.api.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
|
||||
*/
|
||||
public static void updatePlayerData(Player player) {
|
||||
// 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).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
|
||||
}
|
||||
|
||||
// Clear player inventory and ender chest
|
||||
player.getInventory().clear();
|
||||
player.getEnderChest().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<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<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 DataSerializer.AdvancementRecordDate}s to set
|
||||
*/
|
||||
private static void setPlayerAdvancements(Player player, List<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 (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 DataSerializer.StatisticData} to set
|
||||
*/
|
||||
private static void setPlayerStatistics(Player player, 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 DataSerializer.PlayerLocation} data
|
||||
*
|
||||
* @param player The {@link Player} to teleport
|
||||
* @param location The {@link DataSerializer.PlayerLocation}
|
||||
*/
|
||||
private static void setPlayerLocation(Player player, 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 != 0.0D) {
|
||||
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).setBaseValue(maxHealth);
|
||||
}
|
||||
|
||||
// Set health
|
||||
player.setHealth(player.getHealth() > maxHealth ? maxHealth : health);
|
||||
|
||||
// Set health scaling if needed
|
||||
if (healthScale != 0D) {
|
||||
player.setHealthScale(healthScale);
|
||||
} else {
|
||||
player.setHealthScale(maxHealth);
|
||||
}
|
||||
player.setHealthScaled(healthScale != 0D);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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)));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user