forked from public-mirrors/HuskSync
v3.0: New modular, more compatible data format, new API, better UX (#160)
* Start work on v3 * More work on task scheduling * Add comment to notification display slot * Synchronise branches * Use new HuskHomes-style task system * Bump to 2.3 * Remove HuskSyncInitializationException.java * Optimise database for MariaDB * Update libraries, move some around * Tweak command registration * Remove dummyhusksync * Fixup core synchronisation logic to use new task system * Implement new event dispatch subsystem * Remove last remaining future calls * Remove `Event#fire()` * Refactor startup process * New command subsystem, more initialization improvements, locale fixes * Update docs, tweak command perms * Reduce task number during data setting * add todo * Start work on data format / serialization refactor * More work on Bukkit impl * More serialization work * Fixes to serialization, data preview system * Start legacy conversion skeleton * Handle setting empty inventories * Start on-the-fly legacy conversion work * Add advancement conversion * Rewrite advancement get / apply logic * Start work on locked map persistence * More map persistence work * More work on map serialization * Move around persistence logic * Add testing suite * Fix item synchronisation * Finalize more reliable locked map persistence * Remove deprecated method call * remove sync feature enum * Fix held item slot syncing * Make data types modular and API-extensible * Remove some excessive debugging, minor refactor * Fixup date formatting, improve menu UIs * Finish up legacy data converting * Null safety in item stack serializaiton * Fix relocation of nbtapi, update dumping docs * Add v1/MPDB Migrators back in * Fix pinning/unpinning data not working * Consumer instead of Function for editing data * Show file size in DataSnapshotOverview * Fix getIdentifier always returning empty * Re-add items and inventory GUI commands * Improve config file, fixup data restoration * Add min time between backups (more useful backups!) * More work on backups * Fixup backup rotation frequency * Remove stdout debug print in `#getEventPriority` * Improve sync complete locale logic, fix synchronization spelling * Remove `static` on exception * Use dedicated thread for Redis, properly unsubscribe * Refactor `player` package -> `user` * `PlayerDataHolder` -> `UserDataHolder` * Make `StatisticsMap` public, but `@ApiStatus.Internal` * Suppress unused warnings on `Data` * Add option to disable Plan hook * Decompress legacy data before converting * Decompress bytes in fromBytes * Check permission node before serving TAB suggestions * Actually convert legacy item stack data * Fix syntax errors * Minor method refactor in items command * Fixup case-sensitive parsing in HuskSync command * Start API work * More work on API, fix potion effects * Fix cross-server, config formatting for auto-pinned issue * Fix confusion with UserData command, update docs images * Update commands docs * More docs updating * Fix sync feature enabled/disabled checking logic * Fix `#isCustom()` * Enable persistent_data syncing by default * docs: update Sync-Features config snippet * docs: correct typo in Sync Features * More API work * bukkit: slightly optimized schedulers * More API work, various refactorings * docs: Start new API docs * bump dependencies * Add some basic unit tests * docs: Correct typos * More docs work, annotate DB methods as `@Blocking` * Encapsulate `RedisMessage`, minor optimisations * api: Simplify `#getCurrentData` * api: Simplify `editCurrentData`, using `ThrowingConsumers` for better error handling * docs: More Data Snapshot API documenting * docs: add TOC to Data Snapshot API page * bukkit: Make data types extend BukkitData * Move where custom data is stored, finish up Custom Data API docs * Optimise imports * Fix `data_manager_advancements_preview_remaining` locale * Fix advancement and playtime previews * Fix potion effect deserialization * Make snapshot_backup_frequency default to 4, more error handling/logging * docs: Add ToC to Custom Data API * docs: Minor legacy API tweaks * Remove some unneeded catch logic * Suppress a few warnings * Fix Effect constructor being supplied in wrong orderfeat/data-edit-commands
parent
9018ad02e1
commit
105f65c93a
@ -0,0 +1,270 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.DataHolder;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* The HuskSync API implementation for the Bukkit platform
|
||||
* </p>
|
||||
* Retrieve an instance of the API class via {@link #getInstance()}.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
||||
|
||||
// Instance of the plugin
|
||||
private static BukkitHuskSyncAPI instance;
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the API.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
private BukkitHuskSyncAPI(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the HuskSync API - returns an instance of the API
|
||||
*
|
||||
* @return instance of the HuskSync API
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public static BukkitHuskSyncAPI getInstance() {
|
||||
if (instance == null) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Register the API for this platform.
|
||||
*
|
||||
* @param plugin the plugin instance
|
||||
* @since 3.0
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void register(@NotNull BukkitHuskSync plugin) {
|
||||
instance = new BukkitHuskSyncAPI(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Unregister the API for this platform.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void unregister() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link OnlineUser} instance for the given bukkit {@link Player}.
|
||||
*
|
||||
* @param player the bukkit player to get the {@link OnlineUser} instance for
|
||||
* @return the {@link OnlineUser} instance for the given bukkit {@link Player}
|
||||
* @since 2.0
|
||||
*/
|
||||
@NotNull
|
||||
public BukkitUser getUser(@NotNull Player player) {
|
||||
return BukkitUser.adapt(player, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to get the inventory of
|
||||
* @return the {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<BukkitData.Items.Inventory>> getCurrentInventory(@NotNull User user) {
|
||||
return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getInventory)
|
||||
.map(BukkitData.Items.Inventory.class::cast));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.Inventory} of the given {@link Player}
|
||||
*
|
||||
* @param user the user to get the inventory of
|
||||
* @return the {@link BukkitData.Items.Inventory} of the given {@link Player}
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<ItemStack[]>> getCurrentInventoryContents(@NotNull User user) {
|
||||
return getCurrentInventory(user)
|
||||
.thenApply(inventory -> inventory.map(BukkitData.Items.Inventory::getContents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the inventory of
|
||||
* @param contents the contents to set the inventory to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentInventory(@NotNull User user, @NotNull BukkitData.Items.Inventory contents) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.setInventory(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the inventory of
|
||||
* @param contents the contents to set the inventory to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentInventoryContents(@NotNull User user, @NotNull ItemStack[] contents) {
|
||||
editCurrentData(
|
||||
user,
|
||||
dataHolder -> dataHolder.getInventory().ifPresent(
|
||||
inv -> inv.setContents(adaptItems(contents))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the inventory of
|
||||
* @param editor the editor to apply to the inventory
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentInventory(@NotNull User user, ThrowingConsumer<BukkitData.Items.Inventory> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getInventory()
|
||||
.map(BukkitData.Items.Inventory.class::cast)
|
||||
.ifPresent(editor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the inventory of
|
||||
* @param editor the editor to apply to the inventory
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentInventoryContents(@NotNull User user, ThrowingConsumer<ItemStack[]> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getInventory()
|
||||
.map(BukkitData.Items.Inventory.class::cast)
|
||||
.ifPresent(inventory -> editor.accept(inventory.getContents())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to get the ender chest of
|
||||
* @return the {@link BukkitData.Items.EnderChest} of the given {@link User}, or {@link Optional#empty()} if the
|
||||
* user data could not be found
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<BukkitData.Items.EnderChest>> getCurrentEnderChest(@NotNull User user) {
|
||||
return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getEnderChest)
|
||||
.map(BukkitData.Items.EnderChest.class::cast));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.EnderChest} of the given {@link Player}
|
||||
*
|
||||
* @param user the user to get the ender chest of
|
||||
* @return the {@link BukkitData.Items.EnderChest} of the given {@link Player}, or {@link Optional#empty()} if the
|
||||
* user data could not be found
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<ItemStack[]>> getCurrentEnderChestContents(@NotNull User user) {
|
||||
return getCurrentEnderChest(user)
|
||||
.thenApply(enderChest -> enderChest.map(BukkitData.Items.EnderChest::getContents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the ender chest of
|
||||
* @param contents the contents to set the ender chest to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentEnderChest(@NotNull User user, @NotNull BukkitData.Items.EnderChest contents) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.setEnderChest(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the ender chest of
|
||||
* @param contents the contents to set the ender chest to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentEnderChestContents(@NotNull User user, @NotNull ItemStack[] contents) {
|
||||
editCurrentData(
|
||||
user,
|
||||
dataHolder -> dataHolder.getEnderChest().ifPresent(
|
||||
enderChest -> enderChest.setContents(adaptItems(contents))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the ender chest of
|
||||
* @param editor the editor to apply to the ender chest
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentEnderChest(@NotNull User user, Consumer<BukkitData.Items.EnderChest> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getEnderChest()
|
||||
.map(BukkitData.Items.EnderChest.class::cast)
|
||||
.ifPresent(editor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the ender chest of
|
||||
* @param editor the editor to apply to the ender chest
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentEnderChestContents(@NotNull User user, Consumer<ItemStack[]> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getEnderChest()
|
||||
.map(BukkitData.Items.EnderChest.class::cast)
|
||||
.ifPresent(enderChest -> editor.accept(enderChest.getContents())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts an array of {@link ItemStack} to a {@link BukkitData.Items} instance
|
||||
*
|
||||
* @param contents the contents to adapt
|
||||
* @return the adapted {@link BukkitData.Items} instance
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public BukkitData.Items adaptItems(@NotNull ItemStack[] contents) {
|
||||
return BukkitData.Items.ItemArray.adapt(contents);
|
||||
}
|
||||
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* The HuskSync API implementation for the Bukkit platform, providing methods to access and modify player {@link UserData} held by {@link User}s.
|
||||
* </p>
|
||||
* Retrieve an instance of the API class via {@link #getInstance()}.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class HuskSyncAPI extends BaseHuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the API class
|
||||
*/
|
||||
private static final HuskSyncAPI INSTANCE = new HuskSyncAPI();
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the API
|
||||
*/
|
||||
private HuskSyncAPI() {
|
||||
super(BukkitHuskSync.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the HuskSync API - returns an instance of the API
|
||||
*
|
||||
* @return instance of the HuskSync API
|
||||
*/
|
||||
public static @NotNull HuskSyncAPI getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} instance for the given bukkit {@link Player}.
|
||||
*
|
||||
* @param player the bukkit player to get the {@link User} instance for
|
||||
* @return the {@link User} instance for the given bukkit {@link Player}
|
||||
* @since 2.0
|
||||
*/
|
||||
@NotNull
|
||||
public OnlineUser getUser(@NotNull Player player) {
|
||||
return BukkitPlayer.adapt(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory in the database of the given {@link User} to the given {@link ItemStack} contents
|
||||
*
|
||||
* @param user the {@link User} to set the inventory of
|
||||
* @param inventoryContents the {@link ItemStack} contents to set the inventory to
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Void> setInventoryData(@NotNull User user, @NotNull ItemStack[] inventoryContents) {
|
||||
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
|
||||
userData.ifPresent(data -> serializeItemStackArray(inventoryContents)
|
||||
.thenAccept(serializedInventory -> {
|
||||
data.getInventory().orElse(ItemData.empty()).serializedItems = serializedInventory;
|
||||
setUserData(user, data).join();
|
||||
}))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory in the database of the given {@link User} to the given {@link BukkitInventoryMap} contents
|
||||
*
|
||||
* @param user the {@link User} to set the inventory of
|
||||
* @param inventoryMap the {@link BukkitInventoryMap} contents to set the inventory to
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Void> setInventoryData(@NotNull User user, @NotNull BukkitInventoryMap inventoryMap) {
|
||||
return setInventoryData(user, inventoryMap.getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Ender Chest in the database of the given {@link User} to the given {@link ItemStack} contents
|
||||
*
|
||||
* @param user the {@link User} to set the Ender Chest of
|
||||
* @param enderChestContents the {@link ItemStack} contents to set the Ender Chest to
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Void> setEnderChestData(@NotNull User user, @NotNull ItemStack[] enderChestContents) {
|
||||
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
|
||||
userData.ifPresent(data -> serializeItemStackArray(enderChestContents)
|
||||
.thenAccept(serializedInventory -> {
|
||||
data.getEnderChest().orElse(ItemData.empty()).serializedItems = serializedInventory;
|
||||
setUserData(user, data).join();
|
||||
}))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link BukkitInventoryMap} for the given {@link User}, containing their current inventory item data
|
||||
*
|
||||
* @param user the {@link User} to get the {@link BukkitInventoryMap} for
|
||||
* @return future returning the {@link BukkitInventoryMap} for the given {@link User} if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote If the {@link UserData} does not contain an inventory (i.e. inventory synchronisation is disabled), the
|
||||
* returned {@link BukkitInventoryMap} will be equivalent an empty inventory.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Optional<BukkitInventoryMap>> getPlayerInventory(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
|
||||
.map(userData -> deserializeInventory(userData.getInventory()
|
||||
.orElse(ItemData.empty()).serializedItems).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ItemStack}s array contents of the given {@link User}'s Ender Chest data
|
||||
*
|
||||
* @param user the {@link User} to get the Ender Chest contents of
|
||||
* @return future returning the {@link ItemStack} array of Ender Chest items for the user if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote If the {@link UserData} does not contain an Ender Chest (i.e. Ender Chest synchronisation is disabled),
|
||||
* the returned {@link BukkitInventoryMap} will be equivalent to an empty inventory.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Optional<ItemStack[]>> getPlayerEnderChest(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
|
||||
.map(userData -> deserializeItemStackArray(userData.getEnderChest()
|
||||
.orElse(ItemData.empty()).serializedItems).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a Base-64 encoded inventory array string into a {@link ItemStack} array.
|
||||
*
|
||||
* @param serializedItemStackArray The Base-64 encoded inventory array string.
|
||||
* @return The deserialized {@link ItemStack} array.
|
||||
* @throws DataSerializationException If an error occurs during deserialization.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<ItemStack[]> deserializeItemStackArray(@NotNull String serializedItemStackArray)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer
|
||||
.deserializeItemStackArray(serializedItemStackArray).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a serialized {@link ItemStack} array of player inventory contents into a {@link BukkitInventoryMap}
|
||||
*
|
||||
* @param serializedInventory The serialized {@link ItemStack} array of player inventory contents.
|
||||
* @return A {@link BukkitInventoryMap} of the deserialized {@link ItemStack} contents array
|
||||
* @throws DataSerializationException If an error occurs during deserialization.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<BukkitInventoryMap> deserializeInventory(@NotNull String serializedInventory)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer
|
||||
.deserializeInventory(serializedInventory).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an {@link ItemStack} array into a Base-64 encoded string.
|
||||
*
|
||||
* @param itemStacks The {@link ItemStack} array to serialize.
|
||||
* @return The serialized Base-64 encoded string.
|
||||
* @throws DataSerializationException If an error occurs during serialization.
|
||||
* @see #deserializeItemStackArray(String)
|
||||
* @see ItemData
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<String> serializeItemStackArray(@NotNull ItemStack[] itemStacks)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializeItemStackArray(itemStacks).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a Base-64 encoded potion effect array string into a {@link PotionEffect} array.
|
||||
*
|
||||
* @param serializedPotionEffectArray The Base-64 encoded potion effect array string.
|
||||
* @return The deserialized {@link PotionEffect} array.
|
||||
* @throws DataSerializationException If an error occurs during deserialization.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<PotionEffect[]> deserializePotionEffectArray(@NotNull String serializedPotionEffectArray)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer
|
||||
.deserializePotionEffectArray(serializedPotionEffectArray).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a {@link PotionEffect} array into a Base-64 encoded string.
|
||||
*
|
||||
* @param potionEffects The {@link PotionEffect} array to serialize.
|
||||
* @return The serialized Base-64 encoded string.
|
||||
* @throws DataSerializationException If an error occurs during serialization.
|
||||
* @see #deserializePotionEffectArray(String)
|
||||
* @see PotionEffectData
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<String> serializePotionEffectArray(@NotNull PotionEffect[] potionEffects)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializePotionEffectArray(potionEffects).join());
|
||||
}
|
||||
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Commands available on the Bukkit HuskSync implementation
|
||||
*/
|
||||
public enum BukkitCommandType {
|
||||
|
||||
HUSKSYNC_COMMAND(new HuskSyncCommand(BukkitHuskSync.getInstance())),
|
||||
USERDATA_COMMAND(new UserDataCommand(BukkitHuskSync.getInstance())),
|
||||
INVENTORY_COMMAND(new InventoryCommand(BukkitHuskSync.getInstance())),
|
||||
ENDER_CHEST_COMMAND(new EnderChestCommand(BukkitHuskSync.getInstance()));
|
||||
|
||||
public final CommandBase commandBase;
|
||||
|
||||
BukkitCommandType(@NotNull CommandBase commandBase) {
|
||||
this.commandBase = commandBase;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,147 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A mapped player inventory, providing methods to easily access a player's inventory.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitInventoryMap {
|
||||
|
||||
public static final int INVENTORY_SLOT_COUNT = 41;
|
||||
|
||||
private ItemStack[] contents;
|
||||
|
||||
/**
|
||||
* Creates a new mapped inventory from the given contents.
|
||||
*
|
||||
* @param contents the contents of the inventory
|
||||
*/
|
||||
protected BukkitInventoryMap(ItemStack[] contents) {
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contents of the inventory.
|
||||
*
|
||||
* @return the contents of the inventory
|
||||
*/
|
||||
public ItemStack[] getContents() {
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contents of the inventory.
|
||||
*
|
||||
* @param contents the contents of the inventory
|
||||
*/
|
||||
public void setContents(ItemStack[] contents) {
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of the inventory.
|
||||
*
|
||||
* @return the size of the inventory
|
||||
*/
|
||||
public int getSize() {
|
||||
return contents.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the item at the given index.
|
||||
*
|
||||
* @param index the index of the item to get
|
||||
* @return the item at the given index
|
||||
*/
|
||||
public Optional<ItemStack> getItemAt(int index) {
|
||||
if (contents.length >= index) {
|
||||
if (contents[index] == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(contents[index]);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the item at the given index.
|
||||
*
|
||||
* @param itemStack the item to set at the given index
|
||||
* @param index the index of the item to set
|
||||
* @throws IllegalArgumentException if the index is out of bounds
|
||||
*/
|
||||
public void setItemAt(@NotNull ItemStack itemStack, int index) throws IllegalArgumentException {
|
||||
contents[index] = itemStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the main inventory contents.
|
||||
*
|
||||
* @return the main inventory contents
|
||||
*/
|
||||
public ItemStack[] getInventory() {
|
||||
final ItemStack[] inventory = new ItemStack[36];
|
||||
System.arraycopy(contents, 0, inventory, 0, Math.min(contents.length, inventory.length));
|
||||
return inventory;
|
||||
}
|
||||
|
||||
public ItemStack[] getHotbar() {
|
||||
final ItemStack[] armor = new ItemStack[9];
|
||||
for (int i = 0; i <= 9; i++) {
|
||||
armor[i] = getItemAt(i).orElse(null);
|
||||
}
|
||||
return armor;
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getOffHand() {
|
||||
return getItemAt(40);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getHelmet() {
|
||||
return getItemAt(39);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getChestplate() {
|
||||
return getItemAt(38);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getLeggings() {
|
||||
return getItemAt(37);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getBoots() {
|
||||
return getItemAt(36);
|
||||
}
|
||||
|
||||
public ItemStack[] getArmor() {
|
||||
final ItemStack[] armor = new ItemStack[4];
|
||||
for (int i = 36; i < 40; i++) {
|
||||
armor[i - 36] = getItemAt(i).orElse(null);
|
||||
}
|
||||
return armor;
|
||||
}
|
||||
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.mapdataapi.MapData;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.*;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Handles the persistence of {@link MapData} into {@link ItemStack}s.
|
||||
*/
|
||||
public class BukkitMapHandler {
|
||||
|
||||
private static final BukkitHuskSync plugin = BukkitHuskSync.getInstance();
|
||||
private static final NamespacedKey MAP_DATA_KEY = new NamespacedKey(plugin, "map_data");
|
||||
|
||||
/**
|
||||
* Get the {@link MapData} from the given {@link ItemStack} and persist it in its' data container
|
||||
*
|
||||
* @param itemStack the {@link ItemStack} to get the {@link MapData} from
|
||||
*/
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public static void persistMapData(@Nullable ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType() != Material.FILLED_MAP) {
|
||||
return;
|
||||
}
|
||||
final MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
|
||||
if (mapMeta == null || !mapMeta.hasMapView()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the map view from the map
|
||||
final MapView mapView;
|
||||
try {
|
||||
mapView = Bukkit.getScheduler().callSyncMethod(plugin, mapMeta::getMapView).get();
|
||||
if (mapView == null || !mapView.isLocked() || mapView.isVirtual()) {
|
||||
return;
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to save map data for a player", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the map data
|
||||
plugin.debug("Rendering map view onto canvas for locked map");
|
||||
final LockedMapCanvas canvas = new LockedMapCanvas(mapView);
|
||||
for (MapRenderer renderer : mapView.getRenderers()) {
|
||||
renderer.render(mapView, canvas, Bukkit.getServer()
|
||||
.getOnlinePlayers().stream()
|
||||
.findAny()
|
||||
.orElse(null));
|
||||
}
|
||||
|
||||
// Save the extracted rendered map data
|
||||
plugin.debug("Saving pixel canvas data for locked map");
|
||||
if (!mapMeta.getPersistentDataContainer().has(MAP_DATA_KEY, PersistentDataType.BYTE_ARRAY)) {
|
||||
mapMeta.getPersistentDataContainer().set(MAP_DATA_KEY, PersistentDataType.BYTE_ARRAY,
|
||||
canvas.extractMapData().toBytes());
|
||||
itemStack.setItemMeta(mapMeta);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the map data of the given {@link ItemStack} to the given {@link MapData}, applying a map view to the item stack
|
||||
*
|
||||
* @param itemStack the {@link ItemStack} to set the map data of
|
||||
*/
|
||||
public static void setMapRenderer(@Nullable ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType() != Material.FILLED_MAP) {
|
||||
return;
|
||||
}
|
||||
|
||||
final MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
|
||||
if (mapMeta == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!itemStack.getItemMeta().getPersistentDataContainer().has(MAP_DATA_KEY, PersistentDataType.BYTE_ARRAY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final byte[] serializedData = itemStack.getItemMeta().getPersistentDataContainer()
|
||||
.get(MAP_DATA_KEY, PersistentDataType.BYTE_ARRAY);
|
||||
final MapData mapData = MapData.fromByteArray(Objects.requireNonNull(serializedData));
|
||||
plugin.debug("Setting deserialized map data for an item stack");
|
||||
|
||||
// Create a new map view renderer with the map data color at each pixel
|
||||
final MapView view = Bukkit.createMap(Bukkit.getWorlds().get(0));
|
||||
view.getRenderers().clear();
|
||||
view.addRenderer(new PersistentMapRenderer(mapData));
|
||||
view.setLocked(true);
|
||||
view.setScale(MapView.Scale.NORMAL);
|
||||
view.setTrackingPosition(false);
|
||||
view.setUnlimitedTracking(false);
|
||||
mapMeta.setMapView(view);
|
||||
itemStack.setItemMeta(mapMeta);
|
||||
plugin.debug("Successfully applied renderer to map item stack");
|
||||
} catch (IOException | NullPointerException e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to deserialize map data for a player", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MapRenderer} that can be used to render persistently serialized {@link MapData} to a {@link MapView}
|
||||
*/
|
||||
public static class PersistentMapRenderer extends MapRenderer {
|
||||
|
||||
private final MapData mapData;
|
||||
|
||||
private PersistentMapRenderer(@NotNull MapData mapData) {
|
||||
super(false);
|
||||
this.mapData = mapData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player player) {
|
||||
for (int i = 0; i < 128; i++) {
|
||||
for (int j = 0; j < 128; j++) {
|
||||
// We set the pixels in this order to avoid the map being rendered upside down
|
||||
canvas.setPixel(j, i, (byte) mapData.getColorAt(i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
|
||||
*/
|
||||
public static class LockedMapCanvas implements MapCanvas {
|
||||
|
||||
private final MapView mapView;
|
||||
private final int[][] pixels = new int[128][128];
|
||||
private MapCursorCollection cursors;
|
||||
|
||||
private LockedMapCanvas(@NotNull MapView mapView) {
|
||||
this.mapView = mapView;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MapCursorCollection getCursors() {
|
||||
return cursors == null ? (cursors = new MapCursorCollection()) : cursors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCursors(@NotNull MapCursorCollection cursors) {
|
||||
this.cursors = cursors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixel(int x, int y, byte color) {
|
||||
pixels[x][y] = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getPixel(int x, int y) {
|
||||
return (byte) pixels[x][y];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getBasePixel(int x, int y) {
|
||||
return getPixel(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawImage(int x, int y, @NotNull Image image) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawText(int x, int y, @NotNull MapFont font, @NotNull String text) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getDimension() {
|
||||
return mapView.getWorld() == null ? "minecraft:overworld"
|
||||
: switch (mapView.getWorld().getEnvironment()) {
|
||||
case NETHER -> "minecraft:the_nether";
|
||||
case THE_END -> "minecraft:the_end";
|
||||
default -> "minecraft:overworld";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the map data from the canvas. Must be rendered first
|
||||
* @return the extracted map data
|
||||
*/
|
||||
@NotNull
|
||||
private MapData extractMapData() {
|
||||
return MapData.fromPixels(pixels, getDimension(), (byte) 2);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public record BukkitPersistentTypeMapping<T, Z>(PersistentDataTagType type, PersistentDataType<T, Z> bukkitType) {
|
||||
|
||||
public static final BukkitPersistentTypeMapping<?, ?>[] PRIMITIVE_TYPE_MAPPINGS = new BukkitPersistentTypeMapping<?, ?>[]{
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.BYTE, PersistentDataType.BYTE),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.SHORT, PersistentDataType.SHORT),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.INTEGER, PersistentDataType.INTEGER),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.LONG, PersistentDataType.LONG),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.FLOAT, PersistentDataType.FLOAT),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.DOUBLE, PersistentDataType.DOUBLE),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.STRING, PersistentDataType.STRING),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.BYTE_ARRAY, PersistentDataType.BYTE_ARRAY),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.INTEGER_ARRAY, PersistentDataType.INTEGER_ARRAY),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.LONG_ARRAY, PersistentDataType.LONG_ARRAY),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.TAG_CONTAINER_ARRAY, PersistentDataType.TAG_CONTAINER_ARRAY),
|
||||
new BukkitPersistentTypeMapping<>(PersistentDataTagType.TAG_CONTAINER, PersistentDataType.TAG_CONTAINER)
|
||||
};
|
||||
|
||||
public BukkitPersistentTypeMapping(@NotNull PersistentDataTagType type, @NotNull PersistentDataType<T, Z> bukkitType) {
|
||||
this.type = type;
|
||||
this.bukkitType = bukkitType;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public PersistentDataTag<Z> getContainerValue(@NotNull PersistentDataContainer container, @NotNull NamespacedKey key) throws NullPointerException {
|
||||
return new PersistentDataTag<>(type, Objects.requireNonNull(container.get(key, bukkitType)));
|
||||
}
|
||||
|
||||
public void setContainerValue(@NotNull PersistentDataContainerData container, @NotNull Player player, @NotNull NamespacedKey key) throws NullPointerException {
|
||||
container.getTagValue(key.toString(), bukkitType.getComplexType())
|
||||
.ifPresent(value -> player.getPersistentDataContainer().set(key, bukkitType, value));
|
||||
}
|
||||
|
||||
public static Optional<BukkitPersistentTypeMapping<?, ?>> getMapping(@NotNull PersistentDataTagType type) {
|
||||
for (BukkitPersistentTypeMapping<?, ?> mapping : PRIMITIVE_TYPE_MAPPINGS) {
|
||||
if (mapping.type().equals(type)) {
|
||||
return Optional.of(mapping);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.util.BukkitMapPersister;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
|
||||
@Override
|
||||
default Optional<? extends Data> getData(@NotNull Identifier id) {
|
||||
if (!id.isCustom()) {
|
||||
return switch (id.getKeyValue()) {
|
||||
case "inventory" -> getInventory();
|
||||
case "ender_chest" -> getEnderChest();
|
||||
case "potion_effects" -> getPotionEffects();
|
||||
case "advancements" -> getAdvancements();
|
||||
case "location" -> getLocation();
|
||||
case "statistics" -> getStatistics();
|
||||
case "health" -> getHealth();
|
||||
case "hunger" -> getHunger();
|
||||
case "experience" -> getExperience();
|
||||
case "game_mode" -> getGameMode();
|
||||
case "persistent_data" -> getPersistentData();
|
||||
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
|
||||
};
|
||||
}
|
||||
return Optional.ofNullable(getCustomDataStore().get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setData(@NotNull Identifier id, @NotNull Data data) {
|
||||
if (id.isCustom()) {
|
||||
getCustomDataStore().put(id, data);
|
||||
}
|
||||
UserDataHolder.super.setData(id, data);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Items.Inventory> getInventory() {
|
||||
if ((isDead() && !getPlugin().getSettings().doSynchronizeDeadPlayersChangingServer())) {
|
||||
return Optional.of(BukkitData.Items.Inventory.empty());
|
||||
}
|
||||
final PlayerInventory inventory = getBukkitPlayer().getInventory();
|
||||
return Optional.of(BukkitData.Items.Inventory.from(
|
||||
getMapPersister().persistLockedMaps(inventory.getContents(), getBukkitPlayer()),
|
||||
inventory.getHeldItemSlot()
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Items.EnderChest> getEnderChest() {
|
||||
return Optional.of(BukkitData.Items.EnderChest.adapt(
|
||||
getMapPersister().persistLockedMaps(getBukkitPlayer().getEnderChest().getContents(), getBukkitPlayer())
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.PotionEffects> getPotionEffects() {
|
||||
return Optional.of(BukkitData.PotionEffects.from(getBukkitPlayer().getActivePotionEffects()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Advancements> getAdvancements() {
|
||||
return Optional.of(BukkitData.Advancements.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Location> getLocation() {
|
||||
return Optional.of(BukkitData.Location.adapt(getBukkitPlayer().getLocation()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Statistics> getStatistics() {
|
||||
return Optional.of(BukkitData.Statistics.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Health> getHealth() {
|
||||
return Optional.of(BukkitData.Health.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Hunger> getHunger() {
|
||||
return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
return Optional.of(BukkitData.Experience.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.GameMode> getGameMode() {
|
||||
return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.PersistentData> getPersistentData() {
|
||||
return Optional.of(BukkitData.PersistentData.adapt(getBukkitPlayer().getPersistentDataContainer()));
|
||||
}
|
||||
|
||||
boolean isDead();
|
||||
|
||||
@NotNull
|
||||
Player getBukkitPlayer();
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getCustomDataStore();
|
||||
|
||||
@NotNull
|
||||
default BukkitMapPersister getMapPersister() {
|
||||
return (BukkitHuskSync) getPlugin();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class BukkitEventCannon extends EventCannon {
|
||||
|
||||
public BukkitEventCannon() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Event> firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData) {
|
||||
return new BukkitPreSyncEvent(((BukkitPlayer) user).getPlayer(), userData).fire();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Event> fireDataSaveEvent(@NotNull User user, @NotNull UserData userData,
|
||||
@NotNull DataSaveCause saveCause) {
|
||||
return new BukkitDataSaveEvent(user, userData, saveCause).fire();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireSyncCompleteEvent(@NotNull OnlineUser user) {
|
||||
new BukkitSyncCompleteEvent(((BukkitPlayer) user).getPlayer()).fire();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface BukkitEventDispatcher extends EventDispatcher {
|
||||
|
||||
@Override
|
||||
default <T extends Event> boolean fireIsCancelled(@NotNull T event) {
|
||||
Bukkit.getPluginManager().callEvent((org.bukkit.event.Event) event);
|
||||
return event instanceof Cancellable cancellable && cancellable.isCancelled();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default PreSyncEvent getPreSyncEvent(@NotNull OnlineUser user, @NotNull DataSnapshot.Packed data) {
|
||||
return new BukkitPreSyncEvent(user, data, getPlugin());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default DataSaveEvent getDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
return new BukkitDataSaveEvent(user, data, getPlugin());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default SyncCompleteEvent getSyncCompleteEvent(@NotNull OnlineUser user) {
|
||||
return new BukkitSyncCompleteEvent(user, getPlugin());
|
||||
}
|
||||
|
||||
}
|
@ -1,655 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import dev.triumphteam.gui.builder.gui.StorageBuilder;
|
||||
import dev.triumphteam.gui.guis.Gui;
|
||||
import dev.triumphteam.gui.guis.StorageGui;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.roxeez.advancement.display.FrameType;
|
||||
import net.william278.andjam.Toast;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
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.event.inventory.InventoryType;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Bukkit implementation of an {@link OnlineUser}
|
||||
*/
|
||||
public class BukkitPlayer extends OnlineUser {
|
||||
|
||||
private final BukkitHuskSync plugin;
|
||||
private final Player player;
|
||||
|
||||
private BukkitPlayer(@NotNull Player player) {
|
||||
super(player.getUniqueId(), player.getName());
|
||||
this.plugin = BukkitHuskSync.getInstance();
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitPlayer adapt(@NotNull Player player) {
|
||||
return new BukkitPlayer(player);
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<StatusData> getStatus() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final double maxHealth = getMaxHealth(player);
|
||||
return new StatusData(Math.min(player.getHealth(), maxHealth),
|
||||
maxHealth,
|
||||
player.isHealthScaled() ? player.getHealthScale() : 0d,
|
||||
player.getFoodLevel(),
|
||||
player.getSaturation(),
|
||||
player.getExhaustion(),
|
||||
player.getInventory().getHeldItemSlot(),
|
||||
player.getTotalExperience(),
|
||||
player.getLevel(),
|
||||
player.getExp(),
|
||||
player.getGameMode().name(),
|
||||
player.getAllowFlight() && player.isFlying());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull Settings settings) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
// Set max health
|
||||
double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.MAX_HEALTH)) {
|
||||
if (statusData.maxHealth != 0d) {
|
||||
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
|
||||
.setBaseValue(statusData.maxHealth);
|
||||
currentMaxHealth = statusData.maxHealth;
|
||||
}
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HEALTH)) {
|
||||
// Set health
|
||||
final double currentHealth = player.getHealth();
|
||||
if (statusData.health != currentHealth) {
|
||||
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health;
|
||||
final double maxHealth = currentMaxHealth;
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
try {
|
||||
player.setHealth(Math.min(healthToSet, maxHealth));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Failed to set health of player " + player.getName() + " to " + healthToSet);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set health scale
|
||||
try {
|
||||
if (statusData.healthScale != 0d) {
|
||||
player.setHealthScale(statusData.healthScale);
|
||||
} else {
|
||||
player.setHealthScale(statusData.maxHealth);
|
||||
}
|
||||
player.setHealthScaled(statusData.healthScale != 0D);
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Failed to set health scale of player " + player.getName() + " to " + statusData.healthScale);
|
||||
}
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HUNGER)) {
|
||||
player.setFoodLevel(statusData.hunger);
|
||||
player.setSaturation(statusData.saturation);
|
||||
player.setExhaustion(statusData.saturationExhaustion);
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
|
||||
player.getInventory().setHeldItemSlot(statusData.selectedItemSlot);
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.EXPERIENCE)) {
|
||||
player.setTotalExperience(statusData.totalExperience);
|
||||
player.setLevel(statusData.expLevel);
|
||||
player.setExp(statusData.expProgress);
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.GAME_MODE)) {
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
player.setGameMode(GameMode.valueOf(statusData.gameMode)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (statusData.isFlying) {
|
||||
player.setAllowFlight(true);
|
||||
player.setFlying(true);
|
||||
}
|
||||
player.setFlying(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ItemData> getInventory() {
|
||||
final PlayerInventory inventory = player.getInventory();
|
||||
if (inventory.isEmpty()) {
|
||||
return CompletableFuture.completedFuture(ItemData.empty());
|
||||
}
|
||||
return BukkitSerializer.serializeItemStackArray(inventory.getContents())
|
||||
.thenApply(ItemData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setInventory(@NotNull ItemData itemData) {
|
||||
return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> {
|
||||
final CompletableFuture<Void> inventorySetFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
this.clearInventoryCraftingSlots();
|
||||
player.setItemOnCursor(null);
|
||||
player.getInventory().setContents(contents.getContents());
|
||||
player.updateInventory();
|
||||
inventorySetFuture.complete(null);
|
||||
});
|
||||
return inventorySetFuture.join();
|
||||
});
|
||||
}
|
||||
|
||||
// Clears any items the player may have in the crafting slots of their inventory
|
||||
private void clearInventoryCraftingSlots() {
|
||||
final Inventory inventory = player.getOpenInventory().getTopInventory();
|
||||
if (inventory.getType() == InventoryType.CRAFTING) {
|
||||
for (int slot = 0; slot < 5; slot++) {
|
||||
inventory.setItem(slot, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ItemData> getEnderChest() {
|
||||
final Inventory enderChest = player.getEnderChest();
|
||||
if (enderChest.isEmpty()) {
|
||||
return CompletableFuture.completedFuture(ItemData.empty());
|
||||
}
|
||||
return BukkitSerializer.serializeItemStackArray(enderChest.getContents())
|
||||
.thenApply(ItemData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setEnderChest(@NotNull ItemData enderChestData) {
|
||||
return BukkitSerializer.deserializeItemStackArray(enderChestData.serializedItems).thenApplyAsync(contents -> {
|
||||
final CompletableFuture<Void> enderChestSetFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
player.getEnderChest().setContents(contents);
|
||||
enderChestSetFuture.complete(null);
|
||||
});
|
||||
return enderChestSetFuture.join();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<PotionEffectData> getPotionEffects() {
|
||||
return BukkitSerializer.serializePotionEffectArray(player.getActivePotionEffects()
|
||||
.toArray(new PotionEffect[0])).thenApply(PotionEffectData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setPotionEffects(@NotNull PotionEffectData potionEffectData) {
|
||||
return BukkitSerializer.deserializePotionEffectArray(potionEffectData.serializedPotionEffects)
|
||||
.thenApplyAsync(effects -> {
|
||||
final CompletableFuture<Void> potionEffectsSetFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
for (PotionEffect effect : effects) {
|
||||
player.addPotionEffect(effect);
|
||||
}
|
||||
potionEffectsSetFuture.complete(null);
|
||||
});
|
||||
return potionEffectsSetFuture.join();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<AdvancementData>> getAdvancements() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
final ArrayList<AdvancementData> advancementData = new ArrayList<>();
|
||||
|
||||
// Iterate through the server advancement set and add all advancements to the list
|
||||
serverAdvancements.forEachRemaining(advancement -> {
|
||||
final AdvancementProgress advancementProgress = player.getAdvancementProgress(advancement);
|
||||
final Map<String, Date> awardedCriteria = new HashMap<>();
|
||||
|
||||
advancementProgress.getAwardedCriteria().forEach(criteriaKey -> awardedCriteria.put(criteriaKey,
|
||||
advancementProgress.getDateAwarded(criteriaKey)));
|
||||
|
||||
// Only save the advancement if criteria has been completed
|
||||
if (!awardedCriteria.isEmpty()) {
|
||||
advancementData.add(new AdvancementData(advancement.getKey().toString(), awardedCriteria));
|
||||
}
|
||||
});
|
||||
return advancementData;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setAdvancements(@NotNull List<AdvancementData> advancementData) {
|
||||
return CompletableFuture.runAsync(() -> Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
|
||||
// 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;
|
||||
|
||||
// Save current experience and level
|
||||
final int experienceLevel = player.getLevel();
|
||||
final float expProgress = player.getExp();
|
||||
|
||||
// Determines whether the experience might have changed warranting an update
|
||||
final AtomicBoolean correctExperience = new AtomicBoolean(false);
|
||||
|
||||
// Run asynchronously as advancement setting is expensive
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// Apply the advancements to the player
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
while (serverAdvancements.hasNext()) {
|
||||
// Iterate through all advancements
|
||||
final Advancement advancement = serverAdvancements.next();
|
||||
final AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
|
||||
|
||||
advancementData.stream().filter(record -> record.key.equals(advancement.getKey().toString())).findFirst().ifPresentOrElse(
|
||||
// Award all criteria that the player does not have that they do on the cache
|
||||
record -> {
|
||||
record.completedCriteria.keySet().stream()
|
||||
.filter(criterion -> !playerProgress.getAwardedCriteria().contains(criterion))
|
||||
.forEach(criterion -> {
|
||||
Bukkit.getScheduler().runTask(plugin,
|
||||
() -> player.getAdvancementProgress(advancement).awardCriteria(criterion));
|
||||
correctExperience.set(true);
|
||||
});
|
||||
|
||||
// Revoke all criteria that the player does have but should not
|
||||
new ArrayList<>(playerProgress.getAwardedCriteria()).stream().filter(criterion -> !record.completedCriteria.containsKey(criterion))
|
||||
.forEach(criterion -> Bukkit.getScheduler().runTask(plugin,
|
||||
() -> player.getAdvancementProgress(advancement).revokeCriteria(criterion)));
|
||||
|
||||
},
|
||||
// Revoke the criteria as the player shouldn't have any
|
||||
() -> new ArrayList<>(playerProgress.getAwardedCriteria()).forEach(criterion ->
|
||||
Bukkit.getScheduler().runTask(plugin,
|
||||
() -> player.getAdvancementProgress(advancement).revokeCriteria(criterion))));
|
||||
|
||||
// Update the player's experience in case the advancement changed that
|
||||
if (correctExperience.get()) {
|
||||
player.setLevel(experienceLevel);
|
||||
player.setExp(expProgress);
|
||||
correctExperience.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable announcing advancements (back on main thread again)
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (finalAnnounceAdvancementUpdate) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<StatisticsData> getStatistics() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final Map<String, Integer> untypedStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> blockStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> itemStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> entityStatisticValues = new HashMap<>();
|
||||
|
||||
for (Statistic statistic : Statistic.values()) {
|
||||
switch (statistic.getType()) {
|
||||
case ITEM -> {
|
||||
final Map<String, Integer> itemValues = new HashMap<>();
|
||||
Arrays.stream(Material.values()).filter(Material::isItem)
|
||||
.filter(itemMaterial -> (player.getStatistic(statistic, itemMaterial)) != 0)
|
||||
.forEach(itemMaterial -> itemValues.put(itemMaterial.name(),
|
||||
player.getStatistic(statistic, itemMaterial)));
|
||||
if (!itemValues.isEmpty()) {
|
||||
itemStatisticValues.put(statistic.name(), itemValues);
|
||||
}
|
||||
}
|
||||
case BLOCK -> {
|
||||
final Map<String, Integer> blockValues = new HashMap<>();
|
||||
Arrays.stream(Material.values()).filter(Material::isBlock)
|
||||
.filter(blockMaterial -> (player.getStatistic(statistic, blockMaterial)) != 0)
|
||||
.forEach(blockMaterial -> blockValues.put(blockMaterial.name(),
|
||||
player.getStatistic(statistic, blockMaterial)));
|
||||
if (!blockValues.isEmpty()) {
|
||||
blockStatisticValues.put(statistic.name(), blockValues);
|
||||
}
|
||||
}
|
||||
case ENTITY -> {
|
||||
final Map<String, Integer> entityValues = new HashMap<>();
|
||||
Arrays.stream(EntityType.values()).filter(EntityType::isAlive)
|
||||
.filter(entityType -> (player.getStatistic(statistic, entityType)) != 0)
|
||||
.forEach(entityType -> entityValues.put(entityType.name(),
|
||||
player.getStatistic(statistic, entityType)));
|
||||
if (!entityValues.isEmpty()) {
|
||||
entityStatisticValues.put(statistic.name(), entityValues);
|
||||
}
|
||||
}
|
||||
case UNTYPED -> {
|
||||
if (player.getStatistic(statistic) != 0) {
|
||||
untypedStatisticValues.put(statistic.name(), player.getStatistic(statistic));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new StatisticsData(untypedStatisticValues, blockStatisticValues,
|
||||
itemStatisticValues, entityStatisticValues);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
// Set generic statistics
|
||||
for (String statistic : statisticsData.untypedStatistics.keySet()) {
|
||||
try {
|
||||
player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistics.get(statistic));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Failed to set generic statistic " + statistic + " for " + username);
|
||||
}
|
||||
}
|
||||
|
||||
// Set block statistics
|
||||
for (String statistic : statisticsData.blockStatistics.keySet()) {
|
||||
for (String blockMaterial : statisticsData.blockStatistics.get(statistic).keySet()) {
|
||||
try {
|
||||
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(blockMaterial),
|
||||
statisticsData.blockStatistics.get(statistic).get(blockMaterial));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Failed to set " + blockMaterial + " statistic " + statistic + " for " + username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set item statistics
|
||||
for (String statistic : statisticsData.itemStatistics.keySet()) {
|
||||
for (String itemMaterial : statisticsData.itemStatistics.get(statistic).keySet()) {
|
||||
try {
|
||||
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(itemMaterial),
|
||||
statisticsData.itemStatistics.get(statistic).get(itemMaterial));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Failed to set " + itemMaterial + " statistic " + statistic + " for " + username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set entity statistics
|
||||
for (String statistic : statisticsData.entityStatistics.keySet()) {
|
||||
for (String entityType : statisticsData.entityStatistics.get(statistic).keySet()) {
|
||||
try {
|
||||
player.setStatistic(Statistic.valueOf(statistic), EntityType.valueOf(entityType),
|
||||
statisticsData.entityStatistics.get(statistic).get(entityType));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Failed to set " + entityType + " statistic " + statistic + " for " + username);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<LocationData> getLocation() {
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
new LocationData(player.getWorld().getName(), player.getWorld().getUID(), player.getWorld().getEnvironment().name(),
|
||||
player.getLocation().getX(), player.getLocation().getY(), player.getLocation().getZ(),
|
||||
player.getLocation().getYaw(), player.getLocation().getPitch()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setLocation(@NotNull LocationData locationData) {
|
||||
final CompletableFuture<Void> teleportFuture = new CompletableFuture<>();
|
||||
AtomicReference<World> bukkitWorld = new AtomicReference<>(Bukkit.getWorld(locationData.worldName));
|
||||
if (bukkitWorld.get() == null) {
|
||||
bukkitWorld.set(Bukkit.getWorld(locationData.worldUuid));
|
||||
}
|
||||
if (bukkitWorld.get() == null) {
|
||||
Bukkit.getWorlds().stream().filter(world -> world.getEnvironment() == World.Environment
|
||||
.valueOf(locationData.worldEnvironment)).findFirst().ifPresent(bukkitWorld::set);
|
||||
}
|
||||
if (bukkitWorld.get() != null) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
player.teleport(new Location(bukkitWorld.get(),
|
||||
locationData.x, locationData.y, locationData.z,
|
||||
locationData.yaw, locationData.pitch), PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
teleportFuture.complete(null);
|
||||
});
|
||||
}
|
||||
return teleportFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<PersistentDataContainerData> getPersistentDataContainer() {
|
||||
final Map<String, PersistentDataTag<?>> persistentDataMap = new HashMap<>();
|
||||
final PersistentDataContainer container = player.getPersistentDataContainer();
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
container.getKeys().forEach(key -> {
|
||||
BukkitPersistentTypeMapping<?, ?> type = null;
|
||||
for (BukkitPersistentTypeMapping<?, ?> dataType : BukkitPersistentTypeMapping.PRIMITIVE_TYPE_MAPPINGS) {
|
||||
if (container.has(key, dataType.bukkitType())) {
|
||||
type = dataType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (type != null) {
|
||||
persistentDataMap.put(key.toString(), type.getContainerValue(container, key));
|
||||
}
|
||||
});
|
||||
return new PersistentDataContainerData(persistentDataMap);
|
||||
}).exceptionally(throwable -> {
|
||||
plugin.log(Level.WARNING,
|
||||
"Could not read " + player.getName() + "'s persistent data map, skipping!");
|
||||
throwable.printStackTrace();
|
||||
return new PersistentDataContainerData(new HashMap<>());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData container) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
player.getPersistentDataContainer().getKeys().forEach(namespacedKey ->
|
||||
player.getPersistentDataContainer().remove(namespacedKey));
|
||||
container.getTags().forEach(keyString -> {
|
||||
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
||||
if (key != null) {
|
||||
container.getTagType(keyString)
|
||||
.flatMap(BukkitPersistentTypeMapping::getMapping)
|
||||
.ifPresentOrElse(mapping -> mapping.setContainerValue(container, player, key),
|
||||
() -> plugin.log(Level.WARNING,
|
||||
"Could not set " + player.getName() + "'s persistent data key " + keyString +
|
||||
" as it has an invalid type. Skipping!"));
|
||||
}
|
||||
});
|
||||
}).exceptionally(throwable -> {
|
||||
plugin.log(Level.WARNING,
|
||||
"Could not write " + player.getName() + "'s persistent data map, skipping!");
|
||||
throwable.printStackTrace();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Audience getAudience() {
|
||||
return plugin.getAudiences().player(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
try {
|
||||
return player == null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromString(Bukkit.getBukkitVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@NotNull String node) {
|
||||
return player.hasPermission(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<ItemData>> showMenu(@NotNull ItemData itemData, boolean editable,
|
||||
int minimumRows, @NotNull MineDown title) {
|
||||
final CompletableFuture<Optional<ItemData>> updatedData = new CompletableFuture<>();
|
||||
|
||||
// Deserialize the item data to be shown and show it in a triumph GUI
|
||||
BukkitSerializer.deserializeItemStackArray(itemData.serializedItems).thenAccept(items -> {
|
||||
// Build the GUI and populate with items
|
||||
final int itemCount = items.length;
|
||||
final StorageBuilder guiBuilder = Gui.storage()
|
||||
.title(title.toComponent())
|
||||
.rows(Math.max(minimumRows, (int) Math.ceil(itemCount / 9.0)))
|
||||
.disableAllInteractions()
|
||||
.enableOtherActions();
|
||||
final StorageGui gui = editable ? guiBuilder.enableAllInteractions().create() : guiBuilder.create();
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
if (items[i] != null) {
|
||||
gui.getInventory().setItem(i, items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the future with updated data (if editable) when the GUI is closed
|
||||
gui.setCloseGuiAction(event -> {
|
||||
if (!editable) {
|
||||
updatedData.complete(Optional.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get and save the updated items
|
||||
final ItemStack[] updatedItems = Arrays.copyOf(event.getPlayer().getOpenInventory()
|
||||
.getTopInventory().getContents().clone(), itemCount);
|
||||
BukkitSerializer.serializeItemStackArray(updatedItems).thenAccept(serializedItems -> {
|
||||
if (serializedItems.equals(itemData.serializedItems)) {
|
||||
updatedData.complete(Optional.empty());
|
||||
return;
|
||||
}
|
||||
updatedData.complete(Optional.of(new ItemData(serializedItems)));
|
||||
});
|
||||
});
|
||||
|
||||
// Display the GUI (synchronously; on the main server thread)
|
||||
Bukkit.getScheduler().runTask(plugin, () -> gui.open(player));
|
||||
}).exceptionally(throwable -> {
|
||||
// Handle exceptions
|
||||
updatedData.completeExceptionally(throwable);
|
||||
return null;
|
||||
});
|
||||
return updatedData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDead() {
|
||||
return player.getHealth() <= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||
@NotNull String iconMaterial, @NotNull String backgroundType) {
|
||||
try {
|
||||
final Material material = Material.matchMaterial(iconMaterial);
|
||||
Toast.builder(plugin)
|
||||
.setTitle(title.toComponent())
|
||||
.setDescription(description.toComponent())
|
||||
.setIcon(material != null ? material : Material.BARRIER)
|
||||
.setFrameType(FrameType.valueOf(backgroundType))
|
||||
.build()
|
||||
.show(player);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(@NotNull 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLocked() {
|
||||
return plugin.getLockedPlayers().contains(player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNpc() {
|
||||
return player.hasMetadata("NPC");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import dev.triumphteam.gui.builder.gui.StorageBuilder;
|
||||
import dev.triumphteam.gui.guis.Gui;
|
||||
import dev.triumphteam.gui.guis.StorageGui;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.roxeez.advancement.display.FrameType;
|
||||
import net.william278.andjam.Toast;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.BukkitUserDataHolder;
|
||||
import net.william278.husksync.data.Data;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Bukkit platform implementation of an {@link OnlineUser}
|
||||
*/
|
||||
public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final Player player;
|
||||
|
||||
private BukkitUser(@NotNull Player player, @NotNull HuskSync plugin) {
|
||||
super(player.getUniqueId(), player.getName());
|
||||
this.player = player;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static BukkitUser adapt(@NotNull Player player, @NotNull HuskSync plugin) {
|
||||
return new BukkitUser(player, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bukkit {@link Player} instance of this user
|
||||
*
|
||||
* @return the {@link Player} instance
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
return player == null || !player.isOnline();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Audience getAudience() {
|
||||
return ((BukkitHuskSync) plugin).getAudiences().player(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||
@NotNull String iconMaterial, @NotNull String backgroundType) {
|
||||
try {
|
||||
final Material material = Material.matchMaterial(iconMaterial);
|
||||
Toast.builder((BukkitHuskSync) plugin)
|
||||
.setTitle(title.toComponent())
|
||||
.setDescription(description.toComponent())
|
||||
.setIcon(material != null ? material : Material.BARRIER)
|
||||
.setFrameType(FrameType.valueOf(backgroundType))
|
||||
.build()
|
||||
.show(player);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, "Failed to send toast to player " + player.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showGui(@NotNull Data.Items items, @NotNull MineDown title, boolean editable, int size,
|
||||
@NotNull Consumer<Data.Items> onClose) {
|
||||
final ItemStack[] contents = ((BukkitData.Items) items).getContents();
|
||||
final StorageBuilder builder = Gui.storage().rows((int) Math.ceil(size / 9.0d));
|
||||
if (!editable) {
|
||||
builder.disableAllInteractions();
|
||||
}
|
||||
final StorageGui gui = builder.enableOtherActions()
|
||||
.apply(a -> a.getInventory().setContents(contents))
|
||||
.title(title.toComponent()).create();
|
||||
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
||||
Arrays.stream(close.getInventory().getContents()).limit(size).toArray(ItemStack[]::new)
|
||||
)));
|
||||
plugin.runSync(() -> gui.open(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@NotNull String node) {
|
||||
return player.hasPermission(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDead() {
|
||||
return player.getHealth() <= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLocked() {
|
||||
return plugin.getLockedPlayers().contains(player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNpc() {
|
||||
return player.hasMetadata("NPC");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Player getBukkitPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
@ApiStatus.Internal
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
}
|
@ -0,0 +1,279 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
public BukkitLegacyConverter(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DataSnapshot.Packed convert(@NotNull byte[] data) throws DataAdapter.AdaptionException {
|
||||
final JSONObject object = new JSONObject(plugin.getDataAdapter().bytesToString(data));
|
||||
final int version = object.getInt("format_version");
|
||||
if (version != 3) {
|
||||
throw new DataAdapter.AdaptionException(String.format(
|
||||
"Unsupported legacy data format version: %s. Please downgrade to an earlier version of HuskSync, " +
|
||||
"perform a manual legacy migration, then attempt to upgrade again.", version
|
||||
));
|
||||
}
|
||||
|
||||
// Read legacy data from the JSON object
|
||||
final DataSnapshot.Builder builder = DataSnapshot.builder(plugin)
|
||||
.saveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2)
|
||||
.data(readStatusData(object));
|
||||
readInventory(object).ifPresent(builder::inventory);
|
||||
readEnderChest(object).ifPresent(builder::enderChest);
|
||||
readLocation(object).ifPresent(builder::location);
|
||||
readAdvancements(object).ifPresent(builder::advancements);
|
||||
readStatistics(object).ifPresent(builder::statistics);
|
||||
return builder.buildAndPack();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<Identifier, Data> readStatusData(@NotNull JSONObject object) {
|
||||
if (!object.has("status_data")) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
final JSONObject status = object.getJSONObject("status_data");
|
||||
final HashMap<Identifier, Data> containers = new HashMap<>();
|
||||
if (shouldImport(Identifier.HEALTH)) {
|
||||
containers.put(Identifier.HEALTH, BukkitData.Health.from(
|
||||
status.getDouble("health"),
|
||||
status.getDouble("max_health"),
|
||||
status.getDouble("health_scale")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.HUNGER)) {
|
||||
containers.put(Identifier.HUNGER, BukkitData.Hunger.from(
|
||||
status.getInt("hunger"),
|
||||
status.getFloat("saturation"),
|
||||
status.getFloat("saturation_exhaustion")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.EXPERIENCE)) {
|
||||
containers.put(Identifier.EXPERIENCE, BukkitData.Experience.from(
|
||||
status.getInt("total_experience"),
|
||||
status.getInt("experience_level"),
|
||||
status.getFloat("experience_progress")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.GAME_MODE)) {
|
||||
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
|
||||
status.getString("game_mode"),
|
||||
status.getBoolean("is_flying"),
|
||||
status.getBoolean("is_flying")
|
||||
));
|
||||
}
|
||||
return containers;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Items.Inventory> readInventory(@NotNull JSONObject object) {
|
||||
if (!object.has("inventory") || !shouldImport(Identifier.INVENTORY)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject inventoryData = object.getJSONObject("inventory");
|
||||
return Optional.of(BukkitData.Items.Inventory.from(
|
||||
deserializeLegacyItemStacks(inventoryData.getString("serialized_items")), 0
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Items.EnderChest> readEnderChest(@NotNull JSONObject object) {
|
||||
if (!object.has("ender_chest") || !shouldImport(Identifier.ENDER_CHEST)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject inventoryData = object.getJSONObject("ender_chest");
|
||||
return Optional.of(BukkitData.Items.EnderChest.adapt(
|
||||
deserializeLegacyItemStacks(inventoryData.getString("serialized_items"))
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Location> readLocation(@NotNull JSONObject object) {
|
||||
if (!object.has("location") || !shouldImport(Identifier.LOCATION)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject locationData = object.getJSONObject("location");
|
||||
return Optional.of(BukkitData.Location.from(
|
||||
locationData.getDouble("x"),
|
||||
locationData.getDouble("y"),
|
||||
locationData.getDouble("z"),
|
||||
locationData.getFloat("yaw"),
|
||||
locationData.getFloat("pitch"),
|
||||
new Data.Location.World(
|
||||
locationData.getString("world_name"),
|
||||
UUID.fromString(locationData.getString("world_uuid")),
|
||||
locationData.getString("world_environment")
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Advancements> readAdvancements(@NotNull JSONObject object) {
|
||||
if (!object.has("advancements") || !shouldImport(Identifier.ADVANCEMENTS)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONArray advancements = object.getJSONArray("advancements");
|
||||
final List<Data.Advancements.Advancement> converted = new ArrayList<>();
|
||||
advancements.iterator().forEachRemaining(o -> {
|
||||
final JSONObject advancement = (JSONObject) JSONObject.wrap(o);
|
||||
final String key = advancement.getString("key");
|
||||
|
||||
final JSONObject criteria = advancement.getJSONObject("completed_criteria");
|
||||
final Map<String, Date> criteriaMap = new LinkedHashMap<>();
|
||||
criteria.keys().forEachRemaining(criteriaKey -> criteriaMap.put(
|
||||
criteriaKey, parseDate(criteria.getString(criteriaKey)))
|
||||
);
|
||||
converted.add(Data.Advancements.Advancement.adapt(key, criteriaMap));
|
||||
});
|
||||
|
||||
return Optional.of(BukkitData.Advancements.from(converted));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
|
||||
if (!object.has("statistics") || !shouldImport(Identifier.ADVANCEMENTS)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject stats = object.getJSONObject("statistics");
|
||||
return Optional.of(readStatisticMaps(
|
||||
stats.getJSONObject("untyped_statistics"),
|
||||
stats.getJSONObject("block_statistics"),
|
||||
stats.getJSONObject("item_statistics"),
|
||||
stats.getJSONObject("entity_statistics")
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private BukkitData.Statistics readStatisticMaps(@NotNull JSONObject untyped, @NotNull JSONObject blocks,
|
||||
@NotNull JSONObject items, @NotNull JSONObject entities) {
|
||||
final Map<Statistic, Integer> genericStats = new HashMap<>();
|
||||
untyped.keys().forEachRemaining(stat -> genericStats.put(Statistic.valueOf(stat), untyped.getInt(stat)));
|
||||
|
||||
final Map<Statistic, Map<Material, Integer>> blockStats = new HashMap<>();
|
||||
blocks.keys().forEachRemaining(stat -> {
|
||||
final JSONObject blockStat = blocks.getJSONObject(stat);
|
||||
final Map<Material, Integer> blockMap = new HashMap<>();
|
||||
blockStat.keys().forEachRemaining(block -> blockMap.put(Material.valueOf(block), blockStat.getInt(block)));
|
||||
blockStats.put(Statistic.valueOf(stat), blockMap);
|
||||
});
|
||||
|
||||
final Map<Statistic, Map<Material, Integer>> itemStats = new HashMap<>();
|
||||
items.keys().forEachRemaining(stat -> {
|
||||
final JSONObject itemStat = items.getJSONObject(stat);
|
||||
final Map<Material, Integer> itemMap = new HashMap<>();
|
||||
itemStat.keys().forEachRemaining(item -> itemMap.put(Material.valueOf(item), itemStat.getInt(item)));
|
||||
itemStats.put(Statistic.valueOf(stat), itemMap);
|
||||
});
|
||||
|
||||
final Map<Statistic, Map<EntityType, Integer>> entityStats = new HashMap<>();
|
||||
entities.keys().forEachRemaining(stat -> {
|
||||
final JSONObject entityStat = entities.getJSONObject(stat);
|
||||
final Map<EntityType, Integer> entityMap = new HashMap<>();
|
||||
entityStat.keys().forEachRemaining(entity -> entityMap.put(EntityType.valueOf(entity), entityStat.getInt(entity)));
|
||||
entityStats.put(Statistic.valueOf(stat), entityMap);
|
||||
});
|
||||
|
||||
return BukkitData.Statistics.from(genericStats, blockStats, itemStats, entityStats);
|
||||
}
|
||||
|
||||
// Deserialize a legacy item stack array
|
||||
@NotNull
|
||||
public ItemStack[] deserializeLegacyItemStacks(@NotNull String items) {
|
||||
// Return an empty array if there is no inventory data (set the player as having an empty inventory)
|
||||
if (items.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(items))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
final ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the ItemStacks in the array from deserialized ItemStack data
|
||||
int slotIndex = 0;
|
||||
for (ItemStack ignored : inventoryContents) {
|
||||
final ItemStack deserialized = deserializeLegacyItemStack(bukkitInputStream.readObject());
|
||||
inventoryContents[slotIndex] = deserialized;
|
||||
slotIndex++;
|
||||
}
|
||||
|
||||
// Return the converted contents
|
||||
return inventoryContents;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new DataAdapter.AdaptionException("Failed to deserialize legacy item stack data", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialize a single legacy item stack
|
||||
@Nullable
|
||||
private static ItemStack deserializeLegacyItemStack(@Nullable Object serializedItemStack) {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
}
|
||||
|
||||
|
||||
private boolean shouldImport(@NotNull Identifier type) {
|
||||
return plugin.getSettings().isSyncFeatureEnabled(type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Date parseDate(@NotNull String dateString) {
|
||||
try {
|
||||
return new SimpleDateFormat().parse(dateString);
|
||||
} catch (ParseException e) {
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,378 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import de.tr7zw.changeme.nbtapi.NBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.mapdataapi.MapBanner;
|
||||
import net.william278.mapdataapi.MapData;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.*;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public interface BukkitMapPersister {
|
||||
|
||||
// The map used to store HuskSync data in ItemStack NBT
|
||||
String MAP_DATA_KEY = "husksync:persisted_locked_map";
|
||||
// The key used to store the serialized map data in NBT
|
||||
String MAP_PIXEL_DATA_KEY = "canvas_data";
|
||||
// The key used to store the map of World UIDs to MapView IDs in NBT
|
||||
String MAP_VIEW_ID_MAPPINGS_KEY = "id_mappings";
|
||||
|
||||
/**
|
||||
* Persist locked maps in an array of {@link ItemStack}s
|
||||
*
|
||||
* @param items the array of {@link ItemStack}s to persist locked maps in
|
||||
* @param delegateRenderer the player to delegate the rendering of map pixel canvases to
|
||||
* @return the array of {@link ItemStack}s with locked maps persisted to serialized NBT
|
||||
*/
|
||||
@NotNull
|
||||
default ItemStack[] persistLockedMaps(@NotNull ItemStack[] items, @NotNull Player delegateRenderer) {
|
||||
if (!getPlugin().getSettings().doPersistLockedMaps()) {
|
||||
return items;
|
||||
}
|
||||
return forEachMap(items, map -> this.persistMapView(map, delegateRenderer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply persisted locked maps to an array of {@link ItemStack}s
|
||||
*
|
||||
* @param items the array of {@link ItemStack}s to apply persisted locked maps to
|
||||
* @return the array of {@link ItemStack}s with persisted locked maps applied
|
||||
*/
|
||||
@NotNull
|
||||
default ItemStack[] setMapViews(@NotNull ItemStack[] items) {
|
||||
if (!getPlugin().getSettings().doPersistLockedMaps()) {
|
||||
return items;
|
||||
}
|
||||
return forEachMap(items, this::applyMapView);
|
||||
}
|
||||
|
||||
// Perform an operation on each map in an array of ItemStacks
|
||||
@NotNull
|
||||
private ItemStack[] forEachMap(@NotNull ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
final ItemStack item = items[i];
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
|
||||
items[i] = function.apply(item);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
|
||||
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
||||
if (!meta.hasMapView()) {
|
||||
return map;
|
||||
}
|
||||
final MapView view = meta.getMapView();
|
||||
if (view == null || view.getWorld() == null || !view.isLocked() || view.isVirtual()) {
|
||||
return map;
|
||||
}
|
||||
|
||||
NBT.modify(map, nbt -> {
|
||||
// Don't save the map's data twice
|
||||
if (nbt.hasTag(MAP_DATA_KEY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Render the map
|
||||
final PersistentMapCanvas canvas = new PersistentMapCanvas(view);
|
||||
for (MapRenderer renderer : view.getRenderers()) {
|
||||
renderer.render(view, canvas, delegateRenderer);
|
||||
getPlugin().debug(String.format("Rendered locked map canvas to view (#%s)", view.getId()));
|
||||
}
|
||||
|
||||
// Persist map data
|
||||
final ReadWriteNBT mapData = nbt.getOrCreateCompound(MAP_DATA_KEY);
|
||||
final String worldUid = view.getWorld().getUID().toString();
|
||||
mapData.setByteArray(MAP_PIXEL_DATA_KEY, canvas.extractMapData().toBytes());
|
||||
nbt.getOrCreateCompound(MAP_VIEW_ID_MAPPINGS_KEY).setInteger(worldUid, view.getId());
|
||||
getPlugin().debug(String.format("Saved data for locked map (#%s, UID: %s)", view.getId(), worldUid));
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ItemStack applyMapView(@NotNull ItemStack map) {
|
||||
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
||||
NBT.get(map, nbt -> {
|
||||
if (!nbt.hasTag(MAP_DATA_KEY)) {
|
||||
return nbt;
|
||||
}
|
||||
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
|
||||
|
||||
// Search for an existing map view
|
||||
final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY);
|
||||
Optional<String> world = Optional.empty();
|
||||
for (String worldUid : mapIds.getKeys()) {
|
||||
world = Bukkit.getWorlds().stream()
|
||||
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
|
||||
.findFirst();
|
||||
if (world.isPresent()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (world.isPresent()) {
|
||||
final String uid = world.get();
|
||||
final Optional<MapView> existingView = this.getMapView(mapIds.getInteger(uid));
|
||||
if (existingView.isPresent()) {
|
||||
final MapView view = existingView.get();
|
||||
view.setLocked(true);
|
||||
meta.setMapView(view);
|
||||
map.setItemMeta(meta);
|
||||
getPlugin().debug(String.format("View exists (#%s); updated map (UID: %s)", view.getId(), uid));
|
||||
return nbt;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the pixel data and generate a map view otherwise
|
||||
final MapData canvasData;
|
||||
try {
|
||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||
canvasData = MapData.fromByteArray(mapData.getByteArray(MAP_PIXEL_DATA_KEY));
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
|
||||
return nbt;
|
||||
}
|
||||
|
||||
// Add a renderer to the map with the data
|
||||
final MapView view = generateRenderedMap(canvasData);
|
||||
final String worldUid = getDefaultMapWorld().getUID().toString();
|
||||
meta.setMapView(view);
|
||||
map.setItemMeta(meta);
|
||||
|
||||
// Set the map view ID in NBT
|
||||
NBT.modify(map, editable -> {
|
||||
editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY).setInteger(worldUid, view.getId());
|
||||
});
|
||||
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
|
||||
return nbt;
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
// Sets the renderer of a map, and returns the generated MapView
|
||||
@NotNull
|
||||
private MapView generateRenderedMap(@NotNull MapData canvasData) {
|
||||
final MapView view = Bukkit.createMap(getDefaultMapWorld());
|
||||
view.getRenderers().clear();
|
||||
|
||||
// Create a new map view renderer with the map data color at each pixel
|
||||
view.addRenderer(new PersistentMapRenderer(canvasData));
|
||||
view.setLocked(true);
|
||||
view.setScale(MapView.Scale.NORMAL);
|
||||
view.setTrackingPosition(false);
|
||||
view.setUnlimitedTracking(false);
|
||||
|
||||
// Set the view to the map and return it
|
||||
setMapView(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static World getDefaultMapWorld() {
|
||||
final World world = Bukkit.getWorlds().get(0);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("No worlds are loaded on the server!");
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
default Optional<MapView> getMapView(int id) {
|
||||
return getMapViews().containsKey(id) ? Optional.of(getMapViews().get(id)) : Optional.empty();
|
||||
}
|
||||
|
||||
default void setMapView(@NotNull MapView view) {
|
||||
getMapViews().put(view.getId(), view);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MapRenderer} that can be used to render persistently serialized {@link MapData} to a {@link MapView}
|
||||
*/
|
||||
class PersistentMapRenderer extends MapRenderer {
|
||||
|
||||
private final MapData canvasData;
|
||||
|
||||
private PersistentMapRenderer(@NotNull MapData canvasData) {
|
||||
super(false);
|
||||
this.canvasData = canvasData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player player) {
|
||||
// We set the pixels in this order to avoid the map being rendered upside down
|
||||
for (int i = 0; i < 128; i++) {
|
||||
for (int j = 0; j < 128; j++) {
|
||||
canvas.setPixel(j, i, (byte) canvasData.getColorAt(i, j));
|
||||
}
|
||||
}
|
||||
|
||||
// Set the map banners and markers
|
||||
final MapCursorCollection cursors = canvas.getCursors();
|
||||
canvasData.getBanners().forEach(banner -> cursors.addCursor(createBannerCursor(banner)));
|
||||
canvas.setCursors(cursors);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
|
||||
return new MapCursor(
|
||||
(byte) banner.getPosition().getX(),
|
||||
(byte) banner.getPosition().getZ(),
|
||||
(byte) 0,
|
||||
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
|
||||
case "white" -> MapCursor.Type.BANNER_WHITE;
|
||||
case "orange" -> MapCursor.Type.BANNER_ORANGE;
|
||||
case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
|
||||
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
|
||||
case "yellow" -> MapCursor.Type.BANNER_YELLOW;
|
||||
case "lime" -> MapCursor.Type.BANNER_LIME;
|
||||
case "pink" -> MapCursor.Type.BANNER_PINK;
|
||||
case "gray" -> MapCursor.Type.BANNER_GRAY;
|
||||
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
|
||||
case "cyan" -> MapCursor.Type.BANNER_CYAN;
|
||||
case "purple" -> MapCursor.Type.BANNER_PURPLE;
|
||||
case "blue" -> MapCursor.Type.BANNER_BLUE;
|
||||
case "brown" -> MapCursor.Type.BANNER_BROWN;
|
||||
case "green" -> MapCursor.Type.BANNER_GREEN;
|
||||
case "red" -> MapCursor.Type.BANNER_RED;
|
||||
default -> MapCursor.Type.BANNER_BLACK;
|
||||
},
|
||||
true,
|
||||
banner.getText().isEmpty() ? null : banner.getText()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
|
||||
*/
|
||||
class PersistentMapCanvas implements MapCanvas {
|
||||
|
||||
private final MapView mapView;
|
||||
private final int[][] pixels = new int[128][128];
|
||||
private MapCursorCollection cursors;
|
||||
|
||||
private PersistentMapCanvas(@NotNull MapView mapView) {
|
||||
this.mapView = mapView;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MapCursorCollection getCursors() {
|
||||
return cursors == null ? (cursors = new MapCursorCollection()) : cursors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCursors(@NotNull MapCursorCollection cursors) {
|
||||
this.cursors = cursors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixel(int x, int y, byte color) {
|
||||
pixels[x][y] = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getPixel(int x, int y) {
|
||||
return (byte) pixels[x][y];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getBasePixel(int x, int y) {
|
||||
return getPixel(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawImage(int x, int y, @NotNull Image image) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawText(int x, int y, @NotNull MapFont font, @NotNull String text) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getDimension() {
|
||||
return mapView.getWorld() != null ? switch (mapView.getWorld().getEnvironment()) {
|
||||
case NETHER -> "minecraft:the_nether";
|
||||
case THE_END -> "minecraft:the_end";
|
||||
default -> "minecraft:overworld";
|
||||
} : "minecraft:overworld";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the map data from the canvas. Must be rendered first
|
||||
*
|
||||
* @return the extracted map data
|
||||
*/
|
||||
@NotNull
|
||||
private MapData extractMapData() {
|
||||
final List<MapBanner> banners = new ArrayList<>();
|
||||
for (int i = 0; i < getCursors().size(); i++) {
|
||||
final MapCursor cursor = getCursors().getCursor(i);
|
||||
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
|
||||
if (type.startsWith("banner_")) {
|
||||
banners.add(new MapBanner(
|
||||
type.replaceAll("banner_", ""),
|
||||
cursor.getCaption() == null ? "" : cursor.getCaption(),
|
||||
cursor.getX(),
|
||||
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
|
||||
cursor.getY()
|
||||
));
|
||||
}
|
||||
}
|
||||
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Map<Integer, MapView> getMapViews();
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||
import space.arim.morepaperlib.scheduling.ScheduledTask;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public interface BukkitTask extends Task {
|
||||
|
||||
class Sync extends Task.Sync implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
|
||||
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
|
||||
super(plugin, runnable, delayTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (task != null && !cancelled) {
|
||||
task.cancel();
|
||||
}
|
||||
super.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isPluginDisabled()) {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final RegionalScheduler scheduler = ((BukkitHuskSync) getPlugin()).getRegionalScheduler();
|
||||
if (delayTicks > 0) {
|
||||
this.task = scheduler.runDelayed(runnable, delayTicks);
|
||||
} else {
|
||||
this.task = scheduler.run(runnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Async extends Task.Async implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
|
||||
protected Async(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
|
||||
super(plugin, runnable, delayTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (task != null && !cancelled) {
|
||||
task.cancel();
|
||||
}
|
||||
super.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isPluginDisabled()) {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AsynchronousScheduler scheduler = ((BukkitHuskSync) getPlugin()).getAsyncScheduler();
|
||||
if (delayTicks > 0) {
|
||||
plugin.debug("Running async task with delay of " + delayTicks + " ticks");
|
||||
this.task = scheduler.runDelayed(
|
||||
runnable,
|
||||
Duration.of(delayTicks * 50L, ChronoUnit.MILLIS)
|
||||
);
|
||||
} else {
|
||||
this.task = scheduler.run(runnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Repeating extends Task.Repeating implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
|
||||
protected Repeating(@NotNull HuskSync plugin, @NotNull Runnable runnable, long repeatingTicks) {
|
||||
super(plugin, runnable, repeatingTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (task != null && !cancelled) {
|
||||
task.cancel();
|
||||
}
|
||||
super.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isPluginDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
final AsynchronousScheduler scheduler = ((BukkitHuskSync) getPlugin()).getAsyncScheduler();
|
||||
this.task = scheduler.runAtFixedRate(
|
||||
runnable, Duration.ZERO,
|
||||
Duration.of(repeatingTicks * 50L, ChronoUnit.MILLIS)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns if the Bukkit HuskSync plugin is disabled
|
||||
default boolean isPluginDisabled() {
|
||||
return !((BukkitHuskSync) getPlugin()).isEnabled();
|
||||
}
|
||||
|
||||
interface Supplier extends Task.Supplier {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Sync getSyncTask(@NotNull Runnable runnable, long delayTicks) {
|
||||
return new Sync(getPlugin(), runnable, delayTicks);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Async getAsyncTask(@NotNull Runnable runnable, long delayTicks) {
|
||||
return new Async(getPlugin(), runnable, delayTicks);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Repeating getRepeatingTask(@NotNull Runnable runnable, long repeatingTicks) {
|
||||
return new Repeating(getPlugin(), runnable, repeatingTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void cancelTasks() {
|
||||
((BukkitHuskSync) getPlugin()).getScheduler().cancelGlobalTasks();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,40 +1,33 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'commons-io:commons-io:2.13.0'
|
||||
implementation 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
implementation 'net.kyori:adventure-api:4.14.0'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'dev.dejvokep:boosted-yaml:1.3.1'
|
||||
implementation 'net.william278:Annotaml:2.0.1'
|
||||
implementation 'net.william278:DesertWell:2.0.4'
|
||||
implementation 'net.william278:PagineDown:1.1'
|
||||
implementation('com.zaxxer:HikariCP:5.0.1') {
|
||||
api 'commons-io:commons-io:2.13.0'
|
||||
api 'org.apache.commons:commons-text:1.10.0'
|
||||
api 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
api 'net.kyori:adventure-api:4.14.0'
|
||||
api 'org.json:json:20230618'
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||
api 'dev.dejvokep:boosted-yaml:1.3.1'
|
||||
api 'net.william278:annotaml:2.0.7'
|
||||
api 'net.william278:DesertWell:2.0.4'
|
||||
api 'net.william278:PagineDown:1.1'
|
||||
api('com.zaxxer:HikariCP:5.0.1') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
compileOnly 'org.jetbrains:annotations:24.0.1'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
compileOnly 'redis.clients:jedis:' + jedis_version
|
||||
compileOnly 'org.xerial.snappy:snappy-java:' + snappy_version
|
||||
compileOnly 'org.apache.commons:commons-text:' + commons_text_version
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
||||
compileOnly "org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version"
|
||||
compileOnly "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
testImplementation 'redis.clients:jedis:' + jedis_version
|
||||
testImplementation 'org.xerial.snappy:snappy-java:' + snappy_version
|
||||
testImplementation 'org.apache.commons:commons-text:' + commons_text_version
|
||||
testImplementation "redis.clients:jedis:$jedis_version"
|
||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
testCompileOnly 'dev.dejvokep:boosted-yaml:1.3.1'
|
||||
testCompileOnly 'org.jetbrains:annotations:24.0.1'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
|
||||
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.kyori', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
|
||||
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
|
||||
relocate 'net.william278.annotaml', 'net.william278.husksync.libraries.annotaml'
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* An adapter that adapts data to and from a portable byte array.
|
||||
*/
|
||||
public interface DataAdapter {
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a string.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The string
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
default <A extends Adaptable> String toString(@NotNull A data) throws AdaptionException {
|
||||
return new String(this.toBytes(data), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a byte array.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The byte array
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
<A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a JSON string to an {@link Adaptable}.
|
||||
*
|
||||
* @param data The JSON string to adapt.
|
||||
* @param type The class type of the {@link Adaptable} to adapt to.
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The {@link Adaptable}
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
<A extends Adaptable> A fromJson(@NotNull String data, @NotNull Class<A> type) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a JSON string.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The JSON string
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
<A extends Adaptable> String toJson(@NotNull A data) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to an {@link Adaptable}.
|
||||
*
|
||||
* @param data The byte array to adapt.
|
||||
* @param type The class type of the {@link Adaptable} to adapt to.
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The {@link Adaptable}
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
<A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to a string, including decompression if required.
|
||||
*
|
||||
* @param bytes The byte array to convert
|
||||
* @return the string form of the bytes
|
||||
*/
|
||||
@NotNull
|
||||
String bytesToString(byte[] bytes);
|
||||
|
||||
final class AdaptionException extends IllegalStateException {
|
||||
static final String FORMAT = "An exception occurred when adapting serialized/deserialized data: %s";
|
||||
|
||||
public AdaptionException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(String.format(FORMAT, message), cause);
|
||||
}
|
||||
|
||||
public AdaptionException(@NotNull String message) {
|
||||
super(String.format(FORMAT, message));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class GsonAdapter implements DataAdapter {
|
||||
|
||||
private final HuskSync plugin;
|
||||
|
||||
public GsonAdapter(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||
return this.toJson(data).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> String toJson(@NotNull A data) throws AdaptionException {
|
||||
try {
|
||||
return plugin.getGson().toJson(data);
|
||||
} catch (Throwable e) {
|
||||
throw new AdaptionException("Failed to adapt data to JSON via Gson", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
return this.fromJson(new String(data, StandardCharsets.UTF_8), type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String bytesToString(byte[] bytes) {
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public <A extends Adaptable> A fromJson(@NotNull String data, @NotNull Class<A> type) throws AdaptionException {
|
||||
try {
|
||||
return plugin.getGson().fromJson(data, type);
|
||||
} catch (Throwable e) {
|
||||
throw new AdaptionException("Failed to adapt data from JSON via Gson", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.xerial.snappy.Snappy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SnappyGsonAdapter extends GsonAdapter {
|
||||
|
||||
public SnappyGsonAdapter(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||
try {
|
||||
return Snappy.compress(super.toBytes(data));
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to compress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
try {
|
||||
return super.fromBytes(decompressBytes(data), type);
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to decompress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String bytesToString(byte[] bytes) {
|
||||
try {
|
||||
return super.bytesToString(decompressBytes(bytes));
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to decompress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decompressBytes(byte[] bytes) throws IOException {
|
||||
return Snappy.uncompress(bytes);
|
||||
}
|
||||
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* The base implementation of the HuskSync API, containing cross-platform API calls.
|
||||
* </p>
|
||||
* This class should not be used directly, but rather through platform-specific extending API classes.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BaseHuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the implementing plugin.
|
||||
*/
|
||||
protected final HuskSync plugin;
|
||||
|
||||
protected BaseHuskSyncAPI(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} by the given player's account {@link UUID}, if they exist.
|
||||
*
|
||||
* @param uuid the unique id of the player to get the {@link User} instance for
|
||||
* @return future returning the {@link User} instance for the given player's unique id if they exist, otherwise an empty {@link Optional}
|
||||
* @apiNote The player does not have to be online
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return plugin.getDatabase().getUser(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} by the given player's username (case-insensitive), if they exist.
|
||||
*
|
||||
* @param username the username of the {@link User} instance for
|
||||
* @return future returning the {@link User} instance for the given player's username if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote The player does not have to be online, though their username has to be the username
|
||||
* they had when they last joined the server.
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<User>> getUser(@NotNull String username) {
|
||||
return plugin.getDatabase().getUserByName(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User}'s current {@link UserData}
|
||||
*
|
||||
* @param user the {@link User} to get the {@link UserData} for
|
||||
* @return future returning the {@link UserData} for the given {@link User} if they exist, otherwise an empty {@link Optional}
|
||||
* @apiNote If the user is not online on the implementing bukkit server,
|
||||
* the {@link UserData} returned will be their last database-saved UserData.
|
||||
* </p>
|
||||
* Because of this, if the user is online on another server on the network,
|
||||
* then the {@link UserData} returned by this method will <i>not necessarily reflective of
|
||||
* their current state</i>
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (user instanceof OnlineUser) {
|
||||
return ((OnlineUser) user).getUserData(plugin).join();
|
||||
} else {
|
||||
return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link UserData} to the database for the given {@link User}.
|
||||
* </p>
|
||||
* If the user is online and on the same cluster, their data will be updated in game.
|
||||
*
|
||||
* @param user the {@link User} to set the {@link UserData} for
|
||||
* @param userData the {@link UserData} to set for the given {@link User}
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
|
||||
return CompletableFuture.runAsync(() ->
|
||||
plugin.getDatabase().setUserData(user, userData, DataSaveCause.API)
|
||||
.thenRun(() -> plugin.getRedisManager().sendUserDataUpdate(user, userData).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the {@link UserData} of an {@link OnlineUser} to the database
|
||||
*
|
||||
* @param user the {@link OnlineUser} to save the {@link UserData} of
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) {
|
||||
return CompletableFuture.runAsync(() -> user.getUserData(plugin)
|
||||
.thenAccept(optionalUserData -> optionalUserData.ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the saved {@link UserDataSnapshot} records for the given {@link User}
|
||||
*
|
||||
* @param user the {@link User} to get the {@link UserDataSnapshot} for
|
||||
* @return future returning a list {@link UserDataSnapshot} for the given {@link User} if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote The length of the list of VersionedUserData will correspond to the configured
|
||||
* {@code max_user_data_records} config option
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<List<UserDataSnapshot>> getSavedUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> plugin.getDatabase().getUserData(user).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON string representation of the given {@link UserData}
|
||||
*
|
||||
* @param userData the {@link UserData} to get the JSON string representation of
|
||||
* @param prettyPrint whether to pretty print the JSON string
|
||||
* @return the JSON string representation of the given {@link UserData}
|
||||
* @since 2.0
|
||||
*/
|
||||
@NotNull
|
||||
public final String getUserDataJson(@NotNull UserData userData, boolean prettyPrint) {
|
||||
return plugin.getDataAdapter().toJson(userData, prettyPrint);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,458 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* The base implementation of the HuskSync API, containing cross-platform API calls.
|
||||
* </p>
|
||||
* This class should not be used directly, but rather through platform-specific extending API classes.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class HuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the implementing plugin.
|
||||
*/
|
||||
protected final HuskSync plugin;
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the base API class.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
protected HuskSyncAPI(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User} by their UUID
|
||||
*
|
||||
* @param uuid The UUID of the user to get
|
||||
* @return A future containing the user, or an empty optional if the user doesn't exist
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().getUser(uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User} by their username
|
||||
*
|
||||
* @param username The username of the user to get
|
||||
* @return A future containing the user, or an empty optional if the user doesn't exist
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull String username) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().getUserByName(username));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new data snapshot of an {@link OnlineUser}'s data.
|
||||
*
|
||||
* @param user The user to create the snapshot of
|
||||
* @return The snapshot of the user's data
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed createSnapshot(@NotNull OnlineUser user) {
|
||||
return snapshotBuilder().saveCause(DataSnapshot.SaveCause.API).buildAndPack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User}'s current data, as a {@link DataSnapshot.Unpacked}
|
||||
* <p>
|
||||
* If the user is online, this will create a new snapshot of their data with the {@code API} data save cause.
|
||||
* </p>
|
||||
* If the user is offline, this will return the latest snapshot of their data if that exists
|
||||
* (an empty optional will be returned otherwise).
|
||||
*
|
||||
* @param user The user to get the data of
|
||||
* @return A future containing the user's current data, or an empty optional if the user has no data
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
|
||||
return plugin.getRedisManager()
|
||||
.getUserData(UUID.randomUUID(), user)
|
||||
.thenApply(data -> data.or(() -> plugin.getDatabase().getLatestSnapshot(user)))
|
||||
.thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's current data.
|
||||
* <p>
|
||||
* This will update the user's data in the database (creating a new snapshot) and send a data update,
|
||||
* updating the user if they are online.
|
||||
*
|
||||
* @param user The user to set the data of
|
||||
* @param data The data to set
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentData(@NotNull User user, @NotNull DataSnapshot data) {
|
||||
plugin.runAsync(() -> {
|
||||
final DataSnapshot.Packed packed = data instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) data;
|
||||
addSnapshot(user, packed);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, packed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a user's current data.
|
||||
* <p>
|
||||
* This will update the user's data in the database (creating a new snapshot) and send a data update,
|
||||
* updating the user if they are online.
|
||||
*
|
||||
* @param user The user to edit the data of
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentData(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
getCurrentData(user).thenAccept(optional -> optional.ifPresent(data -> {
|
||||
editor.accept(data);
|
||||
setCurrentData(user, data);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all saved data snapshots for a user
|
||||
*
|
||||
* @param user The user to get the data snapshots of
|
||||
* @return The user's data snapshots
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshots(@NotNull User user) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getAllSnapshots(user).stream()
|
||||
.map(snapshot -> snapshot.unpack(plugin))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific data snapshot for a user
|
||||
*
|
||||
* @param user The user to get the data snapshot of
|
||||
* @param versionId The version ID of the snapshot to get
|
||||
* @return The user's data snapshot, or an empty optional if the user has no data
|
||||
* @see #getSnapshots(User)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshot(@NotNull User user, @NotNull UUID versionId) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getSnapshot(user, versionId).stream()
|
||||
.map(snapshot -> snapshot.unpack(plugin))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a data snapshot for a user
|
||||
*
|
||||
* @param user The user to edit the snapshot of
|
||||
* @param versionId The version ID of the snapshot to edit
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editSnapshot(@NotNull User user, @NotNull UUID versionId,
|
||||
@NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().getSnapshot(user, versionId).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
plugin.getDatabase().updateSnapshot(user, unpacked.pack(plugin));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest data snapshot for a user that has been saved in the database.
|
||||
* <p>
|
||||
* Not to be confused with {@link #getCurrentData(User)}, which will return the current data of a user
|
||||
* if they are online (this method will only return their latest <i>saved</i> snapshot).
|
||||
* </p>
|
||||
*
|
||||
* @param user The user to get the latest data snapshot of
|
||||
* @return The user's latest data snapshot, or an empty optional if the user has no data
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getLatestSnapshot(@NotNull User user) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getLatestSnapshot(user).map(snapshot -> snapshot.unpack(plugin))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the latest data snapshot for a user
|
||||
*
|
||||
* @param user The user to edit the latest snapshot of
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editLatestSnapshot(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().getLatestSnapshot(user).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
plugin.getDatabase().updateSnapshot(user, unpacked.pack(plugin));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a data snapshot to the database
|
||||
*
|
||||
* @param user The user to save the data for
|
||||
* @param snapshot The snapshot to save
|
||||
* @since 3.0
|
||||
*/
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().addSnapshot(
|
||||
user, snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an <i>existing</i> data snapshot in the database.
|
||||
* Not to be confused with {@link #addSnapshot(User, DataSnapshot)}, which will add a new snapshot if one
|
||||
* snapshot doesn't exist.
|
||||
*
|
||||
* @param user The user to update the snapshot of
|
||||
* @param snapshot The snapshot to update
|
||||
* @since 3.0
|
||||
*/
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().updateSnapshot(
|
||||
user, snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a data snapshot, preventing it from being rotated
|
||||
*
|
||||
* @param user The user to pin the snapshot of
|
||||
* @param snapshotVersion The version ID of the snapshot to pin
|
||||
* @since 3.0
|
||||
*/
|
||||
public void pinSnapshot(@NotNull User user, @NotNull UUID snapshotVersion) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().pinSnapshot(user, snapshotVersion));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a data snapshot, allowing it to be rotated
|
||||
*
|
||||
* @param user The user to unpin the snapshot of
|
||||
* @param snapshotVersion The version ID of the snapshot to unpin
|
||||
* @since 3.0
|
||||
*/
|
||||
public void unpinSnapshot(@NotNull User user, @NotNull UUID snapshotVersion) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().unpinSnapshot(user, snapshotVersion));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a data snapshot from the database
|
||||
*
|
||||
* @param user The user to delete the snapshot of
|
||||
* @param versionId The version ID of the snapshot to delete
|
||||
* @return A future which will complete with true if the snapshot was deleted, or false if it wasn't
|
||||
* (e.g., if the snapshot didn't exist)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Boolean> deleteSnapshot(@NotNull User user, @NotNull UUID versionId) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().deleteSnapshot(user, versionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a data snapshot from the database
|
||||
*
|
||||
* @param user The user to delete the snapshot of
|
||||
* @param snapshot The snapshot to delete
|
||||
* @return A future which will complete with true if the snapshot was deleted, or false if it wasn't
|
||||
* (e.g., if the snapshot hasn't been saved to the database yet)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Boolean> deleteSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
return deleteSnapshot(user, snapshot.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new custom data type serializer.
|
||||
* <p>
|
||||
* This allows for custom {@link Data} types to be persisted in {@link DataSnapshot}s. To register
|
||||
* a new data type, you must provide a {@link Serializer} for serializing and deserializing the data type
|
||||
* and invoke this method.
|
||||
* </p>
|
||||
* You'll need to do this on every server you wish to sync data between. On servers where the registered
|
||||
* data type is not present, the data will be ignored and snapshots created on that server will not
|
||||
* contain the data.
|
||||
*
|
||||
* @param identifier The identifier of the data type to register.
|
||||
* Create one using {@code Identifier.from(Key.of("your_plugin_name", "key"))}
|
||||
* @param serializer An implementation of {@link Serializer} for serializing and deserializing the {@link Data}
|
||||
* @param <T> A type extending {@link Data}; this will represent the data being held.
|
||||
*/
|
||||
public <T extends Data> void registerDataSerializer(@NotNull Identifier identifier,
|
||||
@NotNull Serializer<T> serializer) {
|
||||
plugin.registerSerializer(identifier, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||
*
|
||||
* @param unpacked The unpacked snapshot
|
||||
* @return The packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed packSnapshot(@NotNull DataSnapshot.Unpacked unpacked) {
|
||||
return unpacked.pack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||
*
|
||||
* @param packed The packed snapshot
|
||||
* @return The unpacked snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked unpackSnapshot(@NotNull DataSnapshot.Packed packed) {
|
||||
return packed.unpack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack, edit, and repack a data snapshot.
|
||||
* </p>
|
||||
* This won't save the snapshot to the database; it'll just edit the data snapshot in place.
|
||||
*
|
||||
* @param packed The packed snapshot
|
||||
* @param editor An editor function for editing the unpacked snapshot
|
||||
* @return The edited packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed editPackedSnapshot(@NotNull DataSnapshot.Packed packed,
|
||||
@NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
final DataSnapshot.Unpacked unpacked = packed.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
return unpacked.pack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the estimated size of a {@link DataSnapshot} in bytes
|
||||
*
|
||||
* @param snapshot The snapshot to get the size of
|
||||
* @return The size of the snapshot in bytes
|
||||
* @since 3.0
|
||||
*/
|
||||
public int getSnapshotFileSize(@NotNull DataSnapshot snapshot) {
|
||||
return (snapshot instanceof DataSnapshot.Packed packed)
|
||||
? packed.getFileSize(plugin)
|
||||
: ((DataSnapshot.Unpacked) snapshot).pack(plugin).getFileSize(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a builder for creating a new data snapshot
|
||||
*
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Builder snapshotBuilder() {
|
||||
return DataSnapshot.builder(plugin).saveCause(DataSnapshot.SaveCause.API);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a JSON string to an {@link Adaptable}
|
||||
*
|
||||
* @param serialized The serialized JSON string
|
||||
* @param type The type of the element
|
||||
* @param <T> The type of the element
|
||||
* @return The deserialized element
|
||||
* @throws Serializer.DeserializationException If the element could not be deserialized
|
||||
*/
|
||||
@NotNull
|
||||
public <T extends Adaptable> T deserializeData(@NotNull String serialized, Class<T> type)
|
||||
throws Serializer.DeserializationException {
|
||||
return plugin.getDataAdapter().fromJson(serialized, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an {@link Adaptable} to a JSON string
|
||||
*
|
||||
* @param element The element to serialize
|
||||
* @param <T> The type of the element
|
||||
* @return The serialized JSON string
|
||||
* @throws Serializer.SerializationException If the element could not be serialized
|
||||
*/
|
||||
@NotNull
|
||||
public <T extends Adaptable> String serializeData(@NotNull T element)
|
||||
throws Serializer.SerializationException {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Get the plugin instance
|
||||
*
|
||||
* @return The plugin instance
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception indicating the plugin has been accessed before it has been registered.
|
||||
*/
|
||||
static final class NotRegisteredException extends IllegalStateException {
|
||||
|
||||
private static final String MESSAGE = """
|
||||
Could not access the HuskSync API as it has not yet been registered. This could be because:
|
||||
1) HuskSync has failed to enable successfully
|
||||
2) Your plugin isn't set to load after HuskSync has
|
||||
(Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)
|
||||
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.
|
||||
4) You have shaded HuskSync into your plugin jar and need to fix your maven/gradle/build script
|
||||
to only include HuskSync as a dependency and not as a shaded dependency.""";
|
||||
|
||||
NotRegisteredException() {
|
||||
super(MESSAGE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class Command extends Node {
|
||||
|
||||
private final String usage;
|
||||
private final Map<String, Boolean> additionalPermissions;
|
||||
|
||||
protected Command(@NotNull String name, @NotNull List<String> aliases, @NotNull String usage,
|
||||
@NotNull HuskSync plugin) {
|
||||
super(name, aliases, plugin);
|
||||
this.usage = usage;
|
||||
this.additionalPermissions = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onExecuted(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
if (!executor.hasPermission(getPermission())) {
|
||||
plugin.getLocales().getLocale("error_no_permission")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.runAsync(() -> this.execute(executor, args));
|
||||
}
|
||||
|
||||
public abstract void execute(@NotNull CommandUser executor, @NotNull String[] args);
|
||||
|
||||
@NotNull
|
||||
public final String getRawUsage() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final String getUsage() {
|
||||
return "/" + getName() + " " + getRawUsage();
|
||||
}
|
||||
|
||||
public final void addAdditionalPermissions(@NotNull Map<String, Boolean> permissions) {
|
||||
permissions.forEach((permission, value) -> this.additionalPermissions.put(getPermission(permission), value));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final Map<String, Boolean> getAdditionalPermissions() {
|
||||
return additionalPermissions;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDescription() {
|
||||
return plugin.getLocales().getRawLocale(getName() + "_command_description")
|
||||
.orElse(getUsage());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an abstract cross-platform representation for a plugin command
|
||||
*/
|
||||
public abstract class CommandBase {
|
||||
|
||||
/**
|
||||
* The input string to match for this command
|
||||
*/
|
||||
public final String command;
|
||||
|
||||
/**
|
||||
* The permission node required to use this command
|
||||
*/
|
||||
public final String permission;
|
||||
|
||||
/**
|
||||
* Alias input strings for this command
|
||||
*/
|
||||
public final String[] aliases;
|
||||
|
||||
/**
|
||||
* Instance of the implementing plugin
|
||||
*/
|
||||
public final HuskSync plugin;
|
||||
|
||||
|
||||
public CommandBase(@NotNull String command, @NotNull Permission permission, @NotNull HuskSync implementor, String... aliases) {
|
||||
this.command = command;
|
||||
this.permission = permission.node;
|
||||
this.plugin = implementor;
|
||||
this.aliases = aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the command is executed
|
||||
*
|
||||
* @param player {@link OnlineUser} executing the command
|
||||
* @param args Command arguments
|
||||
*/
|
||||
public abstract void onExecute(@NotNull OnlineUser player, @NotNull String[] args);
|
||||
|
||||
/**
|
||||
* Returns the localised description string of this command
|
||||
*
|
||||
* @return the command description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return plugin.getLocales().getRawLocale(command + "_command_description")
|
||||
.orElse("A HuskSync command");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class ItemsCommand extends Command implements TabProvider {
|
||||
|
||||
protected ItemsCommand(@NotNull HuskSync plugin, @NotNull List<String> aliases) {
|
||||
super(aliases.get(0), aliases.subList(1, aliases.size()), "<player> [version_uuid]", plugin);
|
||||
setOperatorCommand(true);
|
||||
addAdditionalPermissions(Map.of("edit", true));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
if (!(executor instanceof OnlineUser player)) {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the user to view the items for
|
||||
final Optional<User> optionalUser = parseStringArg(args, 0)
|
||||
.flatMap(name -> plugin.getDatabase().getUserByName(name));
|
||||
if (optionalUser.isEmpty()) {
|
||||
plugin.getLocales().getLocale(
|
||||
args.length >= 1 ? "error_invalid_player" : "error_invalid_syntax", getUsage()
|
||||
).ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the user data
|
||||
final User user = optionalUser.get();
|
||||
parseUUIDArg(args, 1).ifPresentOrElse(
|
||||
version -> this.showSnapshotItems(player, user, version),
|
||||
() -> this.showLatestItems(player, user)
|
||||
);
|
||||
}
|
||||
|
||||
// View (and edit) the latest user data
|
||||
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
|
||||
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
|
||||
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
||||
.ifPresentOrElse(
|
||||
snapshot -> this.showItems(
|
||||
viewer, snapshot.unpack(plugin), user,
|
||||
viewer.hasPermission(getPermission("edit"))
|
||||
),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage)
|
||||
));
|
||||
}
|
||||
|
||||
// View a specific version of the user data
|
||||
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
|
||||
plugin.getDatabase().getSnapshot(user, version)
|
||||
.ifPresentOrElse(
|
||||
snapshot -> this.showItems(
|
||||
viewer, snapshot.unpack(plugin), user, false
|
||||
),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(viewer::sendMessage)
|
||||
);
|
||||
}
|
||||
|
||||
// Show a GUI menu with the correct item data from the snapshot
|
||||
protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
return switch (args.length) {
|
||||
case 0, 1 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class Node implements Executable {
|
||||
|
||||
protected static final String PERMISSION_PREFIX = "husksync.command";
|
||||
|
||||
protected final HuskSync plugin;
|
||||
private final String name;
|
||||
private final List<String> aliases;
|
||||
private boolean operatorCommand = false;
|
||||
|
||||
protected Node(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
|
||||
if (name.isBlank()) {
|
||||
throw new IllegalArgumentException("Command name cannot be blank");
|
||||
}
|
||||
this.name = name;
|
||||
this.aliases = aliases;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getAliases() {
|
||||
return aliases;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getPermission(@NotNull String... child) {
|
||||
final StringJoiner joiner = new StringJoiner(".")
|
||||
.add(PERMISSION_PREFIX)
|
||||
.add(getName());
|
||||
for (final String node : child) {
|
||||
joiner.add(node);
|
||||
}
|
||||
return joiner.toString().trim();
|
||||
}
|
||||
|
||||
public boolean isOperatorCommand() {
|
||||
return operatorCommand;
|
||||
}
|
||||
|
||||
public void setOperatorCommand(boolean operatorCommand) {
|
||||
this.operatorCommand = operatorCommand;
|
||||
}
|
||||
|
||||
protected Optional<String> parseStringArg(@NotNull String[] args, int index) {
|
||||
if (args.length > index) {
|
||||
return Optional.of(args[index]);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
protected Optional<Integer> parseIntArg(@NotNull String[] args, int index) {
|
||||
return parseStringArg(args, index).flatMap(arg -> {
|
||||
try {
|
||||
return Optional.of(Integer.parseInt(arg));
|
||||
} catch (NumberFormatException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected Optional<UUID> parseUUIDArg(@NotNull String[] args, int index) {
|
||||
return parseStringArg(args, index).flatMap(arg -> {
|
||||
try {
|
||||
return Optional.of(UUID.fromString(arg));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Static plugin permission nodes required to execute commands
|
||||
*/
|
||||
public enum Permission {
|
||||
|
||||
/*
|
||||
* /husksync command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /husksync} command (subcommand permissions required)
|
||||
*/
|
||||
COMMAND_HUSKSYNC("husksync.command.husksync", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user view plugin info {@code /husksync info}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_ABOUT("husksync.command.husksync.info", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user reload the plugin {@code /husksync reload}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_RELOAD("husksync.command.husksync.reload", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user view the plugin version and check for updates {@code /husksync update}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_UPDATE("husksync.command.husksync.update", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /userdata command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user view user data {@code /userdata view/list (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA("husksync.command.userdata", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user restore and delete user data {@code /userdata restore/delete (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA_MANAGE("husksync.command.userdata.manage", DefaultAccess.OPERATORS),
|
||||
|
||||
/**
|
||||
* Lets the user dump user data to a file or the web {@code /userdata dump (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA_DUMP("husksync.command.userdata.dump", DefaultAccess.NOBODY),
|
||||
|
||||
/*
|
||||
* /inventory command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /inventory (player)} command and view offline players' inventories
|
||||
*/
|
||||
COMMAND_INVENTORY("husksync.command.inventory", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user edit the contents of offline players' inventories
|
||||
*/
|
||||
COMMAND_INVENTORY_EDIT("husksync.command.inventory.edit", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /enderchest command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /enderchest (player)} command and view offline players' ender chests
|
||||
*/
|
||||
COMMAND_ENDER_CHEST("husksync.command.enderchest", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user edit the contents of offline players' ender chests
|
||||
*/
|
||||
COMMAND_ENDER_CHEST_EDIT("husksync.command.enderchest.edit", DefaultAccess.OPERATORS);
|
||||
|
||||
|
||||
public final String node;
|
||||
public final DefaultAccess defaultAccess;
|
||||
|
||||
Permission(@NotNull String node, @NotNull DefaultAccess defaultAccess) {
|
||||
this.node = node;
|
||||
this.defaultAccess = defaultAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies who gets what permissions by default
|
||||
*/
|
||||
public enum DefaultAccess {
|
||||
/**
|
||||
* Everyone gets this permission node by default
|
||||
*/
|
||||
EVERYONE,
|
||||
/**
|
||||
* Nobody gets this permission node by default
|
||||
*/
|
||||
NOBODY,
|
||||
/**
|
||||
* Server operators ({@code /op}) get this permission node by default
|
||||
*/
|
||||
OPERATORS
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface providing tab completions for a command
|
||||
*/
|
||||
public interface TabCompletable {
|
||||
|
||||
/**
|
||||
* What should be returned when the player or console attempts to TAB-complete a command
|
||||
*
|
||||
* @param args Current command arguments
|
||||
* @return List of String arguments to offer TAB suggestions
|
||||
*/
|
||||
List<String> onTabComplete(@NotNull String[] args);
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TabProvider {
|
||||
|
||||
@Nullable
|
||||
List<String> suggest(@NotNull CommandUser user, @NotNull String[] args);
|
||||
|
||||
@NotNull
|
||||
default List<String> getSuggestions(@NotNull CommandUser user, @NotNull String[] args) {
|
||||
List<String> suggestions = suggest(user, args);
|
||||
if (suggestions == null) {
|
||||
suggestions = List.of();
|
||||
}
|
||||
return filter(suggestions, args);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default List<String> filter(@NotNull List<String> suggestions, @NotNull String[] args) {
|
||||
return suggestions.stream()
|
||||
.filter(suggestion -> args.length == 0 || suggestion.toLowerCase()
|
||||
.startsWith(args[args.length - 1].toLowerCase().trim()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A mapped piece of advancement data
|
||||
*/
|
||||
public class AdvancementData {
|
||||
|
||||
/**
|
||||
* The advancement namespaced key
|
||||
*/
|
||||
@SerializedName("key")
|
||||
public String key;
|
||||
|
||||
/**
|
||||
* A map of completed advancement criteria to when it was completed
|
||||
*/
|
||||
@SerializedName("completed_criteria")
|
||||
public Map<String, Date> completedCriteria;
|
||||
|
||||
public AdvancementData(@NotNull String key, @NotNull Map<String, Date> awardedCriteria) {
|
||||
this.key = key;
|
||||
this.completedCriteria = awardedCriteria;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected AdvancementData() {
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.xerial.snappy.Snappy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CompressedDataAdapter extends JsonDataAdapter {
|
||||
|
||||
@Override
|
||||
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
|
||||
try {
|
||||
return Snappy.compress(super.toBytes(data));
|
||||
} catch (IOException e) {
|
||||
throw new DataAdaptionException("Failed to compress data", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
|
||||
try {
|
||||
return super.fromBytes(Snappy.uncompress(data));
|
||||
} catch (IOException e) {
|
||||
throw new DataAdaptionException("Failed to decompress data", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,368 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A piece of data, held by a {@link DataHolder}
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public interface Data {
|
||||
|
||||
/**
|
||||
* Apply (set) this data container to the given {@link OnlineUser}
|
||||
*
|
||||
* @param user the user to apply this element to
|
||||
* @param plugin the plugin instance
|
||||
*/
|
||||
void apply(@NotNull UserDataHolder user, @NotNull HuskSync plugin);
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Inventories</li>
|
||||
* <li>Ender Chests</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Items extends Data {
|
||||
|
||||
@NotNull
|
||||
Stack[] getStack();
|
||||
|
||||
default int getSlotCount() {
|
||||
return getStack().length;
|
||||
}
|
||||
|
||||
record Stack(@NotNull String material, int amount, @Nullable String name,
|
||||
@Nullable List<String> lore, @NotNull List<String> enchantments) {
|
||||
|
||||
}
|
||||
|
||||
default boolean isEmpty() {
|
||||
return Arrays.stream(getStack()).allMatch(Objects::isNull) || getStack().length == 0;
|
||||
}
|
||||
|
||||
void clear();
|
||||
|
||||
void setContents(@NotNull Items contents);
|
||||
|
||||
/**
|
||||
* A data container holding data for inventories and selected hotbar slot
|
||||
*/
|
||||
interface Inventory extends Items {
|
||||
|
||||
int getHeldItemSlot();
|
||||
|
||||
void setHeldItemSlot(int heldItemSlot) throws IllegalArgumentException;
|
||||
|
||||
default Optional<Stack> getHelmet() {
|
||||
return Optional.ofNullable(getStack()[39]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getChestplate() {
|
||||
return Optional.ofNullable(getStack()[38]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getLeggings() {
|
||||
return Optional.ofNullable(getStack()[37]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getBoots() {
|
||||
return Optional.ofNullable(getStack()[36]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getOffHand() {
|
||||
return Optional.ofNullable(getStack()[40]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for ender chests
|
||||
*/
|
||||
interface EnderChest extends Items {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for potion effects
|
||||
*/
|
||||
interface PotionEffects extends Data {
|
||||
|
||||
@NotNull
|
||||
List<Effect> getActiveEffects();
|
||||
|
||||
/**
|
||||
* Represents a potion effect
|
||||
*
|
||||
* @param type the type of potion effect
|
||||
* @param amplifier the amplifier of the potion effect
|
||||
* @param duration the duration of the potion effect
|
||||
* @param isAmbient whether the potion effect is ambient
|
||||
* @param showParticles whether the potion effect shows particles
|
||||
* @param hasIcon whether the potion effect displays a HUD icon
|
||||
*/
|
||||
record Effect(@SerializedName("type") @NotNull String type,
|
||||
@SerializedName("amplifier") int amplifier,
|
||||
@SerializedName("duration") int duration,
|
||||
@SerializedName("is_ambient") boolean isAmbient,
|
||||
@SerializedName("show_particles") boolean showParticles,
|
||||
@SerializedName("has_icon") boolean hasIcon) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for advancements
|
||||
*/
|
||||
interface Advancements extends Data {
|
||||
|
||||
@NotNull
|
||||
List<Advancement> getCompleted();
|
||||
|
||||
@NotNull
|
||||
default List<Advancement> getCompletedExcludingRecipes() {
|
||||
return getCompleted().stream()
|
||||
.filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe"))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
void setCompleted(@NotNull List<Advancement> completed);
|
||||
|
||||
class Advancement {
|
||||
@SerializedName("key")
|
||||
private String key;
|
||||
|
||||
@SerializedName("completed_criteria")
|
||||
private Map<String, Long> completedCriteria;
|
||||
|
||||
private Advancement(@NotNull String key, @NotNull Map<String, Date> completedCriteria) {
|
||||
this.key = key;
|
||||
this.completedCriteria = adaptDateMap(completedCriteria);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Advancement() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Advancement adapt(@NotNull String key, @NotNull Map<String, Date> completedCriteria) {
|
||||
return new Advancement(key, completedCriteria);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) {
|
||||
return dateMap.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) {
|
||||
return dateMap.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(@NotNull String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public Map<String, Date> getCompletedCriteria() {
|
||||
return adaptLongMap(completedCriteria);
|
||||
}
|
||||
|
||||
public void setCompletedCriteria(Map<String, Date> completedCriteria) {
|
||||
this.completedCriteria = adaptDateMap(completedCriteria);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for the player's location
|
||||
*/
|
||||
interface Location extends Data {
|
||||
double getX();
|
||||
|
||||
void setX(double x);
|
||||
|
||||
double getY();
|
||||
|
||||
void setY(double y);
|
||||
|
||||
double getZ();
|
||||
|
||||
void setZ(double z);
|
||||
|
||||
float getYaw();
|
||||
|
||||
void setYaw(float yaw);
|
||||
|
||||
float getPitch();
|
||||
|
||||
void setPitch(float pitch);
|
||||
|
||||
@NotNull
|
||||
World getWorld();
|
||||
|
||||
void setWorld(@NotNull World world);
|
||||
|
||||
record World(
|
||||
@SerializedName("name") @NotNull String name,
|
||||
@SerializedName("uuid") @NotNull UUID uuid,
|
||||
@SerializedName("environment") @NotNull String environment
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for statistics
|
||||
*/
|
||||
interface Statistics extends Data {
|
||||
@NotNull
|
||||
Map<String, Integer> getGenericStatistics();
|
||||
|
||||
@NotNull
|
||||
Map<String, Map<String, Integer>> getBlockStatistics();
|
||||
|
||||
@NotNull
|
||||
Map<String, Map<String, Integer>> getItemStatistics();
|
||||
|
||||
@NotNull
|
||||
Map<String, Map<String, Integer>> getEntityStatistics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for persistent data containers
|
||||
*/
|
||||
interface PersistentData extends Data {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Health</li>
|
||||
* <li>Max Health</li>
|
||||
* <li>Health Scale</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Health extends Data {
|
||||
double getHealth();
|
||||
|
||||
void setHealth(double health);
|
||||
|
||||
double getMaxHealth();
|
||||
|
||||
void setMaxHealth(double maxHealth);
|
||||
|
||||
double getHealthScale();
|
||||
|
||||
void setHealthScale(double healthScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
*
|
||||
* <li>Food Level</li>
|
||||
* <li>Saturation</li>
|
||||
* <li>Exhaustion</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Hunger extends Data {
|
||||
|
||||
int getFoodLevel();
|
||||
|
||||
void setFoodLevel(int foodLevel);
|
||||
|
||||
float getSaturation();
|
||||
|
||||
void setSaturation(float saturation);
|
||||
|
||||
float getExhaustion();
|
||||
|
||||
void setExhaustion(float exhaustion);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Total experience</li>
|
||||
* <li>Experience level</li>
|
||||
* <li>Experience progress</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Experience extends Data {
|
||||
|
||||
int getTotalExperience();
|
||||
|
||||
void setTotalExperience(int totalExperience);
|
||||
|
||||
int getExpLevel();
|
||||
|
||||
void setExpLevel(int expLevel);
|
||||
|
||||
float getExpProgress();
|
||||
|
||||
void setExpProgress(float expProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Game mode</li>
|
||||
* <li>Allow flight</li>
|
||||
* <li>Is flying</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface GameMode extends Data {
|
||||
|
||||
@NotNull
|
||||
String getGameMode();
|
||||
|
||||
void setGameMode(@NotNull String gameMode);
|
||||
|
||||
boolean getAllowFlight();
|
||||
|
||||
void setAllowFlight(boolean allowFlight);
|
||||
|
||||
boolean getIsFlying();
|
||||
|
||||
void setIsFlying(boolean isFlying);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* An adapter that adapts {@link UserData} to and from a portable byte array.
|
||||
*/
|
||||
public interface DataAdapter {
|
||||
|
||||
/**
|
||||
* Converts {@link UserData} to a byte array
|
||||
*
|
||||
* @param data The {@link UserData} to adapt
|
||||
* @return The byte array.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
byte[] toBytes(@NotNull UserData data) throws DataAdaptionException;
|
||||
|
||||
/**
|
||||
* Serializes {@link UserData} to a JSON string.
|
||||
*
|
||||
* @param data The {@link UserData} to serialize
|
||||
* @param pretty Whether to pretty print the JSON.
|
||||
* @return The output json string.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to {@link UserData}.
|
||||
*
|
||||
* @param data The byte array to adapt.
|
||||
* @return The {@link UserData}.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation, such as if the byte array is invalid.
|
||||
*/
|
||||
@NotNull
|
||||
UserData fromBytes(final byte[] data) throws DataAdaptionException;
|
||||
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface DataHolder {
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getData();
|
||||
|
||||
default Optional<? extends Data> getData(@NotNull Identifier identifier) {
|
||||
return Optional.ofNullable(getData().get(identifier));
|
||||
}
|
||||
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
getData().put(identifier, data);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Items.Inventory> getInventory() {
|
||||
return getData(Identifier.INVENTORY).map(Data.Items.Inventory.class::cast);
|
||||
}
|
||||
|
||||
default void setInventory(@NotNull Data.Items.Inventory inventory) {
|
||||
setData(Identifier.INVENTORY, inventory);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Items.EnderChest> getEnderChest() {
|
||||
return getData(Identifier.ENDER_CHEST).map(Data.Items.EnderChest.class::cast);
|
||||
}
|
||||
|
||||
default void setEnderChest(@NotNull Data.Items.EnderChest enderChest) {
|
||||
setData(Identifier.ENDER_CHEST, enderChest);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.PotionEffects> getPotionEffects() {
|
||||
return getData(Identifier.POTION_EFFECTS).map(Data.PotionEffects.class::cast);
|
||||
}
|
||||
|
||||
default void setPotionEffects(@NotNull Data.PotionEffects potionEffects) {
|
||||
setData(Identifier.POTION_EFFECTS, potionEffects);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Advancements> getAdvancements() {
|
||||
return getData(Identifier.ADVANCEMENTS).map(Data.Advancements.class::cast);
|
||||
}
|
||||
|
||||
default void setAdvancements(@NotNull Data.Advancements advancements) {
|
||||
setData(Identifier.ADVANCEMENTS, advancements);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Location> getLocation() {
|
||||
return Optional.ofNullable((Data.Location) getData().get(Identifier.LOCATION));
|
||||
}
|
||||
|
||||
default void setLocation(@NotNull Data.Location location) {
|
||||
getData().put(Identifier.LOCATION, location);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Statistics> getStatistics() {
|
||||
return Optional.ofNullable((Data.Statistics) getData().get(Identifier.STATISTICS));
|
||||
}
|
||||
|
||||
default void setStatistics(@NotNull Data.Statistics statistics) {
|
||||
getData().put(Identifier.STATISTICS, statistics);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Health> getHealth() {
|
||||
return Optional.ofNullable((Data.Health) getData().get(Identifier.HEALTH));
|
||||
}
|
||||
|
||||
default void setHealth(@NotNull Data.Health health) {
|
||||
getData().put(Identifier.HEALTH, health);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Hunger> getHunger() {
|
||||
return Optional.ofNullable((Data.Hunger) getData().get(Identifier.HUNGER));
|
||||
}
|
||||
|
||||
default void setHunger(@NotNull Data.Hunger hunger) {
|
||||
getData().put(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
return Optional.ofNullable((Data.Experience) getData().get(Identifier.EXPERIENCE));
|
||||
}
|
||||
|
||||
default void setExperience(@NotNull Data.Experience experience) {
|
||||
getData().put(Identifier.EXPERIENCE, experience);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.GameMode> getGameMode() {
|
||||
return Optional.ofNullable((Data.GameMode) getData().get(Identifier.GAME_MODE));
|
||||
}
|
||||
|
||||
default void setGameMode(@NotNull Data.GameMode gameMode) {
|
||||
getData().put(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.PersistentData> getPersistentData() {
|
||||
return Optional.ofNullable((Data.PersistentData) getData().get(Identifier.PERSISTENT_DATA));
|
||||
}
|
||||
|
||||
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
|
||||
getData().put(Identifier.PERSISTENT_DATA, persistentData);
|
||||
}
|
||||
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.api.BaseHuskSyncAPI;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Identifies the cause of a player data save.
|
||||
*
|
||||
* @implNote This enum is saved in the database.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum DataSaveCause {
|
||||
|
||||
/**
|
||||
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
DISCONNECT,
|
||||
/**
|
||||
* Indicates data saved when the world saved
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
/**
|
||||
* Indicates data saved when the user died
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
DEATH,
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
SERVER_SHUTDOWN,
|
||||
/**
|
||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
INVENTORY_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
ENDERCHEST_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by restoring it from a previous version
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
BACKUP_RESTORE,
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @see BaseHuskSyncAPI#saveUserData(OnlineUser)
|
||||
* @see BaseHuskSyncAPI#setUserData(User, UserData)
|
||||
* @since 2.0
|
||||
*/
|
||||
API,
|
||||
/**
|
||||
* Indicates data was saved from being imported from MySQLPlayerDataBridge
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
MPDB_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v1.x)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
LEGACY_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved by an unknown cause.
|
||||
* </p>
|
||||
* This should not be used and is only used for error handling purposes.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
UNKNOWN;
|
||||
|
||||
/**
|
||||
* Returns a {@link DataSaveCause} by name.
|
||||
*
|
||||
* @return the {@link DataSaveCause} or {@link #UNKNOWN} if the name is not valid.
|
||||
*/
|
||||
@NotNull
|
||||
public static DataSaveCause getCauseByName(@NotNull String name) {
|
||||
for (DataSaveCause cause : values()) {
|
||||
if (cause.name().equalsIgnoreCase(name)) {
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return Locales.truncate(name().toLowerCase(Locale.ENGLISH), 10);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,819 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A snapshot of a {@link DataHolder} at a given time.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public class DataSnapshot {
|
||||
|
||||
/*
|
||||
* Current version of the snapshot data format.
|
||||
* HuskSync v3.0 uses v4; HuskSync v2.0 uses v3. HuskSync v1.0 uses v1 or v2
|
||||
*/
|
||||
protected static final int CURRENT_FORMAT_VERSION = 4;
|
||||
|
||||
@SerializedName("id")
|
||||
protected UUID id;
|
||||
|
||||
@SerializedName("pinned")
|
||||
protected boolean pinned;
|
||||
|
||||
@SerializedName("timestamp")
|
||||
protected OffsetDateTime timestamp;
|
||||
|
||||
@SerializedName("save_cause")
|
||||
protected SaveCause saveCause;
|
||||
|
||||
@SerializedName("minecraft_version")
|
||||
protected String minecraftVersion;
|
||||
|
||||
@SerializedName("platform_type")
|
||||
protected String platformType;
|
||||
|
||||
@SerializedName("format_version")
|
||||
protected int formatVersion;
|
||||
|
||||
@SerializedName("data")
|
||||
protected Map<String, String> data;
|
||||
|
||||
private DataSnapshot(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
this.id = id;
|
||||
this.pinned = pinned;
|
||||
this.timestamp = timestamp;
|
||||
this.saveCause = saveCause;
|
||||
this.data = data;
|
||||
this.minecraftVersion = minecraftVersion.toStringWithoutMetadata();
|
||||
this.platformType = platformType;
|
||||
this.formatVersion = formatVersion;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private DataSnapshot() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Builder builder(@NotNull HuskSync plugin) {
|
||||
return new Builder(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data) throws IllegalStateException {
|
||||
final DataSnapshot.Packed snapshot = plugin.getDataAdapter().fromBytes(data, DataSnapshot.Packed.class);
|
||||
if (snapshot.getMinecraftVersion().compareTo(plugin.getMinecraftVersion()) > 0) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the Minecraft version of " +
|
||||
"their user data (%s) is newer than the server's Minecraft version (%s)." +
|
||||
"Please ensure each server is running the same version of Minecraft.",
|
||||
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion()));
|
||||
}
|
||||
if (snapshot.getFormatVersion() > CURRENT_FORMAT_VERSION) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the format version of " +
|
||||
"their user data (%s) is newer than the current format version (%s). " +
|
||||
"Please ensure each server is running the latest version of HuskSync.",
|
||||
snapshot.getFormatVersion(), CURRENT_FORMAT_VERSION));
|
||||
}
|
||||
if (snapshot.getFormatVersion() < CURRENT_FORMAT_VERSION) {
|
||||
if (plugin.getLegacyConverter().isPresent()) {
|
||||
return plugin.getLegacyConverter().get().convert(data);
|
||||
}
|
||||
throw new IllegalStateException(String.format(
|
||||
"No legacy converter to convert format version: %s", snapshot.getFormatVersion()
|
||||
));
|
||||
}
|
||||
if (!snapshot.getPlatformType().equalsIgnoreCase(plugin.getPlatformType())) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the platform type of " +
|
||||
"their user data (%s) is different to the server platform type (%s). " +
|
||||
"Please ensure each server is running the same platform type.",
|
||||
snapshot.getPlatformType(), plugin.getPlatformType()));
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of the snapshot
|
||||
*
|
||||
* @return The snapshot ID
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short display ID of the snapshot
|
||||
*
|
||||
* @return The short display ID
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public String getShortId() {
|
||||
return id.toString().substring(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the snapshot is pinned
|
||||
*
|
||||
* @return Whether the snapshot is pinned
|
||||
* @since 3.0
|
||||
*/
|
||||
public boolean isPinned() {
|
||||
return pinned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the snapshot is pinned
|
||||
*
|
||||
* @param pinned Whether the snapshot is pinned
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setPinned(boolean pinned) {
|
||||
this.pinned = pinned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get why the snapshot was created
|
||||
*
|
||||
* @return The {@link SaveCause data save cause} of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public SaveCause getSaveCause() {
|
||||
return saveCause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set why the snapshot was created
|
||||
*
|
||||
* @param saveCause The {@link SaveCause data save cause} of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setSaveCause(SaveCause saveCause) {
|
||||
this.saveCause = saveCause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get when the snapshot was created
|
||||
*
|
||||
* @return The {@link OffsetDateTime timestamp} of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public OffsetDateTime getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Minecraft version of the server when the Snapshot was created
|
||||
*
|
||||
* @return The Minecraft version of the server when the Snapshot was created
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromString(minecraftVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the platform type of the server when the Snapshot was created
|
||||
*
|
||||
* @return The platform type of the server when the Snapshot was created (e.g. {@code "bukkit"})
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public String getPlatformType() {
|
||||
return platformType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the format version of the snapshot (indicating the version of HuskSync that created it)
|
||||
* <ul>
|
||||
* <li>1: HuskSync v1.0+</li>
|
||||
* <li>2: HuskSync v1.5+</li>
|
||||
* <li>3: HuskSync v2.0+</li>
|
||||
* <li>4: HuskSync v3.0+</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return The format version of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
public int getFormatVersion() {
|
||||
return formatVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* A packed {@link DataSnapshot} that has not been deserialized.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public static class Packed extends DataSnapshot implements Adaptable {
|
||||
|
||||
protected Packed(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, data, minecraftVersion, platformType, formatVersion);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Packed() {
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void edit(@NotNull HuskSync plugin, @NotNull Consumer<Unpacked> editor) {
|
||||
final Unpacked data = unpack(plugin);
|
||||
editor.accept(data);
|
||||
this.pinned = data.isPinned();
|
||||
this.saveCause = data.getSaveCause();
|
||||
this.data = data.serializeData(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of this snapshot at the current system timestamp with a new ID
|
||||
*
|
||||
* @return The copied snapshot (with a new ID, with a timestamp of the current system time)
|
||||
*/
|
||||
@NotNull
|
||||
public Packed copy() {
|
||||
return new Packed(
|
||||
UUID.randomUUID(), pinned, OffsetDateTime.now(), saveCause, data,
|
||||
getMinecraftVersion(), platformType, formatVersion
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public byte[] asBytes(@NotNull HuskSync plugin) throws DataAdapter.AdaptionException {
|
||||
return plugin.getDataAdapter().toBytes(this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public String asJson(@NotNull HuskSync plugin) throws DataAdapter.AdaptionException {
|
||||
return plugin.getDataAdapter().toJson(this);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public int getFileSize(@NotNull HuskSync plugin) {
|
||||
return asBytes(plugin).length;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked unpack(@NotNull HuskSync plugin) {
|
||||
return new Unpacked(
|
||||
id, pinned, timestamp, saveCause, data,
|
||||
getMinecraftVersion(), platformType, formatVersion, plugin
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An unpacked {@link DataSnapshot}.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public static class Unpacked extends DataSnapshot implements DataHolder {
|
||||
|
||||
@Expose(serialize = false, deserialize = false)
|
||||
private final Map<Identifier, Data> deserialized;
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion,
|
||||
@NotNull HuskSync plugin) {
|
||||
super(id, pinned, timestamp, saveCause, data, minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = deserializeData(plugin);
|
||||
}
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<Identifier, Data> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, Map.of(), minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = data;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
return data.entrySet().stream()
|
||||
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry(
|
||||
id, plugin.getSerializers().get(id).deserialize(entry.getValue())
|
||||
)).orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private Map<String, String> serializeData(@NotNull HuskSync plugin) {
|
||||
return deserialized.entrySet().stream()
|
||||
.map((entry) -> Map.entry(entry.getKey().toString(),
|
||||
Objects.requireNonNull(
|
||||
plugin.getSerializers().get(entry.getKey()),
|
||||
String.format("No serializer found for %s", entry.getKey())
|
||||
).serialize(entry.getValue())))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data the snapshot is holding
|
||||
*
|
||||
* @return The data map
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Map<Identifier, Data> getData() {
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack the {@link DataSnapshot} into a {@link DataSnapshot.Packed packed} snapshot
|
||||
*
|
||||
* @param plugin The HuskSync plugin instance
|
||||
* @return The packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public DataSnapshot.Packed pack(@NotNull HuskSync plugin) {
|
||||
return new DataSnapshot.Packed(
|
||||
id, pinned, timestamp, saveCause, serializeData(plugin),
|
||||
getMinecraftVersion(), platformType, formatVersion
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link DataSnapshot}s.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static class Builder {
|
||||
|
||||
private final HuskSync plugin;
|
||||
private SaveCause saveCause;
|
||||
private boolean pinned;
|
||||
private OffsetDateTime timestamp;
|
||||
private final Map<Identifier, Data> data;
|
||||
|
||||
private Builder(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.pinned = false;
|
||||
this.data = new HashMap<>();
|
||||
this.timestamp = OffsetDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cause of the data save
|
||||
*
|
||||
* @param saveCause The cause of the data save
|
||||
* @return The builder
|
||||
* @apiNote If the {@link SaveCause data save cause} specified is configured to auto-pin, then the value of
|
||||
* {@link #pinned(boolean)} will be ignored
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder saveCause(@NotNull SaveCause saveCause) {
|
||||
this.saveCause = saveCause;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the data should be pinned
|
||||
*
|
||||
* @param pinned Whether the data should be pinned
|
||||
* @return The builder
|
||||
* @apiNote If the {@link SaveCause data save cause} specified is configured to auto-pin, this will be ignored
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder pinned(boolean pinned) {
|
||||
this.pinned = pinned;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the timestamp of the snapshot.
|
||||
* By default, this is the current server time.
|
||||
* The timestamp passed to this method cannot be in the future.
|
||||
* <p>
|
||||
* Note that this will affect the rotation of data snapshots in the database if unpinned,
|
||||
* as well as the order snapshots appear in the list.
|
||||
*
|
||||
* @param timestamp The timestamp
|
||||
* @return The builder
|
||||
* @throws IllegalArgumentException if the timestamp is in the future
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder timestamp(@NotNull OffsetDateTime timestamp) {
|
||||
if (timestamp.isAfter(OffsetDateTime.now())) {
|
||||
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future");
|
||||
}
|
||||
this.timestamp = timestamp;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data for a given identifier
|
||||
*
|
||||
* @param identifier The identifier
|
||||
* @param data The data
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder data(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
this.data.put(identifier, data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a map of data to the snapshot
|
||||
*
|
||||
* @param data The data
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder data(@NotNull Map<Identifier, Data> data) {
|
||||
this.data.putAll(data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory contents of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.INVENTORY, inventory)}
|
||||
* </p>
|
||||
*
|
||||
* @param inventory The inventory contents
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder inventory(@NotNull Data.Items.Inventory inventory) {
|
||||
return data(Identifier.INVENTORY, inventory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Ender Chest contents of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.ENDER_CHEST, inventory)}
|
||||
* </p>
|
||||
*
|
||||
* @param enderChest The Ender Chest contents
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder enderChest(@NotNull Data.Items.EnderChest enderChest) {
|
||||
return data(Identifier.ENDER_CHEST, enderChest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the potion effects of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.POTION_EFFECTS, potionEffects)}
|
||||
* </p>
|
||||
*
|
||||
* @param potionEffects The potion effects
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder potionEffects(@NotNull Data.PotionEffects potionEffects) {
|
||||
return data(Identifier.POTION_EFFECTS, potionEffects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the advancements of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.ADVANCEMENTS, advancements)}
|
||||
* </p>
|
||||
*
|
||||
* @param advancements The advancements
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder advancements(@NotNull Data.Advancements advancements) {
|
||||
return data(Identifier.ADVANCEMENTS, advancements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the location of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.LOCATION, location)}
|
||||
* </p>
|
||||
*
|
||||
* @param location The location
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder location(@NotNull Data.Location location) {
|
||||
return data(Identifier.LOCATION, location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the statistics of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.STATISTICS, statistics)}
|
||||
* </p>
|
||||
*
|
||||
* @param statistics The statistics
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder statistics(@NotNull Data.Statistics statistics) {
|
||||
return data(Identifier.STATISTICS, statistics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the health of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.HEALTH, health)}
|
||||
* </p>
|
||||
*
|
||||
* @param health The health
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder health(@NotNull Data.Health health) {
|
||||
return data(Identifier.HEALTH, health);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the hunger of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.HUNGER, hunger)}
|
||||
* </p>
|
||||
*
|
||||
* @param hunger The hunger
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder hunger(@NotNull Data.Hunger hunger) {
|
||||
return data(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the experience of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.EXPERIENCE, experience)}
|
||||
* </p>
|
||||
*
|
||||
* @param experience The experience
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder experience(@NotNull Data.Experience experience) {
|
||||
return data(Identifier.EXPERIENCE, experience);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the game mode of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.GAME_MODE, gameMode)}
|
||||
* </p>
|
||||
*
|
||||
* @param gameMode The game mode
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder gameMode(@NotNull Data.GameMode gameMode) {
|
||||
return data(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the persistent data container of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.PERSISTENT_DATA, persistentData)}
|
||||
* </p>
|
||||
*
|
||||
* @param persistentData The persistent data container data
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder persistentData(@NotNull Data.PersistentData persistentData) {
|
||||
return data(Identifier.PERSISTENT_DATA, persistentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the {@link DataSnapshot}
|
||||
*
|
||||
* @return The {@link DataSnapshot.Unpacked snapshot}
|
||||
* @throws IllegalStateException If no save cause is specified
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked build() throws IllegalStateException {
|
||||
if (saveCause == null) {
|
||||
throw new IllegalStateException("Cannot build DataSnapshot without a save cause");
|
||||
}
|
||||
return new Unpacked(
|
||||
UUID.randomUUID(),
|
||||
pinned || plugin.getSettings().doAutoPin(saveCause),
|
||||
timestamp,
|
||||
saveCause,
|
||||
data,
|
||||
plugin.getMinecraftVersion(),
|
||||
plugin.getPlatformType(),
|
||||
DataSnapshot.CURRENT_FORMAT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and pack the {@link DataSnapshot}
|
||||
*
|
||||
* @return The {@link DataSnapshot.Packed snapshot}
|
||||
* @throws IllegalStateException If no save cause is specified
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed buildAndPack() throws IllegalStateException {
|
||||
return build().pack(plugin);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the cause of a player data save.
|
||||
*
|
||||
* @implNote This enum is saved in the database.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum SaveCause {
|
||||
|
||||
/**
|
||||
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
DISCONNECT,
|
||||
/**
|
||||
* Indicates data saved when the world saved
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
/**
|
||||
* Indicates data saved when the user died
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
DEATH,
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
SERVER_SHUTDOWN,
|
||||
/**
|
||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
INVENTORY_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
ENDERCHEST_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by restoring it from a previous version
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
BACKUP_RESTORE,
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
API,
|
||||
/**
|
||||
* Indicates data was saved from being imported from MySQLPlayerDataBridge
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
MPDB_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v1.x -> v2.x)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
LEGACY_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v2.x -> v3.x)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
CONVERTED_FROM_V2;
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return Locales.truncate(name().toLowerCase(Locale.ENGLISH)
|
||||
.replaceAll("_", " "), 18);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the cause of a player having their data updated.
|
||||
*/
|
||||
public enum UpdateCause {
|
||||
/**
|
||||
* Indicates the data was updated by a synchronization process
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
SYNCHRONIZED("synchronization_complete", "synchronization_failed"),
|
||||
/**
|
||||
* Indicates the data was updated by a user joining the server
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
NEW_USER("user_registration_complete", null),
|
||||
/**
|
||||
* Indicates the data was updated by a data update process (management command, API, etc.)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
UPDATED("data_update_complete", "data_update_failed");
|
||||
|
||||
private final String completedLocale;
|
||||
private final String failureLocale;
|
||||
|
||||
UpdateCause(@Nullable String completedLocale, @Nullable String failureLocale) {
|
||||
this.completedLocale = completedLocale;
|
||||
this.failureLocale = failureLocale;
|
||||
}
|
||||
|
||||
public Optional<MineDown> getCompletedLocale(@NotNull HuskSync plugin) {
|
||||
if (completedLocale != null) {
|
||||
return plugin.getLocales().getLocale(completedLocale);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<MineDown> getFailedLocale(@NotNull HuskSync plugin) {
|
||||
if (failureLocale != null) {
|
||||
return plugin.getLocales().getLocale(failureLocale);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.kyori.adventure.key.InvalidKeyException;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.intellij.lang.annotations.Subst;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Identifiers of different types of {@link Data}s
|
||||
*/
|
||||
public class Identifier {
|
||||
|
||||
public static Identifier INVENTORY = huskSync("inventory", true);
|
||||
public static Identifier ENDER_CHEST = huskSync("ender_chest", true);
|
||||
public static Identifier POTION_EFFECTS = huskSync("potion_effects", true);
|
||||
public static Identifier ADVANCEMENTS = huskSync("advancements", true);
|
||||
public static Identifier LOCATION = huskSync("location", false);
|
||||
public static Identifier STATISTICS = huskSync("statistics", true);
|
||||
public static Identifier HEALTH = huskSync("health", true);
|
||||
public static Identifier HUNGER = huskSync("hunger", true);
|
||||
public static Identifier EXPERIENCE = huskSync("experience", true);
|
||||
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
||||
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||
|
||||
private final Key key;
|
||||
private final boolean configDefault;
|
||||
|
||||
private Identifier(@NotNull Key key, boolean configDefault) {
|
||||
this.key = key;
|
||||
this.configDefault = configDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identifier from a {@link Key}
|
||||
*
|
||||
* @param key the key
|
||||
* @return the identifier
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@NotNull Key key) {
|
||||
if (key.namespace().equals("husksync")) {
|
||||
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
|
||||
}
|
||||
return new Identifier(key, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identifier from a namespace and value
|
||||
*
|
||||
* @param plugin the namespace
|
||||
* @param name the value
|
||||
* @return the identifier
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@Subst("plugin") @NotNull String plugin, @Subst("null") @NotNull String name) {
|
||||
return from(Key.key(plugin, name));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||
boolean configDefault) throws InvalidKeyException {
|
||||
return new Identifier(Key.key("husksync", name), configDefault);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
private static Identifier parse(@NotNull String key) throws InvalidKeyException {
|
||||
return huskSync(key, true);
|
||||
}
|
||||
|
||||
public boolean isEnabledByDefault() {
|
||||
return configDefault;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map.Entry<String, Boolean> getConfigEntry() {
|
||||
return Map.entry(getKeyValue(), configDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Get a map of the default config entries for all HuskSync identifiers
|
||||
*
|
||||
* @return a map of all the config entries
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Boolean> getConfigMap() {
|
||||
return Map.ofEntries(Stream.of(
|
||||
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION,
|
||||
STATISTICS, HEALTH, HUNGER, EXPERIENCE, GAME_MODE, PERSISTENT_DATA
|
||||
)
|
||||
.map(Identifier::getConfigEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the namespace of the identifier
|
||||
*
|
||||
* @return the namespace
|
||||
*/
|
||||
@NotNull
|
||||
public String getKeyNamespace() {
|
||||
return key.namespace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the identifier
|
||||
*
|
||||
* @return the value
|
||||
*/
|
||||
@NotNull
|
||||
public String getKeyValue() {
|
||||
return key.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the identifier is a custom (non-HuskSync) identifier
|
||||
*
|
||||
* @return {@code false} if {@link #getKeyNamespace()} returns "husksync"; {@code true} otherwise
|
||||
*/
|
||||
public boolean isCustom() {
|
||||
return !getKeyNamespace().equals("husksync");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier as a string (the key)
|
||||
*
|
||||
* @return the identifier as a string
|
||||
*/
|
||||
@NotNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return key.asString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the given object is an identifier with the same key as this identifier
|
||||
*
|
||||
* @param obj the object to compare
|
||||
* @return {@code true} if the given object is an identifier with the same key as this identifier
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Identifier other) {
|
||||
return key.equals(other.key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Stores information about the contents of a player's inventory or Ender Chest.
|
||||
*/
|
||||
public class ItemData {
|
||||
|
||||
/**
|
||||
* A Base-64 string of platform-serialized items
|
||||
*/
|
||||
@SerializedName("serialized_items")
|
||||
public String serializedItems;
|
||||
|
||||
/**
|
||||
* Get an empty item data object, representing an empty inventory or Ender Chest
|
||||
*
|
||||
* @return an empty item data object
|
||||
*/
|
||||
@NotNull
|
||||
public static ItemData empty() {
|
||||
return new ItemData("");
|
||||
}
|
||||
|
||||
public ItemData(@NotNull final String serializedItems) {
|
||||
this.serializedItems = serializedItems;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected ItemData() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the item data is empty
|
||||
*
|
||||
* @return {@code true} if the item data is empty; {@code false} otherwise
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return serializedItems.isEmpty();
|
||||
}
|
||||
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class JsonDataAdapter implements DataAdapter {
|
||||
|
||||
@Override
|
||||
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
|
||||
return toJson(data, false).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException {
|
||||
return (pretty ? new GsonBuilder().setPrettyPrinting() : new GsonBuilder()).create().toJson(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
|
||||
try {
|
||||
return new GsonBuilder().create().fromJson(new String(data, StandardCharsets.UTF_8), UserData.class);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new DataAdaptionException("Failed to parse JSON data", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Stores information about a player's location
|
||||
*/
|
||||
public class LocationData {
|
||||
|
||||
/**
|
||||
* Name of the world on the server
|
||||
*/
|
||||
@SerializedName("world_name")
|
||||
public String worldName;
|
||||
/**
|
||||
* Unique id of the world
|
||||
*/
|
||||
@SerializedName("world_uuid")
|
||||
public UUID worldUuid;
|
||||
/**
|
||||
* The environment type of the world (one of "NORMAL", "NETHER", "THE_END")
|
||||
*/
|
||||
@SerializedName("world_environment")
|
||||
public String worldEnvironment;
|
||||
|
||||
/**
|
||||
* The x coordinate of the location
|
||||
*/
|
||||
@SerializedName("x")
|
||||
public double x;
|
||||
/**
|
||||
* The y coordinate of the location
|
||||
*/
|
||||
@SerializedName("y")
|
||||
public double y;
|
||||
/**
|
||||
* The z coordinate of the location
|
||||
*/
|
||||
@SerializedName("z")
|
||||
public double z;
|
||||
|
||||
/**
|
||||
* The location's facing yaw angle
|
||||
*/
|
||||
@SerializedName("yaw")
|
||||
public float yaw;
|
||||
/**
|
||||
* The location's facing pitch angle
|
||||
*/
|
||||
@SerializedName("pitch")
|
||||
public float pitch;
|
||||
|
||||
public LocationData(@NotNull String worldName, @NotNull UUID worldUuid,
|
||||
@NotNull String worldEnvironment,
|
||||
double x, double y, double z,
|
||||
float yaw, float pitch) {
|
||||
this.worldName = worldName;
|
||||
this.worldUuid = worldUuid;
|
||||
this.worldEnvironment = worldEnvironment;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected LocationData() {
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Store's a user's persistent data container, holding a map of plugin-set persistent values
|
||||
*/
|
||||
public class PersistentDataContainerData {
|
||||
|
||||
/**
|
||||
* Map of namespaced key strings to a byte array representing the persistent data
|
||||
*/
|
||||
@SerializedName("persistent_data_map")
|
||||
protected Map<String, PersistentDataTag<?>> persistentDataMap;
|
||||
|
||||
public PersistentDataContainerData(@NotNull Map<String, PersistentDataTag<?>> persistentDataMap) {
|
||||
this.persistentDataMap = persistentDataMap;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected PersistentDataContainerData() {
|
||||
}
|
||||
|
||||
public <T> Optional<T> getTagValue(@NotNull String tagName, @NotNull Class<T> tagClass) {
|
||||
if (!persistentDataMap.containsKey(tagName)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// If the tag cannot be cast to the specified class, return an empty optional
|
||||
final boolean canCast = tagClass.isAssignableFrom(persistentDataMap.get(tagName).value.getClass());
|
||||
if (!canCast) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(tagClass.cast(persistentDataMap.get(tagName).value));
|
||||
}
|
||||
|
||||
public Optional<PersistentDataTagType> getTagType(@NotNull String tagType) {
|
||||
if (persistentDataMap.containsKey(tagType)) {
|
||||
return PersistentDataTagType.getDataType(persistentDataMap.get(tagType).type);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Set<String> getTags() {
|
||||
return persistentDataMap.keySet();
|
||||
}
|
||||
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents a persistent data tag set by a plugin.
|
||||
*/
|
||||
public class PersistentDataTag<T> {
|
||||
|
||||
/**
|
||||
* The enumerated primitive data type name value of the tag
|
||||
*/
|
||||
protected String type;
|
||||
|
||||
/**
|
||||
* The value of the tag
|
||||
*/
|
||||
public T value;
|
||||
|
||||
public PersistentDataTag(@NotNull PersistentDataTagType type, @NotNull T value) {
|
||||
this.type = type.name();
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private PersistentDataTag() {
|
||||
}
|
||||
|
||||
public Optional<PersistentDataTagType> getType() {
|
||||
return PersistentDataTagType.getDataType(type);
|
||||
}
|
||||
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Stores information about a player's statistics
|
||||
*/
|
||||
public class StatisticsData {
|
||||
|
||||
/**
|
||||
* Map of generic statistic names to their values
|
||||
*/
|
||||
@SerializedName("untyped_statistics")
|
||||
public Map<String, Integer> untypedStatistics;
|
||||
|
||||
/**
|
||||
* Map of block type statistics to a map of material types to values
|
||||
*/
|
||||
@SerializedName("block_statistics")
|
||||
public Map<String, Map<String, Integer>> blockStatistics;
|
||||
|
||||
/**
|
||||
* Map of item type statistics to a map of material types to values
|
||||
*/
|
||||
@SerializedName("item_statistics")
|
||||
public Map<String, Map<String, Integer>> itemStatistics;
|
||||
|
||||
/**
|
||||
* Map of entity type statistics to a map of entity types to values
|
||||
*/
|
||||
@SerializedName("entity_statistics")
|
||||
public Map<String, Map<String, Integer>> entityStatistics;
|
||||
|
||||
public StatisticsData(@NotNull Map<String, Integer> untypedStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> blockStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> itemStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> entityStatistics) {
|
||||
this.untypedStatistics = untypedStatistics;
|
||||
this.blockStatistics = blockStatistics;
|
||||
this.itemStatistics = itemStatistics;
|
||||
this.entityStatistics = entityStatistics;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected StatisticsData() {
|
||||
}
|
||||
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Stores status information about a player
|
||||
*/
|
||||
public class StatusData {
|
||||
|
||||
/**
|
||||
* The player's health points
|
||||
*/
|
||||
@SerializedName("health")
|
||||
public double health;
|
||||
|
||||
/**
|
||||
* The player's maximum health points
|
||||
*/
|
||||
@SerializedName("max_health")
|
||||
public double maxHealth;
|
||||
|
||||
/**
|
||||
* The player's health scaling factor
|
||||
*/
|
||||
@SerializedName("health_scale")
|
||||
public double healthScale;
|
||||
|
||||
/**
|
||||
* The player's hunger points
|
||||
*/
|
||||
@SerializedName("hunger")
|
||||
public int hunger;
|
||||
|
||||
/**
|
||||
* The player's saturation points
|
||||
*/
|
||||
@SerializedName("saturation")
|
||||
public float saturation;
|
||||
|
||||
/**
|
||||
* The player's saturation exhaustion points
|
||||
*/
|
||||
@SerializedName("saturation_exhaustion")
|
||||
public float saturationExhaustion;
|
||||
|
||||
/**
|
||||
* The player's currently selected item slot
|
||||
*/
|
||||
@SerializedName("selected_item_slot")
|
||||
public int selectedItemSlot;
|
||||
|
||||
/**
|
||||
* The player's total experience points<p>
|
||||
* (not to be confused with <i>experience level</i> - this is the "points" value shown on the death screen)
|
||||
*/
|
||||
@SerializedName("total_experience")
|
||||
public int totalExperience;
|
||||
|
||||
/**
|
||||
* The player's experience level (shown on the exp bar)
|
||||
*/
|
||||
@SerializedName("experience_level")
|
||||
public int expLevel;
|
||||
|
||||
/**
|
||||
* The player's progress to their next experience level
|
||||
*/
|
||||
@SerializedName("experience_progress")
|
||||
public float expProgress;
|
||||
|
||||
/**
|
||||
* The player's game mode string (one of "SURVIVAL", "CREATIVE", "ADVENTURE", "SPECTATOR")
|
||||
*/
|
||||
@SerializedName("game_mode")
|
||||
public String gameMode;
|
||||
|
||||
/**
|
||||
* If the player is currently flying
|
||||
*/
|
||||
@SerializedName("is_flying")
|
||||
public boolean isFlying;
|
||||
|
||||
public StatusData(final double health, final double maxHealth, final double healthScale,
|
||||
final int hunger, final float saturation, final float saturationExhaustion,
|
||||
final int selectedItemSlot, final int totalExperience, final int expLevel,
|
||||
final float expProgress, final String gameMode, final boolean isFlying) {
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedItemSlot = selectedItemSlot;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.isFlying = isFlying;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected StatusData() {
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.config.Settings;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Flags for setting {@link StatusData}, indicating which elements should be synced
|
||||
*
|
||||
* @deprecated Use the more direct {@link Settings#getSynchronizationFeature(Settings.SynchronizationFeature)} instead
|
||||
*/
|
||||
@Deprecated(since = "2.1")
|
||||
public enum StatusDataFlag {
|
||||
|
||||
SET_HEALTH(Settings.SynchronizationFeature.HEALTH),
|
||||
SET_MAX_HEALTH(Settings.SynchronizationFeature.MAX_HEALTH),
|
||||
SET_HUNGER(Settings.SynchronizationFeature.HUNGER),
|
||||
SET_EXPERIENCE(Settings.SynchronizationFeature.EXPERIENCE),
|
||||
SET_GAME_MODE(Settings.SynchronizationFeature.GAME_MODE),
|
||||
SET_FLYING(Settings.SynchronizationFeature.LOCATION),
|
||||
SET_SELECTED_ITEM_SLOT(Settings.SynchronizationFeature.INVENTORIES);
|
||||
|
||||
private final Settings.SynchronizationFeature feature;
|
||||
|
||||
StatusDataFlag(@NotNull Settings.SynchronizationFeature feature) {
|
||||
this.feature = feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all status data flags
|
||||
*
|
||||
* @return all status data flags as a list
|
||||
* @deprecated Use {@link Settings#getSynchronizationFeature(Settings.SynchronizationFeature)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(since = "2.1")
|
||||
@SuppressWarnings("unused")
|
||||
public static List<StatusDataFlag> getAll() {
|
||||
return Arrays.stream(StatusDataFlag.values()).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all status data flags that are enabled for setting as per the {@link Settings}
|
||||
*
|
||||
* @param settings the settings to use for determining which flags are enabled
|
||||
* @return all status data flags that are enabled for setting
|
||||
* @deprecated Use {@link Settings#getSynchronizationFeature(Settings.SynchronizationFeature)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(since = "2.1")
|
||||
public static List<StatusDataFlag> getFromSettings(@NotNull Settings settings) {
|
||||
return Arrays.stream(StatusDataFlag.values()).filter(
|
||||
flag -> settings.getSynchronizationFeature(flag.feature)).toList();
|
||||
}
|
||||
|
||||
}
|
@ -1,376 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Stores data about a user
|
||||
*/
|
||||
public class UserData {
|
||||
|
||||
/**
|
||||
* Indicates the version of the {@link UserData} format being used.
|
||||
* </p>
|
||||
* This value is to be incremented whenever the format changes.
|
||||
*/
|
||||
public static final int CURRENT_FORMAT_VERSION = 3;
|
||||
|
||||
/**
|
||||
* Stores the user's status data, including health, food, etc.
|
||||
*/
|
||||
@SerializedName("status")
|
||||
@Nullable
|
||||
protected StatusData statusData;
|
||||
|
||||
/**
|
||||
* Stores the user's inventory contents
|
||||
*/
|
||||
@SerializedName("inventory")
|
||||
@Nullable
|
||||
protected ItemData inventoryData;
|
||||
|
||||
/**
|
||||
* Stores the user's ender chest contents
|
||||
*/
|
||||
@SerializedName("ender_chest")
|
||||
@Nullable
|
||||
protected ItemData enderChestData;
|
||||
|
||||
/**
|
||||
* Store's the user's potion effects
|
||||
*/
|
||||
@SerializedName("potion_effects")
|
||||
@Nullable
|
||||
protected PotionEffectData potionEffectData;
|
||||
|
||||
/**
|
||||
* Stores the set of this user's advancements
|
||||
*/
|
||||
@SerializedName("advancements")
|
||||
@Nullable
|
||||
protected List<AdvancementData> advancementData;
|
||||
|
||||
/**
|
||||
* Stores the user's set of statistics
|
||||
*/
|
||||
@SerializedName("statistics")
|
||||
@Nullable
|
||||
protected StatisticsData statisticData;
|
||||
|
||||
/**
|
||||
* Store's the user's world location and coordinates
|
||||
*/
|
||||
@SerializedName("location")
|
||||
@Nullable
|
||||
protected LocationData locationData;
|
||||
|
||||
/**
|
||||
* Stores the user's serialized persistent data container, which contains metadata keys applied by other plugins
|
||||
*/
|
||||
@SerializedName("persistent_data_container")
|
||||
@Nullable
|
||||
protected PersistentDataContainerData persistentDataContainerData;
|
||||
|
||||
/**
|
||||
* Stores the version of Minecraft this data was generated in
|
||||
*/
|
||||
@SerializedName("minecraft_version")
|
||||
@NotNull
|
||||
protected String minecraftVersion;
|
||||
|
||||
/**
|
||||
* Stores the version of the data format being used
|
||||
*/
|
||||
@SerializedName("format_version")
|
||||
protected int formatVersion = CURRENT_FORMAT_VERSION;
|
||||
|
||||
/**
|
||||
* Create a new {@link UserData} object with the provided data
|
||||
*
|
||||
* @param statusData the user's status data ({@link StatusData})
|
||||
* @param inventoryData the user's inventory data ({@link ItemData})
|
||||
* @param enderChestData the user's ender chest data ({@link ItemData})
|
||||
* @param potionEffectData the user's potion effect data ({@link PotionEffectData})
|
||||
* @param advancementData the user's advancement data ({@link AdvancementData})
|
||||
* @param statisticData the user's statistic data ({@link StatisticsData})
|
||||
* @param locationData the user's location data ({@link LocationData})
|
||||
* @param persistentDataContainerData the user's persistent data container data ({@link PersistentDataContainerData})
|
||||
* @param minecraftVersion the version of Minecraft this data was generated in (e.g. {@code "1.19.2"})
|
||||
* @deprecated see {@link #builder(String)} or {@link #builder(Version)} to create a {@link UserDataBuilder}, which
|
||||
* you can use to {@link UserDataBuilder#build()} a {@link UserData} instance with
|
||||
*/
|
||||
@Deprecated(since = "2.1")
|
||||
public UserData(@Nullable StatusData statusData, @Nullable ItemData inventoryData,
|
||||
@Nullable ItemData enderChestData, @Nullable PotionEffectData potionEffectData,
|
||||
@Nullable List<AdvancementData> advancementData, @Nullable StatisticsData statisticData,
|
||||
@Nullable LocationData locationData, @Nullable PersistentDataContainerData persistentDataContainerData,
|
||||
@NotNull String minecraftVersion) {
|
||||
this.statusData = statusData;
|
||||
this.inventoryData = inventoryData;
|
||||
this.enderChestData = enderChestData;
|
||||
this.potionEffectData = potionEffectData;
|
||||
this.advancementData = advancementData;
|
||||
this.statisticData = statisticData;
|
||||
this.locationData = locationData;
|
||||
this.persistentDataContainerData = persistentDataContainerData;
|
||||
this.minecraftVersion = minecraftVersion;
|
||||
}
|
||||
|
||||
// Empty constructor to facilitate json serialization
|
||||
@SuppressWarnings("unused")
|
||||
protected UserData() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link StatusData} from this user data
|
||||
*
|
||||
* @return the {@link StatusData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getStatus()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public StatusData getStatusData() {
|
||||
return statusData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link StatusData} from this user data
|
||||
*
|
||||
* @return an optional containing the {@link StatusData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<StatusData> getStatus() {
|
||||
return Optional.ofNullable(statusData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's inventory from this user data
|
||||
*
|
||||
* @return the inventory {@link ItemData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getInventory()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public ItemData getInventoryData() {
|
||||
return inventoryData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's inventory from this user data
|
||||
*
|
||||
* @return an optional containing the inventory {@link ItemData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<ItemData> getInventory() {
|
||||
return Optional.ofNullable(inventoryData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's ender chest from this user data
|
||||
*
|
||||
* @return the ender chest {@link ItemData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getEnderChest()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public ItemData getEnderChestData() {
|
||||
return enderChestData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's ender chest from this user data
|
||||
*
|
||||
* @return an optional containing the ender chest {@link ItemData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<ItemData> getEnderChest() {
|
||||
return Optional.ofNullable(enderChestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link PotionEffectData} representing player status effects from this user data
|
||||
*
|
||||
* @return the {@link PotionEffectData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getPotionEffects()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public PotionEffectData getPotionEffectsData() {
|
||||
return potionEffectData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link PotionEffectData} representing the player's potion effects from this user data
|
||||
*
|
||||
* @return an optional containing {@link PotionEffectData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<PotionEffectData> getPotionEffects() {
|
||||
return Optional.ofNullable(potionEffectData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of {@link AdvancementData} from this user data
|
||||
*
|
||||
* @return the {@link AdvancementData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getAdvancements()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public List<AdvancementData> getAdvancementData() {
|
||||
return advancementData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of {@link AdvancementData} representing the player's advancements from this user data
|
||||
*
|
||||
* @return an optional containing a {@link List} of {@link AdvancementData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<List<AdvancementData>> getAdvancements() {
|
||||
return Optional.ofNullable(advancementData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link StatisticsData} representing player statistics from this user data
|
||||
*
|
||||
* @return the {@link StatisticsData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getStatistics()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public StatisticsData getStatisticsData() {
|
||||
return statisticData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets {@link StatisticsData} representing player statistics from this user data
|
||||
*
|
||||
* @return an optional containing player {@link StatisticsData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<StatisticsData> getStatistics() {
|
||||
return Optional.ofNullable(statisticData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link LocationData} representing the player location from this user data
|
||||
*
|
||||
* @return the inventory {@link LocationData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getLocation()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public LocationData getLocationData() {
|
||||
return locationData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets {@link LocationData} representing the player location from this user data
|
||||
*
|
||||
* @return an optional containing player {@link LocationData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<LocationData> getLocation() {
|
||||
return Optional.ofNullable(locationData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link PersistentDataContainerData} from this user data
|
||||
*
|
||||
* @return the {@link PersistentDataContainerData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getPersistentDataContainer()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public PersistentDataContainerData getPersistentDataContainerData() {
|
||||
return persistentDataContainerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets {@link PersistentDataContainerData} from this user data
|
||||
*
|
||||
* @return an optional containing the player's {@link PersistentDataContainerData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<PersistentDataContainerData> getPersistentDataContainer() {
|
||||
return Optional.ofNullable(persistentDataContainerData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of Minecraft this data was generated in
|
||||
*
|
||||
* @return the version of Minecraft this data was generated in
|
||||
*/
|
||||
@NotNull
|
||||
public String getMinecraftVersion() {
|
||||
return minecraftVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version of the data format being used
|
||||
*
|
||||
* @return the version of the data format being used
|
||||
*/
|
||||
public int getFormatVersion() {
|
||||
return formatVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new {@link UserDataBuilder} for creating {@link UserData}
|
||||
*
|
||||
* @param minecraftVersion the version of Minecraft this data was generated in (e.g. {@code "1.19.2"})
|
||||
* @return a UserData {@link UserDataBuilder} instance
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public static UserDataBuilder builder(@NotNull String minecraftVersion) {
|
||||
return new UserDataBuilder(minecraftVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new {@link UserDataBuilder} for creating {@link UserData}
|
||||
*
|
||||
* @param minecraftVersion a {@link Version} object, representing the Minecraft version this data was generated in
|
||||
* @return a UserData {@link UserDataBuilder} instance
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public static UserDataBuilder builder(@NotNull Version minecraftVersion) {
|
||||
return builder(minecraftVersion.toStringWithoutMetadata());
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A builder utility for creating {@link UserData} instances
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public class UserDataBuilder {
|
||||
|
||||
@NotNull
|
||||
private final UserData userData;
|
||||
|
||||
protected UserDataBuilder(@NotNull String minecraftVersion) {
|
||||
this.userData = new UserData();
|
||||
this.userData.minecraftVersion = minecraftVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link StatusData} to this {@link UserData}
|
||||
*
|
||||
* @param status the {@link StatusData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setStatus(@NotNull StatusData status) {
|
||||
this.userData.statusData = status;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param inventoryData the inventory {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setInventory(@Nullable ItemData inventoryData) {
|
||||
this.userData.inventoryData = inventoryData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ender chest {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param enderChestData the ender chest {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setEnderChest(@Nullable ItemData enderChestData) {
|
||||
this.userData.enderChestData = enderChestData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link List} of {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param potionEffectData the {@link List} of {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setPotionEffects(@Nullable PotionEffectData potionEffectData) {
|
||||
this.userData.potionEffectData = potionEffectData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link List} of {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param advancementData the {@link List} of {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setAdvancements(@Nullable List<AdvancementData> advancementData) {
|
||||
this.userData.advancementData = advancementData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link StatisticsData} to this {@link UserData}
|
||||
*
|
||||
* @param statisticData the {@link StatisticsData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setStatistics(@Nullable StatisticsData statisticData) {
|
||||
this.userData.statisticData = statisticData;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the {@link LocationData} to this {@link UserData}
|
||||
*
|
||||
* @param locationData the {@link LocationData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setLocation(@Nullable LocationData locationData) {
|
||||
this.userData.locationData = locationData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link PersistentDataContainerData} to this {@link UserData}
|
||||
*
|
||||
* @param persistentDataContainerData the {@link PersistentDataContainerData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setPersistentDataContainer(@Nullable PersistentDataContainerData persistentDataContainerData) {
|
||||
this.userData.persistentDataContainerData = persistentDataContainerData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and get the {@link UserData} instance
|
||||
*
|
||||
* @return the {@link UserData} instance
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserData build() {
|
||||
return this.userData;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A holder of data in the form of {@link Data}s, which can be synced
|
||||
*/
|
||||
public interface UserDataHolder extends DataHolder {
|
||||
|
||||
/**
|
||||
* Get the data that is enabled for syncing in the config
|
||||
*
|
||||
* @return the data that is enabled for syncing
|
||||
* @since 3.0
|
||||
*/
|
||||
@Override
|
||||
@NotNull
|
||||
default Map<Identifier, Data> getData() {
|
||||
return getPlugin().getRegisteredDataTypes().stream()
|
||||
.filter(type -> type.isCustom() || getPlugin().getSettings().isSyncFeatureEnabled(type))
|
||||
.map(id -> Map.entry(id, getData(id)))
|
||||
.filter(data -> data.getValue().isPresent())
|
||||
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the data for the given {@link Identifier} to the holder.
|
||||
* <p>
|
||||
* This will be performed synchronously on the main server thread; it will not happen instantly.
|
||||
*
|
||||
* @param identifier the {@link Identifier} to set the data for
|
||||
* @param data the {@link Data} to set
|
||||
* @since 3.0
|
||||
*/
|
||||
@Override
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
getPlugin().runSync(() -> data.apply(this, getPlugin()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a serialized data snapshot of this data owner
|
||||
*
|
||||
* @param saveCause the cause of the snapshot
|
||||
* @return the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
default DataSnapshot.Packed createSnapshot(@NotNull DataSnapshot.SaveCause saveCause) {
|
||||
return DataSnapshot.builder(getPlugin()).data(this.getData()).saveCause(saveCause).buildAndPack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize and apply a data snapshot to this data owner
|
||||
* <p>
|
||||
* This method will deserialize the data on the current thread, then synchronously apply it on
|
||||
* the main server thread.
|
||||
* </p>
|
||||
* The {@code runAfter} callback function will be run after the snapshot has been applied.
|
||||
*
|
||||
* @param snapshot the snapshot to apply
|
||||
* @param runAfter the function to run asynchronously after the snapshot has been applied
|
||||
* @since 3.0
|
||||
*/
|
||||
default void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull ThrowingConsumer<UserDataHolder> runAfter) {
|
||||
final HuskSync plugin = getPlugin();
|
||||
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
|
||||
plugin.runSync(() -> {
|
||||
unpacked.getData().forEach((type, data) -> {
|
||||
if (plugin.getSettings().isSyncFeatureEnabled(type)) {
|
||||
if (type.isCustom()) {
|
||||
getCustomDataStore().put(type, data);
|
||||
}
|
||||
data.apply(this, plugin);
|
||||
}
|
||||
});
|
||||
plugin.runAsync(() -> runAfter.accept(this));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setInventory(@NotNull Data.Items.Inventory inventory) {
|
||||
this.setData(Identifier.INVENTORY, inventory);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setEnderChest(@NotNull Data.Items.EnderChest enderChest) {
|
||||
this.setData(Identifier.ENDER_CHEST, enderChest);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setPotionEffects(@NotNull Data.PotionEffects potionEffects) {
|
||||
this.setData(Identifier.POTION_EFFECTS, potionEffects);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setAdvancements(@NotNull Data.Advancements advancements) {
|
||||
this.setData(Identifier.ADVANCEMENTS, advancements);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setLocation(@NotNull Data.Location location) {
|
||||
this.setData(Identifier.LOCATION, location);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setStatistics(@NotNull Data.Statistics statistics) {
|
||||
this.setData(Identifier.STATISTICS, statistics);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setHealth(@NotNull Data.Health health) {
|
||||
this.setData(Identifier.HEALTH, health);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setHunger(@NotNull Data.Hunger hunger) {
|
||||
this.setData(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setExperience(@NotNull Data.Experience experience) {
|
||||
this.setData(Identifier.EXPERIENCE, experience);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setGameMode(@NotNull Data.GameMode gameMode) {
|
||||
this.setData(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
|
||||
this.setData(Identifier.PERSISTENT_DATA, persistentData);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getCustomDataStore();
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Represents a uniquely versioned and timestamped snapshot of a user's data, including why it was saved.
|
||||
*
|
||||
* @param versionUUID The unique identifier for this user data version
|
||||
* @param versionTimestamp An epoch milliseconds timestamp of when this data was created
|
||||
* @param userData The {@link UserData} that has been versioned
|
||||
* @param cause The {@link DataSaveCause} that caused this data to be saved
|
||||
*/
|
||||
public record UserDataSnapshot(@NotNull UUID versionUUID, @NotNull Date versionTimestamp,
|
||||
@NotNull DataSaveCause cause, boolean pinned,
|
||||
@NotNull UserData userData) implements Comparable<UserDataSnapshot> {
|
||||
|
||||
/**
|
||||
* Version {@link UserData} into a {@link UserDataSnapshot}, assigning it a random {@link UUID} and the current timestamp {@link Date}
|
||||
* </p>
|
||||
* Note that this method will set {@code cause} to {@link DataSaveCause#API}
|
||||
*
|
||||
* @param userData The {@link UserData} to version
|
||||
* @return A new {@link UserDataSnapshot}
|
||||
* @implNote This isn't used to version data that is going to be set to a database to prevent UUID collisions.<p>
|
||||
* Database implementations should instead use their own UUID generation functions.
|
||||
*/
|
||||
public static UserDataSnapshot create(@NotNull UserData userData) {
|
||||
return new UserDataSnapshot(UUID.randomUUID(), new Date(),
|
||||
DataSaveCause.API, false, userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a menu in chat to an {@link OnlineUser} about this {@link UserDataSnapshot} for a {@link User dataOwner}
|
||||
*
|
||||
* @param user The {@link OnlineUser} to display the menu to
|
||||
* @param dataOwner The {@link User} whose data this snapshot captures a state of
|
||||
* @param locales The {@link Locales} to use for displaying the menu
|
||||
*/
|
||||
public void displayDataOverview(@NotNull OnlineUser user, @NotNull User dataOwner, @NotNull Locales locales) {
|
||||
// Title message, timestamp, owner and cause.
|
||||
locales.getLocale("data_manager_title", versionUUID().toString().split("-")[0],
|
||||
versionUUID().toString(), dataOwner.username, dataOwner.uuid.toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_timestamp",
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(versionTimestamp()))
|
||||
.ifPresent(user::sendMessage);
|
||||
if (pinned()) {
|
||||
locales.getLocale("data_manager_pinned").ifPresent(user::sendMessage);
|
||||
}
|
||||
locales.getLocale("data_manager_cause", cause().name().toLowerCase(Locale.ENGLISH).replaceAll("_", " "))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
// User status data, if present in the snapshot
|
||||
userData().getStatus()
|
||||
.flatMap(statusData -> locales.getLocale("data_manager_status",
|
||||
Integer.toString((int) statusData.health),
|
||||
Integer.toString((int) statusData.maxHealth),
|
||||
Integer.toString(statusData.hunger),
|
||||
Integer.toString(statusData.expLevel),
|
||||
statusData.gameMode.toLowerCase(Locale.ENGLISH)))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
// Advancement and statistic data, if both are present in the snapshot
|
||||
userData().getAdvancements()
|
||||
.flatMap(advancementData -> userData().getStatistics()
|
||||
.flatMap(statisticsData -> locales.getLocale("data_manager_advancements_statistics",
|
||||
Integer.toString(advancementData.size()),
|
||||
generateAdvancementPreview(advancementData, locales),
|
||||
String.format("%.2f", (((statisticsData.untypedStatistics.getOrDefault(
|
||||
"PLAY_ONE_MINUTE", 0)) / 20d) / 60d) / 60d))))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
if (user.hasPermission(Permission.COMMAND_INVENTORY.node)
|
||||
&& user.hasPermission(Permission.COMMAND_ENDER_CHEST.node)) {
|
||||
locales.getLocale("data_manager_item_buttons", dataOwner.username, versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
if (user.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
locales.getLocale("data_manager_management_buttons", dataOwner.username, versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
if (user.hasPermission(Permission.COMMAND_USER_DATA_DUMP.node)) {
|
||||
locales.getLocale("data_manager_system_buttons", dataOwner.username, versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String generateAdvancementPreview(@NotNull List<AdvancementData> advancementData, @NotNull Locales locales) {
|
||||
final StringJoiner joiner = new StringJoiner("\n");
|
||||
final List<AdvancementData> advancementsToPreview = advancementData.stream().filter(dataItem ->
|
||||
!dataItem.key.startsWith("minecraft:recipes/")).toList();
|
||||
final int PREVIEW_SIZE = 8;
|
||||
for (int i = 0; i < advancementsToPreview.size(); i++) {
|
||||
joiner.add(advancementsToPreview.get(i).key);
|
||||
if (i >= PREVIEW_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final int remainingAdvancements = advancementsToPreview.size() - PREVIEW_SIZE;
|
||||
if (remainingAdvancements > 0) {
|
||||
joiner.add(locales.getRawLocale("data_manager_advancements_preview_remaining",
|
||||
Integer.toString(remainingAdvancements)).orElse("+" + remainingAdvancements + "…"));
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare UserData by creation timestamp
|
||||
*
|
||||
* @param other the other UserData to be compared
|
||||
* @return the comparison result; the more recent UserData is greater than the less recent UserData
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(@NotNull UserDataSnapshot other) {
|
||||
return Long.compare(this.versionTimestamp.getTime(), other.versionTimestamp.getTime());
|
||||
}
|
||||
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Used to fire plugin {@link Event}s
|
||||
*/
|
||||
public abstract class EventCannon {
|
||||
|
||||
protected EventCannon() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires a {@link PreSyncEvent}
|
||||
*
|
||||
* @param user The user to fire the event for
|
||||
* @param userData The user data to fire the event with
|
||||
* @return A future that will be completed when the event is fired
|
||||
*/
|
||||
public abstract CompletableFuture<Event> firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData);
|
||||
|
||||
/**
|
||||
* Fires a {@link DataSaveEvent}
|
||||
*
|
||||
* @param user The user to fire the event for
|
||||
* @param userData The user data to fire the event with
|
||||
* @return A future that will be completed when the event is fired
|
||||
*/
|
||||
public abstract CompletableFuture<Event> fireDataSaveEvent(@NotNull User user, @NotNull UserData userData,
|
||||
|
||||
@NotNull DataSaveCause saveCause);
|
||||
|
||||
/**
|
||||
* Fires a {@link SyncCompleteEvent}
|
||||
*
|
||||
* @param user The user to fire the event for
|
||||
*/
|
||||
public abstract void fireSyncCompleteEvent(@NotNull OnlineUser user);
|
||||
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Used to fire plugin {@link Event}s
|
||||
*/
|
||||
public interface EventDispatcher {
|
||||
|
||||
/**
|
||||
* Fire an event synchronously, then run a callback asynchronously.
|
||||
*
|
||||
* @param event The event to fire
|
||||
* @param callback The callback to run after the event has been fired
|
||||
* @param <T> The material of event to fire
|
||||
*/
|
||||
default <T extends Event> void fireEvent(@NotNull T event, @Nullable Consumer<T> callback) {
|
||||
getPlugin().runSync(() -> {
|
||||
if (!fireIsCancelled(event) && callback != null) {
|
||||
getPlugin().runAsync(() -> callback.accept(event));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire an event on this thread, and return whether the event was canceled.
|
||||
*
|
||||
* @param event The event to fire
|
||||
* @param <T> The material of event to fire
|
||||
* @return Whether the event was canceled
|
||||
*/
|
||||
<T extends Event> boolean fireIsCancelled(@NotNull T event);
|
||||
|
||||
@NotNull
|
||||
PreSyncEvent getPreSyncEvent(@NotNull OnlineUser user, @NotNull DataSnapshot.Packed userData);
|
||||
|
||||
@NotNull
|
||||
DataSaveEvent getDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed saveCause);
|
||||
|
||||
@NotNull
|
||||
SyncCompleteEvent getSyncCompleteEvent(@NotNull OnlineUser user);
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
@ -1,240 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.hook;
|
||||
|
||||
import com.djrapitops.plan.extension.CallEvents;
|
||||
import com.djrapitops.plan.extension.DataExtension;
|
||||
import com.djrapitops.plan.extension.ElementOrder;
|
||||
import com.djrapitops.plan.extension.FormatType;
|
||||
import com.djrapitops.plan.extension.annotation.*;
|
||||
import com.djrapitops.plan.extension.icon.Color;
|
||||
import com.djrapitops.plan.extension.icon.Family;
|
||||
import com.djrapitops.plan.extension.icon.Icon;
|
||||
import com.djrapitops.plan.extension.table.Table;
|
||||
import com.djrapitops.plan.extension.table.TableColumnFormat;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@TabInfo(
|
||||
tab = "Current Status",
|
||||
iconName = "id-card",
|
||||
iconFamily = Family.SOLID,
|
||||
elementOrder = {ElementOrder.VALUES, ElementOrder.TABLE, ElementOrder.GRAPH}
|
||||
)
|
||||
@TabInfo(
|
||||
tab = "Data Snapshots",
|
||||
iconName = "clipboard-list",
|
||||
iconFamily = Family.SOLID,
|
||||
elementOrder = {ElementOrder.VALUES, ElementOrder.TABLE, ElementOrder.GRAPH}
|
||||
)
|
||||
@TabOrder({"Current Status", "Data Snapshots"})
|
||||
@PluginInfo(
|
||||
name = "HuskSync",
|
||||
iconName = "exchange-alt",
|
||||
iconFamily = Family.SOLID,
|
||||
color = Color.LIGHT_BLUE
|
||||
)
|
||||
@SuppressWarnings("unused")
|
||||
public class PlanDataExtension implements DataExtension {
|
||||
|
||||
private HuskSync plugin;
|
||||
|
||||
private static final String UNKNOWN_STRING = "N/A";
|
||||
|
||||
private static final String PINNED_HTML_STRING = "📍 ";
|
||||
|
||||
protected PlanDataExtension(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
protected PlanDataExtension() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallEvents[] callExtensionMethodsOn() {
|
||||
return new CallEvents[]{
|
||||
CallEvents.PLAYER_JOIN,
|
||||
CallEvents.PLAYER_LEAVE
|
||||
};
|
||||
}
|
||||
|
||||
private CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull UUID uuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final Optional<User> optionalUser = plugin.getDatabase().getUser(uuid).join();
|
||||
if (optionalUser.isPresent()) {
|
||||
return plugin.getDatabase().getCurrentUserData(optionalUser.get()).join();
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@BooleanProvider(
|
||||
text = "Has Synced",
|
||||
description = "Whether this user has saved, synchronised data.",
|
||||
iconName = "exchange-alt",
|
||||
iconFamily = Family.SOLID,
|
||||
conditionName = "hasSynced",
|
||||
hidden = true
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public boolean getUserHasSynced(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join().isPresent();
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Sync Time",
|
||||
description = "The last time the user had their data synced with the server.",
|
||||
iconName = "clock",
|
||||
iconFamily = Family.SOLID,
|
||||
format = FormatType.DATE_SECOND,
|
||||
priority = 6
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getCurrentDataTimestamp(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join().map(
|
||||
versionedUserData -> versionedUserData.versionTimestamp().getTime())
|
||||
.orElse(new Date().getTime());
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@StringProvider(
|
||||
text = "Version ID",
|
||||
description = "ID of the data version that the user is currently using.",
|
||||
iconName = "bolt",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 5
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getCurrentDataId(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join()
|
||||
.map(versionedUserData -> versionedUserData.versionUUID().toString()
|
||||
.split(Pattern.quote("-"))[0])
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@StringProvider(
|
||||
text = "Health",
|
||||
description = "The number of health points out of the max health points this player currently has.",
|
||||
iconName = "heart",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 4
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getHealth(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(statusData -> (int) statusData.health + "/" + (int) statusData.maxHealth)
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Hunger",
|
||||
description = "The number of hunger points this player currently has.",
|
||||
iconName = "drumstick-bite",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 3
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getHunger(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(statusData -> (long) statusData.hunger)
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Experience Level",
|
||||
description = "The number of experience levels this player currently has.",
|
||||
iconName = "hat-wizard",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 2
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getExperienceLevel(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(statusData -> (long) statusData.expLevel)
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@StringProvider(
|
||||
text = "Game Mode",
|
||||
description = "The game mode this player is currently in.",
|
||||
iconName = "gamepad",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 1
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getGameMode(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(status -> status.gameMode)
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Advancements",
|
||||
description = "The number of advancements & recipes the player has progressed in.",
|
||||
iconName = "award",
|
||||
iconFamily = Family.SOLID
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getAdvancementsCompleted(@NotNull UUID playerUUID) {
|
||||
return getCurrentUserData(playerUUID).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getAdvancements())
|
||||
.map(advancementsData -> (long) advancementsData.size())
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@TableProvider(tableColor = Color.LIGHT_BLUE)
|
||||
@Tab("Data Snapshots")
|
||||
public Table getDataSnapshots(@NotNull UUID playerUUID) {
|
||||
final Table.Factory dataSnapshotsTable = Table.builder()
|
||||
.columnOne("Time", new Icon(Family.SOLID, "clock", Color.NONE))
|
||||
.columnOneFormat(TableColumnFormat.DATE_SECOND)
|
||||
.columnTwo("ID", new Icon(Family.SOLID, "bolt", Color.NONE))
|
||||
.columnThree("Cause", new Icon(Family.SOLID, "flag", Color.NONE))
|
||||
.columnFour("Pinned", new Icon(Family.SOLID, "thumbtack", Color.NONE));
|
||||
plugin.getDatabase().getUser(playerUUID).join().ifPresent(user ->
|
||||
plugin.getDatabase().getUserData(user).join().forEach(versionedUserData -> dataSnapshotsTable.addRow(
|
||||
versionedUserData.versionTimestamp().getTime(),
|
||||
versionedUserData.versionUUID().toString().split("-")[0],
|
||||
versionedUserData.cause().name().toLowerCase(Locale.ENGLISH).replaceAll("_", " "),
|
||||
versionedUserData.pinned() ? PINNED_HTML_STRING + "Pinned" : "Unpinned"
|
||||
)));
|
||||
return dataSnapshotsTable.build();
|
||||
}
|
||||
}
|
@ -1,423 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import de.themoep.minedown.adventure.MineDownParser;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.event.PreSyncEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Represents a logged-in {@link User}
|
||||
*/
|
||||
public abstract class OnlineUser extends User {
|
||||
|
||||
public OnlineUser(@NotNull UUID uuid, @NotNull String username) {
|
||||
super(uuid, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the player's {@link StatusData}
|
||||
*
|
||||
* @return the player's {@link StatusData}
|
||||
*/
|
||||
public abstract CompletableFuture<StatusData> getStatus();
|
||||
|
||||
/**
|
||||
* Set the player's {@link StatusData}
|
||||
*
|
||||
* @param statusData the player's {@link StatusData}
|
||||
* @param statusDataFlags the flags to use for setting the status data
|
||||
* @return a future returning void when complete
|
||||
* @deprecated Use {@link #setStatus(StatusData, Settings)} instead
|
||||
*/
|
||||
@Deprecated(since = "2.1")
|
||||
public final CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
|
||||
@NotNull List<StatusDataFlag> statusDataFlags) {
|
||||
final Settings settings = new Settings();
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.HEALTH.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_HEALTH));
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.MAX_HEALTH.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_MAX_HEALTH));
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.HUNGER.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_HUNGER));
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.EXPERIENCE.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_EXPERIENCE));
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.INVENTORIES.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_SELECTED_ITEM_SLOT));
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.LOCATION.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_GAME_MODE) || statusDataFlags.contains(StatusDataFlag.SET_FLYING));
|
||||
return setStatus(statusData, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's {@link StatusData}
|
||||
*
|
||||
* @param statusData the player's {@link StatusData}
|
||||
* @param settings settings, containing information about which features should be synced
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull Settings settings);
|
||||
|
||||
/**
|
||||
* Get the player's inventory {@link ItemData} contents
|
||||
*
|
||||
* @return The player's inventory {@link ItemData} contents
|
||||
*/
|
||||
public abstract CompletableFuture<ItemData> getInventory();
|
||||
|
||||
/**
|
||||
* Set the player's {@link ItemData}
|
||||
*
|
||||
* @param itemData The player's {@link ItemData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setInventory(@NotNull ItemData itemData);
|
||||
|
||||
/**
|
||||
* Get the player's ender chest {@link ItemData} contents
|
||||
*
|
||||
* @return The player's ender chest {@link ItemData} contents
|
||||
*/
|
||||
public abstract CompletableFuture<ItemData> getEnderChest();
|
||||
|
||||
/**
|
||||
* Set the player's {@link ItemData}
|
||||
*
|
||||
* @param enderChestData The player's {@link ItemData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setEnderChest(@NotNull ItemData enderChestData);
|
||||
|
||||
|
||||
/**
|
||||
* Get the player's {@link PotionEffectData}
|
||||
*
|
||||
* @return The player's {@link PotionEffectData}
|
||||
*/
|
||||
public abstract CompletableFuture<PotionEffectData> getPotionEffects();
|
||||
|
||||
/**
|
||||
* Set the player's {@link PotionEffectData}
|
||||
*
|
||||
* @param potionEffectData The player's {@link PotionEffectData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setPotionEffects(@NotNull PotionEffectData potionEffectData);
|
||||
|
||||
/**
|
||||
* Get the player's set of {@link AdvancementData}
|
||||
*
|
||||
* @return the player's set of {@link AdvancementData}
|
||||
*/
|
||||
public abstract CompletableFuture<List<AdvancementData>> getAdvancements();
|
||||
|
||||
/**
|
||||
* Set the player's {@link AdvancementData}
|
||||
*
|
||||
* @param advancementData List of the player's {@link AdvancementData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setAdvancements(@NotNull List<AdvancementData> advancementData);
|
||||
|
||||
/**
|
||||
* Get the player's {@link StatisticsData}
|
||||
*
|
||||
* @return The player's {@link StatisticsData}
|
||||
*/
|
||||
public abstract CompletableFuture<StatisticsData> getStatistics();
|
||||
|
||||
/**
|
||||
* Set the player's {@link StatisticsData}
|
||||
*
|
||||
* @param statisticsData The player's {@link StatisticsData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData);
|
||||
|
||||
/**
|
||||
* Get the player's {@link LocationData}
|
||||
*
|
||||
* @return the player's {@link LocationData}
|
||||
*/
|
||||
public abstract CompletableFuture<LocationData> getLocation();
|
||||
|
||||
/**
|
||||
* Set the player's {@link LocationData}
|
||||
*
|
||||
* @param locationData the player's {@link LocationData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setLocation(@NotNull LocationData locationData);
|
||||
|
||||
/**
|
||||
* Get the player's {@link PersistentDataContainerData}
|
||||
*
|
||||
* @return The player's {@link PersistentDataContainerData} when fetched
|
||||
*/
|
||||
public abstract CompletableFuture<PersistentDataContainerData> getPersistentDataContainer();
|
||||
|
||||
/**
|
||||
* Set the player's {@link PersistentDataContainerData}
|
||||
*
|
||||
* @param persistentDataContainerData The player's {@link PersistentDataContainerData} to set
|
||||
* @return A future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData);
|
||||
|
||||
/**
|
||||
* Indicates if the player has gone offline
|
||||
*
|
||||
* @return {@code true} if the player has left the server; {@code false} otherwise
|
||||
*/
|
||||
public abstract boolean isOffline();
|
||||
|
||||
/**
|
||||
* Returns the implementing Minecraft server version
|
||||
*
|
||||
* @return The Minecraft server version
|
||||
*/
|
||||
@NotNull
|
||||
public abstract Version getMinecraftVersion();
|
||||
|
||||
/**
|
||||
* Get the player's adventure {@link Audience}
|
||||
*
|
||||
* @return the player's {@link Audience}
|
||||
*/
|
||||
@NotNull
|
||||
public abstract Audience getAudience();
|
||||
|
||||
/**
|
||||
* Send a message to this player
|
||||
*
|
||||
* @param component the {@link Component} message to send
|
||||
*/
|
||||
public void sendMessage(@NotNull Component component) {
|
||||
getAudience().sendMessage(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted message to this player
|
||||
*
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendMessage(@NotNull MineDown mineDown) {
|
||||
sendMessage(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted action bar message to this player
|
||||
*
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||
getAudience().sendActionBar(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a toast message to this player
|
||||
*
|
||||
* @param title the title of the toast
|
||||
* @param description the description of the toast
|
||||
* @param iconMaterial the namespace-keyed material to use as an icon of the toast
|
||||
* @param backgroundType the background ("ToastType") of the toast
|
||||
*/
|
||||
public abstract void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||
@NotNull String iconMaterial, @NotNull String backgroundType);
|
||||
|
||||
/**
|
||||
* Returns if the player has the permission node
|
||||
*
|
||||
* @param node The permission node string
|
||||
* @return {@code true} if the player has permission node; {@code false} otherwise
|
||||
*/
|
||||
public abstract boolean hasPermission(@NotNull String node);
|
||||
|
||||
/**
|
||||
* Show a GUI chest menu to the player, containing the given {@link ItemData}
|
||||
*
|
||||
* @param itemData Item data to be shown in the GUI
|
||||
* @param editable If the player should be able to remove, replace and move around the items
|
||||
* @param minimumRows The minimum number of rows to show in the chest menu
|
||||
* @param title The title of the chest menu, as a {@link MineDown} locale
|
||||
* @return A future returning the {@link ItemData} in the chest menu when the player closes it
|
||||
* @since 2.1
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<ItemData>> showMenu(@NotNull ItemData itemData, boolean editable,
|
||||
int minimumRows, @NotNull MineDown title);
|
||||
|
||||
/**
|
||||
* Returns true if the player is dead
|
||||
*
|
||||
* @return true if the player is dead
|
||||
*/
|
||||
public abstract boolean isDead();
|
||||
|
||||
/**
|
||||
* Apply {@link UserData} to a player, updating their inventory, status, statistics, etc. as per the config.
|
||||
* <p>
|
||||
* This will only set data that is enabled as per the enabled settings in the config file.
|
||||
* Data present in the {@link UserData} object, but not enabled to be set in the config, will be ignored.
|
||||
*
|
||||
* @param plugin The plugin instance
|
||||
* @return a future returning a boolean when complete; if the sync was successful, the future will return {@code true}.
|
||||
*/
|
||||
public final CompletableFuture<Boolean> setData(@NotNull UserData data, @NotNull HuskSync plugin) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Prevent synchronising user data from newer versions of Minecraft
|
||||
if (Version.fromString(data.getMinecraftVersion()).compareTo(plugin.getMinecraftVersion()) > 0) {
|
||||
plugin.log(Level.SEVERE, "Cannot set data for " + username +
|
||||
" because the Minecraft version of their user data (" + data.getMinecraftVersion() +
|
||||
") is newer than the server's Minecraft version (" + plugin.getMinecraftVersion() + ").");
|
||||
return false;
|
||||
}
|
||||
// Prevent synchronising user data from newer versions of the plugin
|
||||
if (data.getFormatVersion() > UserData.CURRENT_FORMAT_VERSION) {
|
||||
plugin.log(Level.SEVERE, "Cannot set data for " + username +
|
||||
" because the format version of their user data (v" + data.getFormatVersion() +
|
||||
") is newer than the current format version (v" + UserData.CURRENT_FORMAT_VERSION + ").");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fire the PreSyncEvent
|
||||
final PreSyncEvent preSyncEvent = (PreSyncEvent) plugin.getEventCannon().firePreSyncEvent(this, data).join();
|
||||
final UserData finalData = preSyncEvent.getUserData();
|
||||
final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{
|
||||
if (!isOffline() && !preSyncEvent.isCancelled()) {
|
||||
final Settings settings = plugin.getSettings();
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
|
||||
finalData.getInventory().ifPresent(itemData -> add(setInventory(itemData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ENDER_CHESTS)) {
|
||||
finalData.getEnderChest().ifPresent(itemData -> add(setEnderChest(itemData)));
|
||||
}
|
||||
finalData.getStatus().ifPresent(statusData -> add(setStatus(statusData, settings)));
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.POTION_EFFECTS)) {
|
||||
finalData.getPotionEffects().ifPresent(potionEffectData -> add(setPotionEffects(potionEffectData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ADVANCEMENTS)) {
|
||||
finalData.getAdvancements().ifPresent(advancementData -> add(setAdvancements(advancementData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.STATISTICS)) {
|
||||
finalData.getStatistics().ifPresent(statisticData -> add(setStatistics(statisticData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
|
||||
finalData.getLocation().ifPresent(locationData -> add(setLocation(locationData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.PERSISTENT_DATA_CONTAINER)) {
|
||||
finalData.getPersistentDataContainer().ifPresent(persistentDataContainerData ->
|
||||
add(setPersistentDataContainer(persistentDataContainerData)));
|
||||
}
|
||||
}
|
||||
}};
|
||||
// Apply operations in parallel, join when complete
|
||||
return CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).thenApply(unused -> true)
|
||||
.exceptionally(exception -> {
|
||||
// Handle synchronisation exceptions
|
||||
plugin.log(Level.SEVERE, "Failed to set data for player " + username + " (" + exception.getMessage() + ")");
|
||||
exception.printStackTrace();
|
||||
return false;
|
||||
}).join();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the player's current {@link UserData} in an {@link Optional}.
|
||||
* <p>
|
||||
* Since v2.1, this method will respect the data synchronisation settings; user data will only be as big as the
|
||||
* enabled synchronisation values set in the config file
|
||||
* <p>
|
||||
* Also note that if the {@code SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES} ConfigOption has been set,
|
||||
* the user's inventory will only be returned if the player is alive.
|
||||
* <p>
|
||||
* If the user data could not be returned due to an exception, the optional will return empty
|
||||
*
|
||||
* @param plugin The plugin instance
|
||||
*/
|
||||
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull HuskSync plugin) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final UserDataBuilder builder = UserData.builder(getMinecraftVersion());
|
||||
final List<CompletableFuture<Void>> dataGetOperations = new ArrayList<>() {{
|
||||
if (!isOffline()) {
|
||||
final Settings settings = plugin.getSettings();
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
|
||||
if (isDead() && settings.isSynchroniseDeadPlayersChangingServer()) {
|
||||
plugin.debug("Player " + username + " is dead, so their inventory will be set to empty.");
|
||||
add(CompletableFuture.runAsync(() -> builder.setInventory(ItemData.empty())));
|
||||
} else {
|
||||
add(getInventory().thenAccept(builder::setInventory));
|
||||
}
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ENDER_CHESTS)) {
|
||||
add(getEnderChest().thenAccept(builder::setEnderChest));
|
||||
}
|
||||
add(getStatus().thenAccept(builder::setStatus));
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.POTION_EFFECTS)) {
|
||||
add(getPotionEffects().thenAccept(builder::setPotionEffects));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ADVANCEMENTS)) {
|
||||
add(getAdvancements().thenAccept(builder::setAdvancements));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.STATISTICS)) {
|
||||
add(getStatistics().thenAccept(builder::setStatistics));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
|
||||
add(getLocation().thenAccept(builder::setLocation));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.PERSISTENT_DATA_CONTAINER)) {
|
||||
add(getPersistentDataContainer().thenAccept(builder::setPersistentDataContainer));
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
// Apply operations in parallel, join when complete
|
||||
CompletableFuture.allOf(dataGetOperations.toArray(new CompletableFuture[0])).join();
|
||||
return Optional.of(builder.build());
|
||||
}).exceptionally(exception -> {
|
||||
plugin.log(Level.SEVERE, "Failed to get user data from online player " + username + " (" + exception.getMessage() + ")");
|
||||
exception.printStackTrace();
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if the player is locked
|
||||
*
|
||||
* @return the player's locked status
|
||||
*/
|
||||
public abstract boolean isLocked();
|
||||
|
||||
/**
|
||||
* Get if the player is a NPC
|
||||
*
|
||||
* @return if the player is a NPC with metadata
|
||||
*/
|
||||
public abstract boolean isNpc();
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import de.themoep.minedown.adventure.MineDownParser;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Represents a logged-in {@link User}
|
||||
*/
|
||||
public abstract class OnlineUser extends User implements CommandUser, UserDataHolder {
|
||||
|
||||
public OnlineUser(@NotNull UUID uuid, @NotNull String username) {
|
||||
super(uuid, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the player has gone offline
|
||||
*
|
||||
* @return {@code true} if the player has left the server; {@code false} otherwise
|
||||
*/
|
||||
public abstract boolean isOffline();
|
||||
|
||||
/**
|
||||
* Get the player's adventure {@link Audience}
|
||||
*
|
||||
* @return the player's {@link Audience}
|
||||
*/
|
||||
@NotNull
|
||||
public abstract Audience getAudience();
|
||||
|
||||
/**
|
||||
* Send a message to this player
|
||||
*
|
||||
* @param component the {@link Component} message to send
|
||||
*/
|
||||
public void sendMessage(@NotNull Component component) {
|
||||
getAudience().sendMessage(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted message to this player
|
||||
*
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendMessage(@NotNull MineDown mineDown) {
|
||||
sendMessage(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted action bar message to this player
|
||||
*
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||
getAudience().sendActionBar(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a toast message to this player
|
||||
*
|
||||
* @param title the title of the toast
|
||||
* @param description the description of the toast
|
||||
* @param iconMaterial the namespace-keyed material to use as an hasIcon of the toast
|
||||
* @param backgroundType the background ("ToastType") of the toast
|
||||
*/
|
||||
public abstract void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||
@NotNull String iconMaterial, @NotNull String backgroundType);
|
||||
|
||||
/**
|
||||
* Show a GUI chest menu to the user
|
||||
*
|
||||
* @param items the items to fill the menu with
|
||||
* @param title the title of the menu
|
||||
* @param editable whether the menu is editable (items can be removed or added)
|
||||
* @param size the size of the menu
|
||||
* @param onClose the action to perform when the menu is closed
|
||||
*/
|
||||
public abstract void showGui(@NotNull Data.Items.Items items, @NotNull MineDown title, boolean editable, int size,
|
||||
@NotNull Consumer<Data.Items.Items> onClose);
|
||||
|
||||
/**
|
||||
* Returns if the player has the permission node
|
||||
*
|
||||
* @param node The permission node string
|
||||
* @return {@code true} if the player has permission node; {@code false} otherwise
|
||||
*/
|
||||
public abstract boolean hasPermission(@NotNull String node);
|
||||
|
||||
|
||||
/**
|
||||
* Set a player's status from a {@link DataSnapshot}
|
||||
*
|
||||
* @param snapshot The {@link DataSnapshot} to set the player's status from
|
||||
*/
|
||||
public void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull DataSnapshot.UpdateCause cause) {
|
||||
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
|
||||
if (!isOffline()) {
|
||||
UserDataHolder.super.applySnapshot(
|
||||
event.getData(), (owner) -> completeSync(true, cause, getPlugin())
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a player's synchronization completion
|
||||
*
|
||||
* @param succeeded Whether the synchronization succeeded
|
||||
* @param plugin The plugin instance
|
||||
*/
|
||||
public void completeSync(boolean succeeded, @NotNull DataSnapshot.UpdateCause cause, @NotNull HuskSync plugin) {
|
||||
if (succeeded) {
|
||||
switch (plugin.getSettings().getNotificationDisplaySlot()) {
|
||||
case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
|
||||
case ACTION_BAR -> cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar);
|
||||
case TOAST -> cause.getCompletedLocale(plugin)
|
||||
.ifPresent(locale -> this.sendToast(
|
||||
locale, new MineDown(""),
|
||||
"minecraft:bell",
|
||||
"TASK"
|
||||
));
|
||||
}
|
||||
plugin.fireEvent(
|
||||
plugin.getSyncCompleteEvent(this),
|
||||
(event) -> plugin.getLockedPlayers().remove(getUuid())
|
||||
);
|
||||
} else {
|
||||
cause.getFailedLocale(plugin).ifPresent(this::sendMessage);
|
||||
}
|
||||
|
||||
// Ensure the user is in the database
|
||||
plugin.getDatabase().ensureUser(this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Map<Identifier, Data> getCustomDataStore() {
|
||||
return getPlugin().getPlayerCustomDataStore(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if the player is locked
|
||||
*
|
||||
* @return the player's locked status
|
||||
*/
|
||||
public abstract boolean isLocked();
|
||||
|
||||
/**
|
||||
* Get if the player is a NPC
|
||||
*
|
||||
* @return if the player is a NPC with metadata
|
||||
*/
|
||||
public abstract boolean isNpc();
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue