9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-21 15:49:20 +00:00

Compare commits

..

44 Commits
2.1 ... 2.2

Author SHA1 Message Date
William
5817de83e5 Fix locked map data not being applied in some cases 2023-01-03 12:38:17 +00:00
William
30dd48ce88 Add additional checks and error handling when setting health 2023-01-03 12:14:18 +00:00
William
cf7912a89e Bump to 2.2 2023-01-03 11:58:06 +00:00
William
9900b44858 Disable locked map syncing by default 2023-01-03 11:56:47 +00:00
William
9019181208 Merge remote-tracking branch 'origin/master' 2023-01-03 11:51:29 +00:00
William
99483387f1 Add additional error handling for player health and statistic updating 2023-01-03 11:51:25 +00:00
William
42177f2582 Merge pull request #75 from emmanuelvlad/expose-locked-players 2023-01-01 16:19:52 +00:00
William
e4e0743205 [ci skip] Update license year (2023) 2023-01-01 16:18:58 +00:00
evlad
105927a57f added dummy method 2022-12-31 04:41:20 +03:00
evlad
71706bf9ae expose locked players + add a method on OnlineUser 2022-12-31 04:30:45 +03:00
William
101e0c11d7 Fix unsynced players having data saved on world save / death 2022-12-27 19:31:14 +00:00
William
70323fb2e2 Merge remote-tracking branch 'origin/master' 2022-12-26 16:42:48 +00:00
William
9dc5577175 Clear the player's cursor when setting inventory contents 2022-12-26 16:06:22 +00:00
William
117d5edea2 [ci skip] Update badge 2022-12-19 16:04:35 +00:00
William
3f0f518037 [ci-skip] Clarify minimum Java version in README 2022-11-20 18:43:04 +00:00
William
2017ecc20f Minor refactoring / code improvements 2022-11-18 14:40:15 +00:00
William
ded89ad343 ACTION_BAR as default notification slot 2022-11-18 11:25:00 +00:00
William
c4b194f8d6 Fix unlocked maps getting wrongly locked 2022-11-17 02:46:40 +00:00
William
d682e6e6c6 Fix notifications through Toast AndJam library 2022-11-17 02:34:52 +00:00
William
6fef9c4eae Remove map itemstack debug logging mess 2022-11-16 22:40:23 +00:00
William278
16eee05065 Add notification slot configuration, support for toasts 2022-11-16 22:31:57 +00:00
William278
b664e2586d Bump gson to 2.10 2022-11-16 22:06:26 +00:00
William278
d594c9c257 Truncate long data save cause names, close #60 2022-11-16 14:44:32 +00:00
William278
532a65eca8 Ensure players remain locked on disconnect and shutdown, close #67 2022-11-16 12:06:01 +00:00
William
5af8ae0da5 Use canvas rendering approach, finish locked map synchronisation 2022-11-15 23:37:41 +00:00
William278
c0709f82bd Add the ability to synchronise/persist locked maps cross-server, close #14 2022-11-15 18:29:37 +00:00
William278
945b65e1bc Minor performance improvement to event cancelling, add checks against inventory clicks 2022-11-15 17:46:55 +00:00
William278
efcb36d345 Fix database username at wrong config path 2022-11-15 11:37:15 +00:00
William
30cd89c578 Remove unnecessary debug 2022-11-15 00:43:52 +00:00
William
bb3753b8e4 Fix typo 2022-11-15 00:42:34 +00:00
William
d5569ad3ed Fix event priorities in config, bump to 2.1.3 2022-11-15 00:36:45 +00:00
William
d8386fd2a2 Fix edit nodes not being respected 2022-11-14 18:25:44 +00:00
William
3bfea58f35 Make event priority configurable for three key events 2022-11-14 11:47:33 +00:00
William
51cf7beeb8 Remove redundant compiler warning suppressors 2022-11-14 11:04:41 +00:00
William
df247b41f4 Bump to v2.1.2 2022-11-06 22:32:46 +00:00
William
bac760165e Tweak logic for determining if a player is dead, fix issues with <1HP players being detected dead 2022-11-06 22:31:33 +00:00
William
dd39482ed1 Tweaked bukkit implementation of #isDead detection 2022-10-28 13:00:08 +01:00
William
c05f165278 Bump to v2.1.1 2022-10-26 15:21:06 +01:00
William
c888759d33 Merge remote-tracking branch 'origin/master' 2022-10-26 15:20:21 +01:00
William
089ea5b63a Fix unsafe joins on inventory and ender chest commands, better exception catching, Close #58 2022-10-26 15:20:08 +01:00
William
9020e9d906 Merge pull request #57 from iVillager/patch-1
Update it-it.yml
2022-10-19 19:41:27 +01:00
Villag3r_
7584ea0070 Update it-it.yml 2022-10-19 19:46:25 +02:00
William
9c243c2893 Merge pull request #56 from Ceddix/master
Updated German locales
2022-10-19 15:27:08 +01:00
Ceddix
8ba90fadc4 Updated german locales 2022-10-19 15:39:09 +02:00
30 changed files with 796 additions and 202 deletions

View File

@@ -1,4 +1,4 @@
Copyright © William278 2022. All rights reserved Copyright © William278 2023. All rights reserved
LICENSE LICENSE
This source code is provided as reference to licensed individuals that have purchased the HuskSync This source code is provided as reference to licensed individuals that have purchased the HuskSync

View File

@@ -1,5 +1,5 @@
# [![HuskSync Banner](images/banner-graphic.png)](https://github.com/WiIIiam278/HuskSync) # [![HuskSync Banner](images/banner-graphic.png)](https://github.com/WiIIiam278/HuskSync)
[![GitHub CI](https://img.shields.io/github/workflow/status/WiIIiam278/HuskSync/Java%20CI?logo=github)](https://github.com/WiIIiam278/HuskSync/actions/workflows/java_ci.yml) [![GitHub CI](https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/java_ci.yml?branch=master&logo=github)](https://github.com/WiIIiam278/HuskSync/actions/workflows/java_ci.yml)
[![JitPack API](https://img.shields.io/jitpack/version/net.william278/HuskSync?color=%2300fb9a&label=api&logo=gradle)](https://jitpack.io/#net.william278/HuskSync) [![JitPack API](https://img.shields.io/jitpack/version/net.william278/HuskSync?color=%2300fb9a&label=api&logo=gradle)](https://jitpack.io/#net.william278/HuskSync)
[![Support Discord](https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/tVYhJfyDWG) [![Support Discord](https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/tVYhJfyDWG)
@@ -20,7 +20,7 @@
## Requirements ## Requirements
* A MySQL Database (v8.0+). * A MySQL Database (v8.0+).
* A Redis Database (v5.0+) * A Redis Database (v5.0+)
* Any number of proxied Spigot servers (Minecraft v1.16.5+) * Any number of proxied Spigot servers (Minecraft v1.16.5+, Java 16+)
## Setup ## Setup
1. Place the plugin jar file in the `/plugins/` directory of each Spigot server. You do not need to install HuskSync as a proxy plugin. 1. Place the plugin jar file in the `/plugins/` directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.

View File

@@ -3,6 +3,8 @@ dependencies {
implementation 'org.bstats:bstats-bukkit:3.0.0' implementation 'org.bstats:bstats-bukkit:3.0.0'
implementation 'net.william278:mpdbdataconverter:1.0.1' implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0' implementation 'net.william278:hsldataconverter:1.0'
implementation 'net.william278:MapDataAPI:1.0.2'
implementation 'net.william278:AndJam:1.0.2'
implementation 'me.lucko:commodore:2.2' implementation 'me.lucko:commodore:2.2'
implementation 'net.kyori:adventure-platform-bukkit:4.1.2' implementation 'net.kyori:adventure-platform-bukkit:4.1.2'
implementation 'dev.triumphteam:triumph-gui:3.1.3' implementation 'dev.triumphteam:triumph-gui:3.1.3'
@@ -12,8 +14,10 @@ dependencies {
compileOnly 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT' compileOnly 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
compileOnly 'dev.dejvokep:boosted-yaml:1.3' compileOnly 'dev.dejvokep:boosted-yaml:1.3'
compileOnly 'com.zaxxer:HikariCP:5.0.1' compileOnly 'com.zaxxer:HikariCP:5.0.1'
compileOnly 'redis.clients:jedis:' + jedis_version
compileOnly 'net.william278:DesertWell:1.1' compileOnly 'net.william278:DesertWell:1.1'
compileOnly 'net.william278:Annotaml:2.0' compileOnly 'net.william278:Annotaml:2.0'
compileOnly 'net.william278:AdvancementAPI:97a9583413'
} }
shadowJar { shadowJar {
@@ -31,6 +35,10 @@ shadowJar {
relocate 'dev.dejvokep', 'net.william278.husksync.libraries' relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
relocate 'net.querz', 'net.william278.husksync.libraries.nbt'
relocate 'net.roxeez', 'net.william278.husksync.libraries'
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore' relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby' relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'

View File

@@ -134,7 +134,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
// Prepare redis connection // Prepare redis connection
this.redisManager = new RedisManager(this); this.redisManager = new RedisManager(this);
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server..."); getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server...");
initialized.set(this.redisManager.initialize().join()); initialized.set(this.redisManager.initialize());
if (initialized.get()) { if (initialized.get()) {
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server"); getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server");
} else { } else {
@@ -307,6 +307,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return audiences; return audiences;
} }
@Override
public Set<UUID> getLockedPlayers() {
return this.eventListener.getLockedPlayers();
}
@Override @Override
public CompletableFuture<Boolean> reload() { public CompletableFuture<Boolean> reload() {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {

View File

@@ -0,0 +1,207 @@
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.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 = mapMeta.getMapView();
if (mapView == null || !mapView.isLocked() || mapView.isVirtual()) {
return;
}
// Get the map data
plugin.getLoggingAdapter().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.getLoggingAdapter().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.getLoggingAdapter().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.getLoggingAdapter().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);
}
}
}

View File

@@ -1,6 +1,7 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.config.Settings;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import org.bukkit.util.io.BukkitObjectInputStream; import org.bukkit.util.io.BukkitObjectInputStream;
@@ -40,7 +41,11 @@ public class BukkitSerializer {
bukkitOutputStream.writeInt(inventoryContents.length); bukkitOutputStream.writeInt(inventoryContents.length);
// Write each serialize each ItemStack to the output stream // Write each serialize each ItemStack to the output stream
final boolean persistLockedMaps = BukkitHuskSync.getInstance().getSettings().getSynchronizationFeature(Settings.SynchronizationFeature.LOCKED_MAPS);
for (ItemStack inventoryItem : inventoryContents) { for (ItemStack inventoryItem : inventoryContents) {
if (persistLockedMaps) {
BukkitMapHandler.persistMapData(inventoryItem);
}
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem)); bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
} }
@@ -89,8 +94,13 @@ public class BukkitSerializer {
// Set the ItemStacks in the array from deserialized ItemStack data // Set the ItemStacks in the array from deserialized ItemStack data
int slotIndex = 0; int slotIndex = 0;
final boolean persistLockedMaps = BukkitHuskSync.getInstance().getSettings().getSynchronizationFeature(Settings.SynchronizationFeature.LOCKED_MAPS);
for (ItemStack ignored : inventoryContents) { for (ItemStack ignored : inventoryContents) {
inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject()); final ItemStack deserialized = deserializeItemStack(bukkitInputStream.readObject());
if (persistLockedMaps) {
BukkitMapHandler.setMapRenderer(deserialized);
}
inventoryContents[slotIndex] = deserialized;
slotIndex++; slotIndex++;
} }

View File

@@ -0,0 +1,37 @@
package net.william278.husksync.listener;
import net.william278.husksync.config.Settings;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.jetbrains.annotations.NotNull;
public interface BukkitDeathEventListener extends Listener {
boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority);
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
default void onPlayerDeathHighest(@NotNull PlayerDeathEvent event) {
if (handleEvent(Settings.EventType.DEATH_LISTENER, Settings.EventPriority.HIGHEST)) {
handlePlayerDeath(event);
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
default void onPlayerDeath(@NotNull PlayerDeathEvent event) {
if (handleEvent(Settings.EventType.DEATH_LISTENER, Settings.EventPriority.NORMAL)) {
handlePlayerDeath(event);
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
default void onPlayerDeathLowest(@NotNull PlayerDeathEvent event) {
if (handleEvent(Settings.EventType.DEATH_LISTENER, Settings.EventPriority.LOWEST)) {
handlePlayerDeath(event);
}
}
void handlePlayerDeath(@NotNull PlayerDeathEvent player);
}

View File

@@ -1,6 +1,7 @@
package net.william278.husksync.listener; package net.william278.husksync.listener;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.BukkitInventoryMap; import net.william278.husksync.data.BukkitInventoryMap;
import net.william278.husksync.data.BukkitSerializer; import net.william278.husksync.data.BukkitSerializer;
import net.william278.husksync.data.ItemData; import net.william278.husksync.data.ItemData;
@@ -16,11 +17,10 @@ import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityPickupItemEvent; import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.WorldSaveEvent; import org.bukkit.event.world.WorldSaveEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -28,39 +28,35 @@ import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class BukkitEventListener extends EventListener implements Listener { public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
BukkitDeathEventListener, Listener {
public BukkitEventListener(@NotNull BukkitHuskSync huskSync) { public BukkitEventListener(@NotNull BukkitHuskSync huskSync) {
super(huskSync); super(huskSync);
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync); Bukkit.getServer().getPluginManager().registerEvents(this, huskSync);
} }
@EventHandler(priority = EventPriority.LOWEST) @Override
public void onPlayerJoin(@NotNull PlayerJoinEvent event) { public boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority) {
super.handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer())); return plugin.getSettings().getEventPriority(type).equals(priority);
} }
@EventHandler(priority = EventPriority.LOWEST) @Override
public void onPlayerQuit(@NotNull PlayerQuitEvent event) { public void handlePlayerQuit(@NotNull BukkitPlayer player) {
super.handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer())); super.handlePlayerQuit(player);
} }
@EventHandler(ignoreCancelled = true) @Override
public void onWorldSave(@NotNull WorldSaveEvent event) { public void handlePlayerJoin(@NotNull BukkitPlayer player) {
// Handle saving player data snapshots when the world saves super.handlePlayerJoin(player);
if (!plugin.getSettings().saveOnWorldSave) return;
CompletableFuture.runAsync(() -> super.saveOnWorldSave(event.getWorld().getPlayers()
.stream().map(BukkitPlayer::adapt)
.collect(Collectors.toList())));
} }
@EventHandler(ignoreCancelled = true) @Override
public void onPlayerDeath(PlayerDeathEvent event) { public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
final OnlineUser user = BukkitPlayer.adapt(event.getEntity()); final OnlineUser user = BukkitPlayer.adapt(event.getEntity());
// If the player is locked or the plugin disabling, clear their drops // If the player is locked or the plugin disabling, clear their drops
if (cancelPlayerEvent(user)) { if (cancelPlayerEvent(user.uuid)) {
event.getDrops().clear(); event.getDrops().clear();
return; return;
} }
@@ -77,6 +73,16 @@ public class BukkitEventListener extends EventListener implements Listener {
.thenAccept(serializedDrops -> super.saveOnPlayerDeath(user, new ItemData(serializedDrops))); .thenAccept(serializedDrops -> super.saveOnPlayerDeath(user, new ItemData(serializedDrops)));
} }
@EventHandler(ignoreCancelled = true)
public void onWorldSave(@NotNull WorldSaveEvent event) {
// Handle saving player data snapshots when the world saves
if (!plugin.getSettings().saveOnWorldSave) return;
CompletableFuture.runAsync(() -> super.saveOnWorldSave(event.getWorld().getPlayers()
.stream().map(BukkitPlayer::adapt)
.collect(Collectors.toList())));
}
/* /*
* Events to cancel if the player has not been set yet * Events to cancel if the player has not been set yet
@@ -84,43 +90,47 @@ public class BukkitEventListener extends EventListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onDropItem(@NotNull PlayerDropItemEvent event) { public void onDropItem(@NotNull PlayerDropItemEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer()))); event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPickupItem(@NotNull EntityPickupItemEvent event) { public void onPickupItem(@NotNull EntityPickupItemEvent event) {
if (event.getEntity() instanceof Player player) { if (event.getEntity() instanceof Player player) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player))); event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
} }
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerInteract(@NotNull PlayerInteractEvent event) { public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer()))); event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockPlace(@NotNull BlockPlaceEvent event) { public void onBlockPlace(@NotNull BlockPlaceEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer()))); event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockBreak(@NotNull BlockBreakEvent event) { public void onBlockBreak(@NotNull BlockBreakEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer()))); event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryOpen(@NotNull InventoryOpenEvent event) { public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
if (event.getPlayer() instanceof Player player) { if (event.getPlayer() instanceof Player player) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player))); event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
} }
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryClick(@NotNull InventoryClickEvent event) {
event.setCancelled(cancelPlayerEvent(event.getWhoClicked().getUniqueId()));
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) { public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
if (event.getEntity() instanceof Player player) { if (event.getEntity() instanceof Player player) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player))); event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
} }
} }

View File

@@ -0,0 +1,38 @@
package net.william278.husksync.listener;
import net.william278.husksync.config.Settings;
import net.william278.husksync.player.BukkitPlayer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.jetbrains.annotations.NotNull;
public interface BukkitJoinEventListener extends Listener {
boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority);
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
default void onPlayerJoinHighest(@NotNull PlayerJoinEvent event) {
if (handleEvent(Settings.EventType.JOIN_LISTENER, Settings.EventPriority.HIGHEST)) {
handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
default void onPlayerJoin(@NotNull PlayerJoinEvent event) {
if (handleEvent(Settings.EventType.JOIN_LISTENER, Settings.EventPriority.NORMAL)) {
handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
default void onPlayerJoinLowest(@NotNull PlayerJoinEvent event) {
if (handleEvent(Settings.EventType.JOIN_LISTENER, Settings.EventPriority.LOWEST)) {
handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
}
}
void handlePlayerJoin(@NotNull BukkitPlayer player);
}

View File

@@ -0,0 +1,38 @@
package net.william278.husksync.listener;
import net.william278.husksync.config.Settings;
import net.william278.husksync.player.BukkitPlayer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
public interface BukkitQuitEventListener extends Listener {
boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority);
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
default void onPlayerQuitHighest(@NotNull PlayerQuitEvent event) {
if (handleEvent(Settings.EventType.QUIT_LISTENER, Settings.EventPriority.HIGHEST)) {
handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
default void onPlayerQuit(@NotNull PlayerQuitEvent event) {
if (handleEvent(Settings.EventType.QUIT_LISTENER, Settings.EventPriority.NORMAL)) {
handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
default void onPlayerQuitLowest(@NotNull PlayerQuitEvent event) {
if (handleEvent(Settings.EventType.QUIT_LISTENER, Settings.EventPriority.LOWEST)) {
handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
}
}
void handlePlayerQuit(@NotNull BukkitPlayer player);
}

View File

@@ -6,6 +6,8 @@ import dev.triumphteam.gui.builder.gui.StorageBuilder;
import dev.triumphteam.gui.guis.Gui; import dev.triumphteam.gui.guis.Gui;
import dev.triumphteam.gui.guis.StorageGui; import dev.triumphteam.gui.guis.StorageGui;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import net.roxeez.advancement.display.FrameType;
import net.william278.andjam.Toast;
import net.william278.desertwell.Version; import net.william278.desertwell.Version;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
@@ -88,8 +90,8 @@ public class BukkitPlayer extends OnlineUser {
@Override @Override
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull Settings settings) { public CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull Settings settings) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)) // Set max health
.getBaseValue(); double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.MAX_HEALTH)) { if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.MAX_HEALTH)) {
if (statusData.maxHealth != 0d) { if (statusData.maxHealth != 0d) {
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)) Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
@@ -98,22 +100,33 @@ public class BukkitPlayer extends OnlineUser {
} }
} }
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HEALTH)) { if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HEALTH)) {
// Set health
final double currentHealth = player.getHealth(); final double currentHealth = player.getHealth();
if (statusData.health != currentHealth) { if (statusData.health != currentHealth) {
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health; final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health;
if (healthToSet < 1) { final double maxHealth = currentMaxHealth;
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.setHealth(healthToSet)); Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
} else { try {
player.setHealth(healthToSet); player.setHealth(Math.min(healthToSet, maxHealth));
} } catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set health of player " + player.getName() + " to " + healthToSet);
}
});
} }
if (statusData.healthScale != 0d) { // Set health scale
player.setHealthScale(statusData.healthScale); try {
} else { if (statusData.healthScale != 0d) {
player.setHealthScale(statusData.maxHealth); player.setHealthScale(statusData.healthScale);
} else {
player.setHealthScale(statusData.maxHealth);
}
player.setHealthScaled(statusData.healthScale != 0D);
} catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set health scale of player " + player.getName() + " to " + statusData.healthScale);
} }
player.setHealthScaled(statusData.healthScale != 0D);
} }
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HUNGER)) { if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HUNGER)) {
player.setFoodLevel(statusData.hunger); player.setFoodLevel(statusData.hunger);
@@ -155,7 +168,9 @@ public class BukkitPlayer extends OnlineUser {
return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> { return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> {
final CompletableFuture<Void> inventorySetFuture = new CompletableFuture<>(); final CompletableFuture<Void> inventorySetFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
player.setItemOnCursor(null);
player.getInventory().setContents(contents.getContents()); player.getInventory().setContents(contents.getContents());
player.updateInventory();
inventorySetFuture.complete(null); inventorySetFuture.complete(null);
}); });
return inventorySetFuture.join(); return inventorySetFuture.join();
@@ -351,32 +366,52 @@ public class BukkitPlayer extends OnlineUser {
@Override @Override
public CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData) { public CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
// Set untyped statistics // Set generic statistics
for (String statistic : statisticsData.untypedStatistics.keySet()) { for (String statistic : statisticsData.untypedStatistics.keySet()) {
player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistics.get(statistic)); try {
player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistics.get(statistic));
} catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set generic statistic " + statistic + " for " + username);
}
} }
// Set block statistics // Set block statistics
for (String statistic : statisticsData.blockStatistics.keySet()) { for (String statistic : statisticsData.blockStatistics.keySet()) {
for (String blockMaterial : statisticsData.blockStatistics.get(statistic).keySet()) { for (String blockMaterial : statisticsData.blockStatistics.get(statistic).keySet()) {
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(blockMaterial), try {
statisticsData.blockStatistics.get(statistic).get(blockMaterial)); player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(blockMaterial),
statisticsData.blockStatistics.get(statistic).get(blockMaterial));
} catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set " + blockMaterial + " statistic " + statistic + " for " + username);
}
} }
} }
// Set item statistics // Set item statistics
for (String statistic : statisticsData.itemStatistics.keySet()) { for (String statistic : statisticsData.itemStatistics.keySet()) {
for (String itemMaterial : statisticsData.itemStatistics.get(statistic).keySet()) { for (String itemMaterial : statisticsData.itemStatistics.get(statistic).keySet()) {
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(itemMaterial), try {
statisticsData.itemStatistics.get(statistic).get(itemMaterial)); player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(itemMaterial),
statisticsData.itemStatistics.get(statistic).get(itemMaterial));
} catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set " + itemMaterial + " statistic " + statistic + " for " + username);
}
} }
} }
// Set entity statistics // Set entity statistics
for (String statistic : statisticsData.entityStatistics.keySet()) { for (String statistic : statisticsData.entityStatistics.keySet()) {
for (String entityType : statisticsData.entityStatistics.get(statistic).keySet()) { for (String entityType : statisticsData.entityStatistics.get(statistic).keySet()) {
player.setStatistic(Statistic.valueOf(statistic), EntityType.valueOf(entityType), try {
statisticsData.entityStatistics.get(statistic).get(entityType)); player.setStatistic(Statistic.valueOf(statistic), EntityType.valueOf(entityType),
statisticsData.entityStatistics.get(statistic).get(entityType));
} catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set " + entityType + " statistic " + statistic + " for " + username);
}
} }
} }
}); });
@@ -573,52 +608,52 @@ public class BukkitPlayer extends OnlineUser {
// Deserialize the item data to be shown and show it in a triumph GUI // Deserialize the item data to be shown and show it in a triumph GUI
BukkitSerializer.deserializeItemStackArray(itemData.serializedItems).thenAccept(items -> { BukkitSerializer.deserializeItemStackArray(itemData.serializedItems).thenAccept(items -> {
try { // Build the GUI and populate with items
// Build the GUI and populate with items final int itemCount = items.length;
final int itemCount = items.length; final StorageBuilder guiBuilder = Gui.storage()
final StorageBuilder guiBuilder = Gui.storage() .title(title.toComponent())
.title(title.toComponent()) .rows(Math.max(minimumRows, (int) Math.ceil(itemCount / 9.0)))
.rows(Math.max(minimumRows, (int) Math.ceil(itemCount / 9.0))) .disableAllInteractions()
.disableAllInteractions() .enableOtherActions();
.enableOtherActions(); final StorageGui gui = editable ? guiBuilder.enableAllInteractions().create() : guiBuilder.create();
final StorageGui gui = editable ? guiBuilder.enableAllInteractions().create() : guiBuilder.create(); for (int i = 0; i < itemCount; i++) {
for (int i = 0; i < itemCount; i++) { if (items[i] != null) {
if (items[i] != null) { gui.getInventory().setItem(i, items[i]);
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;
} }
// Complete the future with updated data (if editable) when the GUI is closed // Get and save the updated items
gui.setCloseGuiAction(event -> { final ItemStack[] updatedItems = Arrays.copyOf(event.getPlayer().getOpenInventory()
if (!editable) { .getTopInventory().getContents().clone(), itemCount);
BukkitSerializer.serializeItemStackArray(updatedItems).thenAccept(serializedItems -> {
if (serializedItems.equals(itemData.serializedItems)) {
updatedData.complete(Optional.empty()); updatedData.complete(Optional.empty());
return; return;
} }
updatedData.complete(Optional.of(new ItemData(serializedItems)));
// 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) // Display the GUI (synchronously; on the main server thread)
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> gui.open(player)); Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> gui.open(player));
} catch (Exception e) { }).exceptionally(throwable -> {
e.printStackTrace(); // Handle exceptions
} updatedData.completeExceptionally(throwable);
return null;
}); });
return updatedData; return updatedData;
} }
@Override @Override
public boolean isDead() { public boolean isDead() {
return player.getHealth() < 1; return player.getHealth() <= 0;
} }
@Override @Override
@@ -628,6 +663,23 @@ public class BukkitPlayer extends OnlineUser {
.replace().toComponent()); .replace().toComponent());
} }
@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.getInstance())
.setTitle(title.toComponent())
.setDescription(description.toComponent())
.setIcon(material != null ? material : Material.BARRIER)
.setFrameType(FrameType.valueOf(backgroundType))
.build()
.show(player);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override @Override
public void sendMessage(@NotNull MineDown mineDown) { public void sendMessage(@NotNull MineDown mineDown) {
audience.sendMessage(mineDown audience.sendMessage(mineDown
@@ -654,4 +706,9 @@ public class BukkitPlayer extends OnlineUser {
return maxHealth; return maxHealth;
} }
@Override
public boolean isLocked() {
return BukkitHuskSync.getInstance().getLockedPlayers().contains(player.getUniqueId());
}
} }

View File

@@ -13,7 +13,7 @@ public class BukkitLogger extends Logger {
} }
@Override @Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Exception e) { public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable e) {
logger.log(level, message, e); logger.log(level, message, e);
} }

View File

@@ -2,7 +2,7 @@ dependencies {
implementation 'commons-io:commons-io:2.11.0' implementation 'commons-io:commons-io:2.11.0'
implementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT' implementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
implementation 'net.kyori:adventure-api:4.11.0' implementation 'net.kyori:adventure-api:4.11.0'
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.10'
implementation 'dev.dejvokep:boosted-yaml:1.3' implementation 'dev.dejvokep:boosted-yaml:1.3'
implementation 'net.william278:Annotaml:2.0' implementation 'net.william278:Annotaml:2.0'
implementation 'net.william278:DesertWell:1.1' implementation 'net.william278:DesertWell:1.1'

View File

@@ -165,4 +165,6 @@ public interface HuskSync {
*/ */
CompletableFuture<Boolean> reload(); CompletableFuture<Boolean> reload();
Set<UUID> getLockedPlayers();
} }

View File

@@ -12,8 +12,10 @@ import org.jetbrains.annotations.NotNull;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class EnderChestCommand extends CommandBase implements TabCompletable { public class EnderChestCommand extends CommandBase implements TabCompletable {
@@ -44,9 +46,10 @@ public class EnderChestCommand extends CommandBase implements TabCompletable {
"/enderchest <player> [version_uuid]").ifPresent(player::sendMessage); "/enderchest <player> [version_uuid]").ifPresent(player::sendMessage);
} }
} else { } else {
// View latest user data // View (and edit) the latest user data
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse( plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
versionedUserData -> showEnderChestMenu(player, versionedUserData, user, true), versionedUserData -> showEnderChestMenu(player, versionedUserData, user,
player.hasPermission(Permission.COMMAND_ENDER_CHEST_EDIT.node)),
() -> plugin.getLocales().getLocale("error_no_data_to_display") () -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage))); .ifPresent(player::sendMessage)));
} }
@@ -67,28 +70,34 @@ public class EnderChestCommand extends CommandBase implements TabCompletable {
// Show inventory menu // Show inventory menu
player.showMenu(itemData, allowEdit, 3, plugin.getLocales() player.showMenu(itemData, allowEdit, 3, plugin.getLocales()
.getLocale("ender_chest_viewer_menu_title", dataOwner.username) .getLocale("ender_chest_viewer_menu_title", dataOwner.username)
.orElse(new MineDown("Ender Chest Viewer"))).thenAccept(dataOnClose -> { .orElse(new MineDown("Ender Chest Viewer")))
if (dataOnClose.isEmpty() || !allowEdit) { .exceptionally(throwable -> {
return; plugin.getLoggingAdapter().log(Level.WARNING, "Exception displaying inventory menu to " + player.username, throwable);
} return Optional.empty();
})
.thenAccept(dataOnClose -> {
if (dataOnClose.isEmpty() || !allowEdit) {
return;
}
// Create the updated data // Create the updated data
final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion()); final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion());
data.getStatus().ifPresent(builder::setStatus); data.getStatus().ifPresent(builder::setStatus);
data.getAdvancements().ifPresent(builder::setAdvancements); data.getAdvancements().ifPresent(builder::setAdvancements);
data.getLocation().ifPresent(builder::setLocation); data.getLocation().ifPresent(builder::setLocation);
data.getPersistentDataContainer().ifPresent(builder::setPersistentDataContainer); data.getPersistentDataContainer().ifPresent(builder::setPersistentDataContainer);
data.getStatistics().ifPresent(builder::setStatistics); data.getStatistics().ifPresent(builder::setStatistics);
data.getPotionEffects().ifPresent(builder::setPotionEffects); data.getPotionEffects().ifPresent(builder::setPotionEffects);
data.getInventory().ifPresent(builder::setInventory); data.getInventory().ifPresent(builder::setInventory);
builder.setEnderChest(dataOnClose.get()); builder.setEnderChest(dataOnClose.get());
// Set the updated data // Set the updated data
final UserData updatedUserData = builder.build(); final UserData updatedUserData = builder.build();
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join(); plugin.getDatabase()
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join(); .setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND)
}); .thenRun(() -> plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData));
});
}); });
}); });
} }

View File

@@ -12,8 +12,10 @@ import org.jetbrains.annotations.NotNull;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class InventoryCommand extends CommandBase implements TabCompletable { public class InventoryCommand extends CommandBase implements TabCompletable {
@@ -44,9 +46,10 @@ public class InventoryCommand extends CommandBase implements TabCompletable {
"/inventory <player> [version_uuid]").ifPresent(player::sendMessage); "/inventory <player> [version_uuid]").ifPresent(player::sendMessage);
} }
} else { } else {
// View latest user data // View (and edit) the latest user data
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse( plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
versionedUserData -> showInventoryMenu(player, versionedUserData, user, true), versionedUserData -> showInventoryMenu(player, versionedUserData, user,
player.hasPermission(Permission.COMMAND_INVENTORY_EDIT.node)),
() -> plugin.getLocales().getLocale("error_no_data_to_display") () -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage))); .ifPresent(player::sendMessage)));
} }
@@ -69,13 +72,15 @@ public class InventoryCommand extends CommandBase implements TabCompletable {
player.showMenu(itemData, allowEdit, 5, plugin.getLocales() player.showMenu(itemData, allowEdit, 5, plugin.getLocales()
.getLocale("inventory_viewer_menu_title", dataOwner.username) .getLocale("inventory_viewer_menu_title", dataOwner.username)
.orElse(new MineDown("Inventory Viewer"))) .orElse(new MineDown("Inventory Viewer")))
.exceptionally(throwable -> {
plugin.getLoggingAdapter().log(Level.WARNING, "Exception displaying inventory menu to " + player.username, throwable);
return Optional.empty();
})
.thenAccept(dataOnClose -> { .thenAccept(dataOnClose -> {
if (dataOnClose.isEmpty() || !allowEdit) { if (dataOnClose.isEmpty() || !allowEdit) {
return; return;
} }
plugin.getLoggingAdapter().debug("Inventory data changed, updating user, etc!");
// Create the updated data // Create the updated data
final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion()); final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion());
data.getStatus().ifPresent(builder::setStatus); data.getStatus().ifPresent(builder::setStatus);
@@ -89,8 +94,9 @@ public class InventoryCommand extends CommandBase implements TabCompletable {
// Set the updated data // Set the updated data
final UserData updatedUserData = builder.build(); final UserData updatedUserData = builder.build();
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join(); plugin.getDatabase()
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join(); .setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND)
.thenRun(() -> plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData));
}); });
}); });
}); });

View File

@@ -121,6 +121,21 @@ public class Locales {
return value.toString(); return value.toString();
} }
/**
* Truncates a String to a specified length, and appends an ellipsis if it is longer than the specified length
*
* @param string The string to truncate
* @param length The maximum length of the string
* @return The truncated string
*/
@NotNull
public static String truncate(@NotNull String string, int length) {
if (string.length() > length) {
return string.substring(0, length) + "";
}
return string;
}
/** /**
* Returns the base list options to use for a paginated chat list * Returns the base list options to use for a paginated chat list
* *

View File

@@ -7,7 +7,6 @@ import org.jetbrains.annotations.NotNull;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.Optional;
/** /**
* Plugin settings, read from config.yml * Plugin settings, read from config.yml
@@ -19,8 +18,7 @@ import java.util.Optional;
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┣╸ Information: https://william278.net/project/husksync ┣╸ Information: https://william278.net/project/husksync
┗╸ Documentation: https://william278.net/docs/husksync""", ┗╸ Documentation: https://william278.net/docs/husksync""",
versionField = "config_version", versionNumber = 3)
versionField = "config_version", versionNumber = 2)
public class Settings { public class Settings {
// Top-level settings // Top-level settings
@@ -47,7 +45,7 @@ public class Settings {
@YamlKey("database.credentials.database") @YamlKey("database.credentials.database")
public String mySqlDatabase = "HuskSync"; public String mySqlDatabase = "HuskSync";
@YamlKey("database.mysql.credentials.username") @YamlKey("database.credentials.username")
public String mySqlUsername = "root"; public String mySqlUsername = "root";
@YamlKey("database.credentials.password") @YamlKey("database.credentials.password")
@@ -77,8 +75,7 @@ public class Settings {
@NotNull @NotNull
public String getTableName(@NotNull TableName tableName) { public String getTableName(@NotNull TableName tableName) {
return Optional.ofNullable(tableNames.get(tableName.name().toLowerCase())) return tableNames.getOrDefault(tableName.name().toLowerCase(), tableName.defaultName);
.orElse(tableName.defaultName);
} }
@@ -111,6 +108,9 @@ public class Settings {
@YamlKey("synchronization.compress_data") @YamlKey("synchronization.compress_data")
public boolean compressData = true; public boolean compressData = true;
@YamlKey("synchronization.notification_display_slot")
public NotificationDisplaySlot notificationDisplaySlot = NotificationDisplaySlot.ACTION_BAR;
@YamlKey("synchronization.save_dead_player_inventories") @YamlKey("synchronization.save_dead_player_inventories")
public boolean saveDeadPlayerInventories = true; public boolean saveDeadPlayerInventories = true;
@@ -121,8 +121,20 @@ public class Settings {
public Map<String, Boolean> synchronizationFeatures = SynchronizationFeature.getDefaults(); public Map<String, Boolean> synchronizationFeatures = SynchronizationFeature.getDefaults();
public boolean getSynchronizationFeature(@NotNull SynchronizationFeature feature) { public boolean getSynchronizationFeature(@NotNull SynchronizationFeature feature) {
return Optional.ofNullable(synchronizationFeatures.get(feature.name().toLowerCase())) return synchronizationFeatures.getOrDefault(feature.name().toLowerCase(), feature.enabledByDefault);
.orElse(feature.enabledByDefault); }
@YamlKey("synchronization.event_priorities")
public Map<String, String> synchronizationEventPriorities = EventType.getDefaults();
@NotNull
public EventPriority getEventPriority(@NotNull Settings.EventType eventType) {
try {
return EventPriority.valueOf(synchronizationEventPriorities.get(eventType.name().toLowerCase()));
} catch (IllegalArgumentException e) {
e.printStackTrace();
return EventPriority.NORMAL;
}
} }
@@ -139,11 +151,13 @@ public class Settings {
this.defaultName = defaultName; this.defaultName = defaultName;
} }
@NotNull
private Map.Entry<String, String> toEntry() { private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(), defaultName); return Map.entry(name().toLowerCase(), defaultName);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@NotNull
private static Map<String, String> getDefaults() { private static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values()) return Map.ofEntries(Arrays.stream(values())
.map(TableName::toEntry) .map(TableName::toEntry)
@@ -151,11 +165,32 @@ public class Settings {
} }
} }
/**
* Determines the slot a system notification should be displayed in
*/
public enum NotificationDisplaySlot {
/**
* Displays the notification in the action bar
*/
ACTION_BAR,
/**
* Displays the notification in the chat
*/
CHAT,
/**
* Displays the notification in an advancement toast
*/
TOAST,
/**
* Does not display the notification
*/
NONE
}
/** /**
* Represents enabled synchronisation features * Represents enabled synchronisation features
*/ */
public enum SynchronizationFeature { public enum SynchronizationFeature {
INVENTORIES(true), INVENTORIES(true),
ENDER_CHESTS(true), ENDER_CHESTS(true),
HEALTH(true), HEALTH(true),
@@ -167,6 +202,7 @@ public class Settings {
GAME_MODE(true), GAME_MODE(true),
STATISTICS(true), STATISTICS(true),
PERSISTENT_DATA_CONTAINER(false), PERSISTENT_DATA_CONTAINER(false),
LOCKED_MAPS(false),
LOCATION(false); LOCATION(false);
private final boolean enabledByDefault; private final boolean enabledByDefault;
@@ -175,11 +211,13 @@ public class Settings {
this.enabledByDefault = enabledByDefault; this.enabledByDefault = enabledByDefault;
} }
@NotNull
private Map.Entry<String, Boolean> toEntry() { private Map.Entry<String, Boolean> toEntry() {
return Map.entry(name().toLowerCase(), enabledByDefault); return Map.entry(name().toLowerCase(), enabledByDefault);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@NotNull
private static Map<String, Boolean> getDefaults() { private static Map<String, Boolean> getDefaults() {
return Map.ofEntries(Arrays.stream(values()) return Map.ofEntries(Arrays.stream(values())
.map(SynchronizationFeature::toEntry) .map(SynchronizationFeature::toEntry)
@@ -187,4 +225,51 @@ public class Settings {
} }
} }
/**
* Represents events that HuskSync listens to, with a configurable priority listener
*/
public enum EventType {
JOIN_LISTENER(EventPriority.LOWEST),
QUIT_LISTENER(EventPriority.LOWEST),
DEATH_LISTENER(EventPriority.NORMAL);
private final EventPriority defaultPriority;
EventType(@NotNull EventPriority defaultPriority) {
this.defaultPriority = defaultPriority;
}
@NotNull
private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(), defaultPriority.name());
}
@SuppressWarnings("unchecked")
@NotNull
private static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values())
.map(EventType::toEntry)
.toArray(Map.Entry[]::new));
}
}
/**
* Represents priorities for events that HuskSync listens to
*/
public enum EventPriority {
/**
* Listens and processes the event execution last
*/
HIGHEST,
/**
* Listens in between {@link #HIGHEST} and {@link #LOWEST} priority marked
*/
NORMAL,
/**
* Listens and processes the event execution first
*/
LOWEST
}
} }

View File

@@ -1,5 +1,6 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import net.william278.husksync.config.Locales;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.api.BaseHuskSyncAPI; import net.william278.husksync.api.BaseHuskSyncAPI;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
@@ -100,4 +101,9 @@ public enum DataSaveCause {
return UNKNOWN; return UNKNOWN;
} }
@NotNull
public String getDisplayName() {
return Locales.truncate(name().toLowerCase(), 10);
}
} }

View File

@@ -11,7 +11,7 @@ import java.util.Map;
public class StatisticsData { public class StatisticsData {
/** /**
* Map of untyped statistic names to their values * Map of generic statistic names to their values
*/ */
@SerializedName("untyped_statistics") @SerializedName("untyped_statistics")
public Map<String, Integer> untypedStatistics; public Map<String, Integer> untypedStatistics;

View File

@@ -1,5 +1,6 @@
package net.william278.husksync.listener; package net.william278.husksync.listener;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.ItemData; import net.william278.husksync.data.ItemData;
@@ -121,9 +122,17 @@ public abstract class EventListener {
*/ */
private void handleSynchronisationCompletion(@NotNull OnlineUser user, boolean succeeded) { private void handleSynchronisationCompletion(@NotNull OnlineUser user, boolean succeeded) {
if (succeeded) { if (succeeded) {
plugin.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar); switch (plugin.getSettings().notificationDisplaySlot) {
lockedPlayers.remove(user.uuid); case CHAT -> plugin.getLocales().getLocale("synchronisation_complete")
.ifPresent(user::sendMessage);
case ACTION_BAR -> plugin.getLocales().getLocale("synchronisation_complete")
.ifPresent(user::sendActionBar);
case TOAST -> plugin.getLocales().getLocale("synchronisation_complete")
.ifPresent(locale -> user.sendToast(locale, new MineDown(""),
"minecraft:bell", "TASK"));
}
plugin.getDatabase().ensureUser(user).join(); plugin.getDatabase().ensureUser(user).join();
lockedPlayers.remove(user.uuid);
plugin.getEventCannon().fireSyncCompleteEvent(user); plugin.getEventCannon().fireSyncCompleteEvent(user);
} else { } else {
plugin.getLocales().getLocale("synchronisation_failed") plugin.getLocales().getLocale("synchronisation_failed")
@@ -154,7 +163,7 @@ public abstract class EventListener {
optionalUserData -> optionalUserData.ifPresent(userData -> plugin.getRedisManager() optionalUserData -> optionalUserData.ifPresent(userData -> plugin.getRedisManager()
.setUserData(user, userData).thenRun(() -> plugin.getDatabase() .setUserData(user, userData).thenRun(() -> plugin.getDatabase()
.setUserData(user, userData, DataSaveCause.DISCONNECT))))) .setUserData(user, userData, DataSaveCause.DISCONNECT)))))
.thenRun(() -> lockedPlayers.remove(user.uuid)).exceptionally(throwable -> { .exceptionally(throwable -> {
plugin.getLoggingAdapter().log(Level.SEVERE, plugin.getLoggingAdapter().log(Level.SEVERE,
"An exception occurred handling a player disconnection"); "An exception occurred handling a player disconnection");
throwable.printStackTrace(); throwable.printStackTrace();
@@ -171,8 +180,11 @@ public abstract class EventListener {
if (disabling || !plugin.getSettings().saveOnWorldSave) { if (disabling || !plugin.getSettings().saveOnWorldSave) {
return; return;
} }
usersInWorld.forEach(user -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join().ifPresent( usersInWorld.stream()
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.WORLD_SAVE).join())); .filter(user -> !lockedPlayers.contains(user.uuid))
.forEach(user -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings())
.thenAccept(data -> data.ifPresent(userData -> plugin.getDatabase()
.setUserData(user, userData, DataSaveCause.WORLD_SAVE))));
} }
/** /**
@@ -182,7 +194,7 @@ public abstract class EventListener {
* @param drops The items that this user would have dropped * @param drops The items that this user would have dropped
*/ */
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull ItemData drops) { protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull ItemData drops) {
if (disabling || !plugin.getSettings().saveOnDeath) { if (disabling || !plugin.getSettings().saveOnDeath || lockedPlayers.contains(user.uuid)) {
return; return;
} }
@@ -196,11 +208,11 @@ public abstract class EventListener {
/** /**
* Determine whether a player event should be cancelled * Determine whether a player event should be cancelled
* *
* @param user {@link OnlineUser} performing the event * @param userUuid The UUID of the user to check
* @return Whether the event should be cancelled * @return Whether the event should be cancelled
*/ */
protected final boolean cancelPlayerEvent(@NotNull OnlineUser user) { protected final boolean cancelPlayerEvent(@NotNull UUID userUuid) {
return disabling || lockedPlayers.contains(user.uuid); return disabling || lockedPlayers.contains(userUuid);
} }
/** /**
@@ -209,12 +221,23 @@ public abstract class EventListener {
public final void handlePluginDisable() { public final void handlePluginDisable() {
disabling = true; disabling = true;
plugin.getOnlineUsers().stream().filter(user -> !lockedPlayers.contains(user.uuid)).forEach( // Save data for all online users
user -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join().ifPresent( plugin.getOnlineUsers().stream()
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.SERVER_SHUTDOWN).join())); .filter(user -> !lockedPlayers.contains(user.uuid))
.forEach(user -> {
lockedPlayers.add(user.uuid);
user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join()
.ifPresent(userData -> plugin.getDatabase()
.setUserData(user, userData, DataSaveCause.SERVER_SHUTDOWN).join());
});
// Close outstanding connections
plugin.getDatabase().close(); plugin.getDatabase().close();
plugin.getRedisManager().close(); plugin.getRedisManager().close();
} }
public final Set<UUID> getLockedPlayers() {
return this.lockedPlayers;
}
} }

View File

@@ -198,6 +198,17 @@ public abstract class OnlineUser extends User {
*/ */
public abstract void sendActionBar(@NotNull MineDown mineDown); public abstract void sendActionBar(@NotNull MineDown mineDown);
/**
* 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 * Returns if the player has the permission node
* *
@@ -246,15 +257,15 @@ public abstract class OnlineUser extends User {
// Prevent synchronising user data from newer versions of Minecraft // Prevent synchronising user data from newer versions of Minecraft
if (Version.fromMinecraftVersionString(data.getMinecraftVersion()).compareTo(serverMinecraftVersion) > 0) { if (Version.fromMinecraftVersionString(data.getMinecraftVersion()).compareTo(serverMinecraftVersion) > 0) {
logger.log(Level.SEVERE, "Cannot set data for " + username + logger.log(Level.SEVERE, "Cannot set data for " + username +
" because the Minecraft version of their user data (" + data.getMinecraftVersion() + " because the Minecraft version of their user data (" + data.getMinecraftVersion() +
") is newer than the server's Minecraft version (" + serverMinecraftVersion + ")."); ") is newer than the server's Minecraft version (" + serverMinecraftVersion + ").");
return false; return false;
} }
// Prevent synchronising user data from newer versions of the plugin // Prevent synchronising user data from newer versions of the plugin
if (data.getFormatVersion() > UserData.CURRENT_FORMAT_VERSION) { if (data.getFormatVersion() > UserData.CURRENT_FORMAT_VERSION) {
logger.log(Level.SEVERE, "Cannot set data for " + username + logger.log(Level.SEVERE, "Cannot set data for " + username +
" because the format version of their user data (v" + data.getFormatVersion() + " because the format version of their user data (v" + data.getFormatVersion() +
") is newer than the current format version (v" + UserData.CURRENT_FORMAT_VERSION + ")."); ") is newer than the current format version (v" + UserData.CURRENT_FORMAT_VERSION + ").");
return false; return false;
} }
@@ -321,6 +332,7 @@ public abstract class OnlineUser extends User {
if (!isOffline()) { if (!isOffline()) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) { if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
if (isDead() && settings.saveDeadPlayerInventories) { if (isDead() && settings.saveDeadPlayerInventories) {
logger.debug("Player " + username + " is dead, so their inventory will be set to empty.");
add(CompletableFuture.runAsync(() -> builder.setInventory(ItemData.empty()))); add(CompletableFuture.runAsync(() -> builder.setInventory(ItemData.empty())));
} else { } else {
add(getInventory().thenAccept(builder::setInventory)); add(getInventory().thenAccept(builder::setInventory));
@@ -359,4 +371,10 @@ public abstract class OnlineUser extends User {
}); });
} }
/**
* Get if the player is locked
*
* @return the player's locked status
*/
public abstract boolean isLocked();
} }

View File

@@ -1,5 +1,6 @@
package net.william278.husksync.redis; package net.william278.husksync.redis;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.UserData; import net.william278.husksync.data.UserData;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
@@ -18,7 +19,7 @@ import java.util.concurrent.CompletableFuture;
/** /**
* Manages the connection to the Redis server, handling the caching of user data * Manages the connection to the Redis server, handling the caching of user data
*/ */
public class RedisManager { public class RedisManager extends JedisPubSub {
protected static final String KEY_NAMESPACE = "husksync:"; protected static final String KEY_NAMESPACE = "husksync:";
protected static String clusterId = ""; protected static String clusterId = "";
@@ -52,21 +53,19 @@ public class RedisManager {
* *
* @return a future returning void when complete * @return a future returning void when complete
*/ */
public CompletableFuture<Boolean> initialize() { public boolean initialize() {
return CompletableFuture.supplyAsync(() -> { if (redisPassword.isBlank()) {
if (redisPassword.isBlank()) { jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisUseSsl);
jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisUseSsl); } else {
} else { jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisPassword, redisUseSsl);
jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisPassword, redisUseSsl); }
} try {
try { jedisPool.getResource().ping();
jedisPool.getResource().ping(); } catch (JedisException e) {
} catch (JedisException e) { return false;
return false; }
} CompletableFuture.runAsync(this::subscribe);
CompletableFuture.runAsync(this::subscribe); return true;
return true;
});
} }
private void subscribe() { private void subscribe() {
@@ -74,33 +73,43 @@ public class RedisManager {
new Jedis(redisHost, redisPort, DefaultJedisClientConfig.builder() new Jedis(redisHost, redisPort, DefaultJedisClientConfig.builder()
.password(redisPassword).timeoutMillis(0).ssl(redisUseSsl).build())) { .password(redisPassword).timeoutMillis(0).ssl(redisUseSsl).build())) {
subscriber.connect(); subscriber.connect();
subscriber.subscribe(new JedisPubSub() { subscriber.subscribe(this, Arrays.stream(RedisMessageType.values())
@Override .map(RedisMessageType::getMessageChannel)
public void onMessage(@NotNull String channel, @NotNull String message) { .toArray(String[]::new));
RedisMessageType.getTypeFromChannel(channel).ifPresent(messageType -> {
if (messageType == RedisMessageType.UPDATE_USER_DATA) {
final RedisMessage redisMessage = RedisMessage.fromJson(message);
plugin.getOnlineUser(redisMessage.targetUserUuid).ifPresent(user -> {
final UserData userData = plugin.getDataAdapter().fromBytes(redisMessage.data);
user.setData(userData, plugin.getSettings(), plugin.getEventCannon(),
plugin.getLoggingAdapter(), plugin.getMinecraftVersion()).thenAccept(succeeded -> {
if (succeeded) {
plugin.getLocales().getLocale("data_update_complete")
.ifPresent(user::sendActionBar);
plugin.getEventCannon().fireSyncCompleteEvent(user);
} else {
plugin.getLocales().getLocale("data_update_failed")
.ifPresent(user::sendMessage);
}
});
});
}
});
}
}, Arrays.stream(RedisMessageType.values()).map(RedisMessageType::getMessageChannel).toArray(String[]::new));
} }
} }
@Override
public void onMessage(@NotNull String channel, @NotNull String message) {
final RedisMessageType messageType = RedisMessageType.getTypeFromChannel(channel).orElse(null);
if (messageType != RedisMessageType.UPDATE_USER_DATA) {
return;
}
final RedisMessage redisMessage = RedisMessage.fromJson(message);
plugin.getOnlineUser(redisMessage.targetUserUuid).ifPresent(user -> {
final UserData userData = plugin.getDataAdapter().fromBytes(redisMessage.data);
user.setData(userData, plugin.getSettings(), plugin.getEventCannon(),
plugin.getLoggingAdapter(), plugin.getMinecraftVersion()).thenAccept(succeeded -> {
if (succeeded) {
switch (plugin.getSettings().notificationDisplaySlot) {
case CHAT -> plugin.getLocales().getLocale("data_update_complete")
.ifPresent(user::sendMessage);
case ACTION_BAR -> plugin.getLocales().getLocale("data_update_complete")
.ifPresent(user::sendActionBar);
case TOAST -> plugin.getLocales().getLocale("data_update_complete")
.ifPresent(locale -> user.sendToast(locale, new MineDown(""),
"minecraft:bell", "TASK"));
}
plugin.getEventCannon().fireSyncCompleteEvent(user);
} else {
plugin.getLocales().getLocale("data_update_failed")
.ifPresent(user::sendMessage);
}
});
});
}
protected void sendMessage(@NotNull String channel, @NotNull String message) { protected void sendMessage(@NotNull String channel, @NotNull String message) {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
jedis.publish(channel, message); jedis.publish(channel, message);

View File

@@ -32,7 +32,7 @@ public class DataSnapshotList {
.format(snapshot.versionTimestamp()), .format(snapshot.versionTimestamp()),
snapshot.versionUUID().toString().split("-")[0], snapshot.versionUUID().toString().split("-")[0],
snapshot.versionUUID().toString(), snapshot.versionUUID().toString(),
snapshot.cause().name().toLowerCase().replaceAll("_", " "), snapshot.cause().getDisplayName(),
dataOwner.username, dataOwner.username,
snapshot.pinned() ? "" : " ") snapshot.pinned() ? "" : " ")
.orElse("" + snapshot.versionUUID())).toList(), .orElse("" + snapshot.versionUUID())).toList(),

View File

@@ -11,7 +11,7 @@ public abstract class Logger {
private boolean debug; private boolean debug;
public abstract void log(@NotNull Level level, @NotNull String message, @NotNull Exception e); public abstract void log(@NotNull Level level, @NotNull String message, @NotNull Throwable e);
public abstract void log(@NotNull Level level, @NotNull String message); public abstract void log(@NotNull Level level, @NotNull String message);

View File

@@ -22,20 +22,20 @@ data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text
data_manager_advancements_statistics: '[⭐ Erfolge: %1%](color=#ffc43b-#f5c962 show_text=&7Erfolge in denen du Fortschritt gemacht hast:\n&8%2%) [⌛ Spielzeit: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Deine verbrachte Zeit im Spiel\n&8⚠ Basierend auf Spielstatistiken)\n' data_manager_advancements_statistics: '[⭐ Erfolge: %1%](color=#ffc43b-#f5c962 show_text=&7Erfolge in denen du Fortschritt gemacht hast:\n&8%2%) [⌛ Spielzeit: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Deine verbrachte Zeit im Spiel\n&8⚠ Basierend auf Spielstatistiken)\n'
data_manager_item_buttons: '[View:](gray) [[🪣 Inventar…]](color=#a17b5f-#f5b98c show_text=&7Klicke zum Ansehen run_command=/inventory %1% %2%) [[⌀ Endertruhe…]](#b649c4-#d254ff show_text=&7Klicke zum Ansehen run_command=/enderchest %1% %2%)' data_manager_item_buttons: '[View:](gray) [[🪣 Inventar…]](color=#a17b5f-#f5b98c show_text=&7Klicke zum Ansehen run_command=/inventory %1% %2%) [[⌀ Endertruhe…]](#b649c4-#d254ff show_text=&7Klicke zum Ansehen run_command=/enderchest %1% %2%)'
data_manager_management_buttons: '[Verwalte:](gray) [[❌ Löschen…]](#ff3300 show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss zu löschen.\n&8Dies betrifft nicht die aktuellen Nutzerdaten.\n&#ff3300&⚠ Dieser Schritt kann nicht rückgängig gemacht werden! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Wiederherstellen…]](#00fb9a show_text=&7Klicke, um die Nutzerdaten wiederherzustellen.\n&8Dies wird die Nutzerdaten auf den Stand des Schnappschusses setzen.\n&#ff3300&⚠ Die aktuellen Nutzerdaten von %1% werden überschrieben! suggest_command=/husksync:userdata restore %1% %2%) [[※ Anheften/Loslösen…]](#d8ff2b show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss anzuheften oder loszulösen\n&8Angeheftete Nutzerdaten-Schnappschüsse werden nicht automatisch rotiert run_command=/userdata pin %1% %2%)' data_manager_management_buttons: '[Verwalte:](gray) [[❌ Löschen…]](#ff3300 show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss zu löschen.\n&8Dies betrifft nicht die aktuellen Nutzerdaten.\n&#ff3300&⚠ Dieser Schritt kann nicht rückgängig gemacht werden! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Wiederherstellen…]](#00fb9a show_text=&7Klicke, um die Nutzerdaten wiederherzustellen.\n&8Dies wird die Nutzerdaten auf den Stand des Schnappschusses setzen.\n&#ff3300&⚠ Die aktuellen Nutzerdaten von %1% werden überschrieben! suggest_command=/husksync:userdata restore %1% %2%) [[※ Anheften/Loslösen…]](#d8ff2b show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss anzuheften oder loszulösen\n&8Angeheftete Nutzerdaten-Schnappschüsse werden nicht automatisch rotiert run_command=/userdata pin %1% %2%)'
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)' data_manager_system_buttons: '[System:](gray) [[⏷ Daten-Dump…]](dark_gray show_text=&7Klicke, um diesen rohen Nutzerdaten-Schnappschuss in eine Datei zu speichern.\n&8Daten-Dumps können unter ~/plugins/HuskSync/dumps/ gefunden werden. run_command=/husksync:userdata dump %1% %2% file) [[☂ Web-Dump…]](dark_gray show_text=&7Klicke, um diesen rohen Nutzerdaten-Schnappschuss auf den mc-logs Service hochzuladen.\n&8Du erhältst dann eine URL, die die Daten enthält. run_command=/husksync:userdata dump %1% %2% web)'
data_manager_advancements_preview_remaining: '&7und %1% weitere…' data_manager_advancements_preview_remaining: '&7und %1% weitere…'
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n' data_list_title: '[Nutzerdaten-Schnappschüsse von %1%:](#00fb9a) [(%2%-%3% von](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7Daten-Schnappschuss %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Angeheftet:\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\n&8Zeitpunkt der Speicherung der Daten run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Versions-UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Der Grund für das Speichern der Daten run_command=/userdata view %6% %4%)' data_list_item: '[%1%](gray show_text=&7Daten-Schnappschuss %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Angeheftet:\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\n&8Zeitpunkt der Speicherung der Daten run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Versions-UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Der Grund für das Speichern der Daten run_command=/userdata view %6% %4%)'
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)' data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)' data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)'
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)' data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
data_unpinned: '[※ Nutzerdaten-Schnappschuss erfolgreich losgelöst](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)' data_unpinned: '[※ Nutzerdaten-Schnappschuss erfolgreich losgelöst](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%' data_dumped: '[☂ Nutzerdaten-Schnappschuss %1% für %2% erfolgreich gedumpt nach:](#00fb9a) &7%3%'
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%' list_footer: '\n%1%[Seite](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) ' list_previous_page_button: '[◀](white show_text=&7Siehe vorherige Seite run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)' list_next_page_button: ' [▶](white show_text=&7Siehe nächste Seite run_command=%2% %1%)'
list_page_jumpers: '(%1%)' list_page_jumpers: '(%1%)'
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)' list_page_jumper_button: '[%1%](show_text=&7Springe zu Seite %1% run_command=%2% %1%)'
list_page_jumper_current_page: '[%1%](#00fb9a)' list_page_jumper_current_page: '[%1%](#00fb9a)'
list_page_jumper_separator: ' ' list_page_jumper_separator: ' '
list_page_jumper_group_separator: '…' list_page_jumper_group_separator: '…'

View File

@@ -22,20 +22,20 @@ data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text
data_manager_advancements_statistics: '[⭐ Progressi: %1%](color=#ffc43b-#f5c962 show_text=&7Progressi compiuti in:\n&8%2%) [⌛ Tempo di gioco: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo di gioco\n&8⚠ Basato sulle statistiche di gioco)\n' data_manager_advancements_statistics: '[⭐ Progressi: %1%](color=#ffc43b-#f5c962 show_text=&7Progressi compiuti in:\n&8%2%) [⌛ Tempo di gioco: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo di gioco\n&8⚠ Basato sulle statistiche di gioco)\n'
data_manager_item_buttons: '[View:](gray) [[🪣 Inventario…]](color=#a17b5f-#f5b98c show_text=&7Clicca per visualizzare run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Clicca per visualizzare run_command=/enderchest %1% %2%)' data_manager_item_buttons: '[View:](gray) [[🪣 Inventario…]](color=#a17b5f-#f5b98c show_text=&7Clicca per visualizzare run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Clicca per visualizzare run_command=/enderchest %1% %2%)'
data_manager_management_buttons: '[Gestisci:](gray) [[❌ Cancella…]](#ff3300 show_text=&7Fare clic per eliminare questa istantanea.\n&8Questo non influisce sui dati attuali dell''utente.\n&#ff3300&⚠ Questo non può essere annullato! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Ripristina…]](#00fb9a show_text=&7Clicca per ripristinare i dati dell''utente.\n&8I dati dell''utente saranno ripristinati a quest''istantanea.\n&#ff3300&⚠ I dati di %1% saranno sovrascritti! suggest_command=/husksync:userdata restore %1% %2%) [[※ fissa/sblocca...]](#d8ff2b show_text=&7Clicca per fissare o sbloccare quest''istantanea\n&8Le istantanee fissate non saranno cancellate automaticamente run_command=/userdata pin %1% %2%)' data_manager_management_buttons: '[Gestisci:](gray) [[❌ Cancella…]](#ff3300 show_text=&7Fare clic per eliminare questa istantanea.\n&8Questo non influisce sui dati attuali dell''utente.\n&#ff3300&⚠ Questo non può essere annullato! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Ripristina…]](#00fb9a show_text=&7Clicca per ripristinare i dati dell''utente.\n&8I dati dell''utente saranno ripristinati a quest''istantanea.\n&#ff3300&⚠ I dati di %1% saranno sovrascritti! suggest_command=/husksync:userdata restore %1% %2%) [[※ fissa/sblocca...]](#d8ff2b show_text=&7Clicca per fissare o sbloccare quest''istantanea\n&8Le istantanee fissate non saranno cancellate automaticamente run_command=/userdata pin %1% %2%)'
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)' data_manager_system_buttons: '[Sistema:](gray) [[⏷ Dump del File…]](dark_gray show_text=&7Clicca per ottenere il dump dei dati del giocatore.\n&8I dati salvati sono posizioanti nella cartella ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Dump su Web…]](dark_gray show_text=&7Clicca per ottenere il dump del file su mc-logs\n&8 Ti verrà consegnato l''url per visionare il dump. run_command=/husksync:userdata dump %1% %2% web)'
data_manager_advancements_preview_remaining: '&7e %1% altro…' data_manager_advancements_preview_remaining: '&7e %1% altro…'
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n' data_list_title: '[Lista delle istantanee di %1%:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7Istantanea dei dati %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Fissato:\n&8Le istantanee fissate non saranno cancellate automaticamente. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Timestamp della versione:&7\n&8Quando i dati sono stati salvati run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Versione di UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Causa del salvataggio:\n&8Cosa ha causato il salvataggio dei dati run_command=/userdata view %6% %4%)' data_list_item: '[%1%](gray show_text=&7Istantanea dei dati %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Fissato:\n&8Le istantanee fissate non saranno cancellate automaticamente. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Timestamp della versione:&7\n&8Quando i dati sono stati salvati run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Versione di UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Causa del salvataggio:\n&8Cosa ha causato il salvataggio dei dati run_command=/userdata view %6% %4%)'
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)' data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)' data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)'
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)' data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_unpinned: '[※ L''istantanea dei dati utente è stata sbloccata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)' data_unpinned: '[※ L''istantanea dei dati utente è stata sbloccata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%' data_dumped: '[☂ Hai ottenuto il dump dell''istantanea %1% di %2% nel formato:](#00fb9a) &7%3%'
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%' list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) ' list_previous_page_button: '[◀](white show_text=&7Visualizza pagina precedente run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)' list_next_page_button: ' [▶](white show_text=&7Visualizza pagina successiva run_command=%2% %1%)'
list_page_jumpers: '(%1%)' list_page_jumpers: '(%1%)'
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)' list_page_jumper_button: '[%1%](show_text=&7Vai alla pagina %1% run_command=%2% %1%)'
list_page_jumper_current_page: '[%1%](#00fb9a)' list_page_jumper_current_page: '[%1%](#00fb9a)'
list_page_jumper_separator: ' ' list_page_jumper_separator: ' '
list_page_jumper_group_separator: '…' list_page_jumper_group_separator: '…'

View File

@@ -11,7 +11,7 @@ public class DummyLogger extends Logger {
} }
@Override @Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Exception e) { public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable e) {
System.out.println(level.getName() + ": " + message); System.out.println(level.getName() + ": " + message);
e.printStackTrace(); e.printStackTrace();
} }

View File

@@ -142,6 +142,12 @@ public class DummyPlayer extends OnlineUser {
// do nothing // do nothing
} }
@Override
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) {
// do nothing
}
@Override @Override
public boolean hasPermission(@NotNull String node) { public boolean hasPermission(@NotNull String node) {
return true; return true;
@@ -160,4 +166,9 @@ public class DummyPlayer extends OnlineUser {
return false; return false;
} }
@Override
public boolean isLocked() {
return false;
}
} }

View File

@@ -3,7 +3,7 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true org.gradle.daemon=true
javaVersion=16 javaVersion=16
plugin_version=2.1 plugin_version=2.2
plugin_archive=husksync plugin_archive=husksync
jedis_version=4.2.3 jedis_version=4.2.3