diff --git a/.gitignore b/.gitignore
index 68b6a914..cfbf48c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -118,3 +118,8 @@ run/
!gradle-wrapper.jar
/build-output-final/
/target/
+
+# Don't include generated test suite files
+/test/servers/
+/test/HuskSync
+/test/config.yml
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 7a4a3ea2..8dc622b9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -16,7 +16,7 @@
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
+ "control" means (i) the power, direct or indirect, to saveCause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
@@ -95,7 +95,7 @@
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
- (b) You must cause any modified files to carry prominent notices
+ (b) You must saveCause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
diff --git a/README.md b/README.md
index 34ffb477..4fcad8ca 100644
--- a/README.md
+++ b/README.md
@@ -26,12 +26,12 @@
-**HuskSync** is a modern, cross-server player data synchronisation system that enables the comprehensive synchronisation of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
+**HuskSync** is a modern, cross-server player data synchronization system that enables the comprehensive synchronization of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
## Features
-**⭐ Seamless synchronisation** — Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
+**⭐ Seamless synchronization** — Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
-**⭐ Complete player synchronisation** — Sync inventories, Ender Chests, health, hunger, effects, advancements, statistics, locked maps & [more](https://william278.net/docs/husksync/sync-features)—no data left behind!
+**⭐ Complete player synchronization** — Sync inventories, Ender Chests, health, hunger, effects, advancements, statistics, locked maps & [more](https://william278.net/docs/husksync/sync-features)—no data left behind!
**⭐ Backup, restore & rotate** — Something gone wrong? Restore players back to a previous data state. Rotate and manage data snapshots in-game!
diff --git a/build.gradle b/build.gradle
index 66d1c5f8..6a385620 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,7 +19,6 @@ ext {
set 'mysql_driver_version', mysql_driver_version.toString()
set 'mariadb_driver_version', mariadb_driver_version.toString()
set 'snappy_version', snappy_version.toString()
- set 'commons_text_version', commons_text_version.toString()
}
import org.apache.tools.ant.filters.ReplaceTokens
@@ -39,16 +38,18 @@ allprojects {
mavenLocal()
mavenCentral()
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
+ maven { url 'https://repo.codemc.io/repository/maven-public/' }
maven { url 'https://repo.minebench.de/' }
maven { url 'https://repo.alessiodp.com/releases/' }
- maven { url 'https://repo.mattstudios.me/artifactory/public/' }
maven { url 'https://jitpack.io' }
+ maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
maven { url 'https://libraries.minecraft.net/' }
- maven { url 'https://william278.net/releases/' }
+ maven { url 'https://repo.william278.net/releases/' }
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
+ testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
}
diff --git a/bukkit/build.gradle b/bukkit/build.gradle
index 24b5db59..547a61a6 100644
--- a/bukkit/build.gradle
+++ b/bukkit/build.gradle
@@ -1,23 +1,27 @@
dependencies {
implementation project(path: ':common')
+
implementation 'org.bstats:bstats-bukkit:3.0.2'
implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0'
- implementation 'net.william278:MapDataAPI:1.0.2'
- implementation 'net.william278:AndJam:1.0.2'
+ implementation 'net.william278:mapdataapi:1.0.3'
+ implementation 'net.william278:andjam:1.0.2'
implementation 'me.lucko:commodore:2.2'
implementation 'net.kyori:adventure-platform-bukkit:4.3.0'
implementation 'dev.triumphteam:triumph-gui:3.1.5'
+ implementation 'space.arim.morepaperlib:morepaperlib:0.4.3'
+ implementation 'de.tr7zw:item-nbt-api:2.12.0-RC1'
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
compileOnly 'commons-io:commons-io:2.13.0'
+ compileOnly 'org.json:json:20230618'
compileOnly 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
compileOnly 'dev.dejvokep:boosted-yaml:1.3.1'
compileOnly 'com.zaxxer:HikariCP:5.0.1'
- compileOnly 'redis.clients:jedis:' + jedis_version
compileOnly 'net.william278:DesertWell:2.0.4'
- compileOnly 'net.william278:Annotaml:2.0.1'
+ compileOnly 'net.william278:annotaml:2.0.7'
compileOnly 'net.william278:AdvancementAPI:97a9583413'
+ compileOnly "redis.clients:jedis:$jedis_version"
}
shadowJar {
@@ -26,7 +30,11 @@ shadowJar {
}
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
+ relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
+ relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
+ relocate 'org.json', 'net.william278.husksync.libraries.json'
+ relocate 'com.fatboyindustrial', 'net.william278.husktowns.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'net.kyori', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
@@ -37,7 +45,7 @@ shadowJar {
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.querz', 'net.william278.husksync.libraries.nbtparser'
relocate 'net.roxeez', 'net.william278.husksync.libraries'
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
@@ -47,4 +55,6 @@ shadowJar {
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'net.william278.annotaml', 'net.william278.husksync.libraries.annotaml'
+ relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
+ relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
}
\ No newline at end of file
diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
index e62be5a7..9c9a5a42 100644
--- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
+++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
@@ -19,253 +19,215 @@
package net.william278.husksync;
+import com.google.gson.Gson;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
-import net.william278.annotaml.Annotaml;
import net.william278.desertwell.util.Version;
+import net.william278.husksync.adapter.DataAdapter;
+import net.william278.husksync.adapter.GsonAdapter;
+import net.william278.husksync.adapter.SnappyGsonAdapter;
+import net.william278.husksync.api.BukkitHuskSyncAPI;
import net.william278.husksync.command.BukkitCommand;
-import net.william278.husksync.command.BukkitCommandType;
-import net.william278.husksync.command.Permission;
import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Settings;
-import net.william278.husksync.data.CompressedDataAdapter;
-import net.william278.husksync.data.DataAdapter;
-import net.william278.husksync.data.JsonDataAdapter;
+import net.william278.husksync.data.BukkitSerializer;
+import net.william278.husksync.data.Data;
+import net.william278.husksync.data.Identifier;
+import net.william278.husksync.data.Serializer;
import net.william278.husksync.database.Database;
import net.william278.husksync.database.MySqlDatabase;
-import net.william278.husksync.event.BukkitEventCannon;
-import net.william278.husksync.event.EventCannon;
+import net.william278.husksync.event.BukkitEventDispatcher;
import net.william278.husksync.hook.PlanHook;
import net.william278.husksync.listener.BukkitEventListener;
import net.william278.husksync.listener.EventListener;
import net.william278.husksync.migrator.LegacyMigrator;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.migrator.MpdbMigrator;
-import net.william278.husksync.player.BukkitPlayer;
-import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager;
+import net.william278.husksync.user.BukkitUser;
+import net.william278.husksync.user.ConsoleUser;
+import net.william278.husksync.user.OnlineUser;
+import net.william278.husksync.util.BukkitLegacyConverter;
+import net.william278.husksync.util.BukkitMapPersister;
+import net.william278.husksync.util.BukkitTask;
+import net.william278.husksync.util.LegacyConverter;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
-import org.bukkit.command.PluginCommand;
import org.bukkit.entity.Player;
-import org.bukkit.permissions.PermissionDefault;
-import org.bukkit.plugin.Plugin;
+import org.bukkit.map.MapView;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
+import space.arim.morepaperlib.MorePaperLib;
+import space.arim.morepaperlib.commands.CommandRegistration;
+import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
+import space.arim.morepaperlib.scheduling.GracefulScheduling;
+import space.arim.morepaperlib.scheduling.RegionalScheduler;
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
import java.util.*;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.stream.Collectors;
-public class BukkitHuskSync extends JavaPlugin implements HuskSync {
+public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier, BukkitEventDispatcher,
+ BukkitMapPersister {
/**
* Metrics ID for HuskSync on Bukkit.
*/
private static final int METRICS_ID = 13140;
+ private static final String PLATFORM_TYPE_ID = "bukkit";
+
private Database database;
private RedisManager redisManager;
private EventListener eventListener;
private DataAdapter dataAdapter;
- private EventCannon eventCannon;
+ private Map> serializers;
+ private Map> playerCustomDataStore;
private Settings settings;
private Locales locales;
private List availableMigrators;
-
+ private LegacyConverter legacyConverter;
+ private Map mapViews;
private BukkitAudiences audiences;
- private static BukkitHuskSync instance;
-
- /**
- * (Internal use only) Returns the instance of the implementing Bukkit plugin
- *
- * @return the instance of the Bukkit plugin
- */
- public static BukkitHuskSync getInstance() {
- return instance;
- }
-
- @Override
- public void onLoad() {
- instance = this;
- }
+ private MorePaperLib paperLib;
+ private AsynchronousScheduler asyncScheduler;
+ private RegionalScheduler regionalScheduler;
+ private Gson gson;
@Override
public void onEnable() {
- // Initialize HuskSync
- final AtomicBoolean initialized = new AtomicBoolean(true);
- try {
- // Create adventure audience
- this.audiences = BukkitAudiences.create(this);
-
- // Load settings and locales
- log(Level.INFO, "Loading plugin configuration settings & locales...");
- initialized.set(reload().join());
- if (initialized.get()) {
- log(Level.INFO, "Successfully loaded plugin configuration settings & locales");
- } else {
- throw new HuskSyncInitializationException("Failed to load plugin configuration settings and/or locales");
- }
-
- // Prepare data adapter
+ // Initial plugin setup
+ this.gson = createGson();
+ this.audiences = BukkitAudiences.create(this);
+ this.paperLib = new MorePaperLib(this);
+ this.availableMigrators = new ArrayList<>();
+ this.serializers = new LinkedHashMap<>();
+ this.playerCustomDataStore = new ConcurrentHashMap<>();
+ this.mapViews = new ConcurrentHashMap<>();
+
+ // Load settings and locales
+ initialize("plugin config & locale files", (plugin) -> this.loadConfigs());
+
+ // Prepare data adapter
+ initialize("data adapter", (plugin) -> {
if (settings.doCompressData()) {
- dataAdapter = new CompressedDataAdapter();
+ dataAdapter = new SnappyGsonAdapter(this);
} else {
- dataAdapter = new JsonDataAdapter();
+ dataAdapter = new GsonAdapter(this);
}
+ });
- // Prepare event cannon
- eventCannon = new BukkitEventCannon();
+ // Prepare serializers
+ initialize("data serializers", (plugin) -> {
+ registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
+ registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
+ registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
+ registerSerializer(Identifier.LOCATION, new BukkitSerializer.Location(this));
+ registerSerializer(Identifier.HEALTH, new BukkitSerializer.Health(this));
+ registerSerializer(Identifier.HUNGER, new BukkitSerializer.Hunger(this));
+ registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.GameMode(this));
+ registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
+ registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Statistics(this));
+ registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Experience(this));
+ registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
+ });
- // Prepare migrators
- availableMigrators = new ArrayList<>();
+ // Setup available migrators
+ initialize("data migrators/converters", (plugin) -> {
availableMigrators.add(new LegacyMigrator(this));
- final Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
- if (mySqlPlayerDataBridge != null) {
- availableMigrators.add(new MpdbMigrator(this, mySqlPlayerDataBridge));
+ if (isDependencyLoaded("MySqlPlayerDataBridge")) {
+ availableMigrators.add(new MpdbMigrator(this));
}
+ legacyConverter = new BukkitLegacyConverter(this);
+ });
- // Prepare database connection
+ // Initialize the database
+ initialize(getSettings().getDatabaseType().getDisplayName() + " database connection", (plugin) -> {
this.database = new MySqlDatabase(this);
- log(Level.INFO, String.format("Attempting to establish connection to the %s database...",
- settings.getDatabaseType().getDisplayName()));
this.database.initialize();
- if (initialized.get()) {
- log(Level.INFO, String.format("Successfully established a connection to the %s database",
- settings.getDatabaseType().getDisplayName()));
- } else {
- throw new HuskSyncInitializationException("Failed to establish a connection to the database. " +
- "Please check the supplied database credentials in the config file");
- }
+ });
- // Prepare redis connection
+ // Prepare redis connection
+ initialize("Redis server connection", (plugin) -> {
this.redisManager = new RedisManager(this);
- log(Level.INFO, "Attempting to establish connection to the Redis server...");
- initialized.set(this.redisManager.initialize());
- if (initialized.get()) {
- log(Level.INFO, "Successfully established a connection to the Redis server");
- } else {
- throw new HuskSyncInitializationException("Failed to establish a connection to the Redis server. " +
- "Please check the supplied Redis credentials in the config file");
- }
+ this.redisManager.initialize();
+ });
- // Register events
- log(Level.INFO, "Registering events...");
- this.eventListener = new BukkitEventListener(this);
- log(Level.INFO, "Successfully registered events listener");
-
- // Register permissions
- log(Level.INFO, "Registering permissions & commands...");
- Arrays.stream(Permission.values()).forEach(permission -> getServer().getPluginManager()
- .addPermission(new org.bukkit.permissions.Permission(permission.node, switch (permission.defaultAccess) {
- case EVERYONE -> PermissionDefault.TRUE;
- case NOBODY -> PermissionDefault.FALSE;
- case OPERATORS -> PermissionDefault.OP;
- })));
-
- // Register commands
- for (final BukkitCommandType bukkitCommandType : BukkitCommandType.values()) {
- final PluginCommand pluginCommand = getCommand(bukkitCommandType.commandBase.command);
- if (pluginCommand != null) {
- new BukkitCommand(bukkitCommandType.commandBase, this).register(pluginCommand);
- }
- }
- log(Level.INFO, "Successfully registered permissions & commands");
+ // Register events
+ initialize("events", (plugin) -> this.eventListener = new BukkitEventListener(this));
- // Hook into plan
- if (Bukkit.getPluginManager().getPlugin("Plan") != null) {
- log(Level.INFO, "Enabling Plan integration...");
+ // Register commands
+ initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
+
+ // Register plugin hooks
+ initialize("hooks", (plugin) -> {
+ if (isDependencyLoaded("Plan") && getSettings().usePlanHook()) {
new PlanHook(this).hookIntoPlan();
- log(Level.INFO, "Plan integration enabled!");
}
+ });
- // Hook into bStats metrics
- try {
- new Metrics(this, METRICS_ID);
- } catch (final Exception e) {
- log(Level.WARNING, "Skipped bStats metrics initialization due to an exception.");
- }
+ // Register API
+ initialize("api", (plugin) -> BukkitHuskSyncAPI.register(this));
- // Check for updates
- if (settings.doCheckForUpdates()) {
- log(Level.INFO, "Checking for updates...");
- getLatestVersionIfOutdated().thenAccept(newestVersion ->
- newestVersion.ifPresent(newVersion -> log(Level.WARNING,
- "An update is available for HuskSync, v" + newVersion
- + " (Currently running v" + getPluginVersion() + ")")));
- }
- } catch (IllegalStateException exception) {
- log(Level.SEVERE, """
- ***************************************************
-
- Failed to initialize HuskSync!
-
- ***************************************************
- The plugin was disabled due to an error. Please check
- the logs below for details.
- No user data will be synchronised.
- ***************************************************
- Caused by: %error_message%
- """
- .replaceAll("%error_message%", exception.getMessage()));
- initialized.set(false);
- } catch (Exception exception) {
- log(Level.SEVERE, "An unhandled exception occurred initializing HuskSync!", exception);
- initialized.set(false);
- } finally {
- // Validate initialization
- if (initialized.get()) {
- log(Level.INFO, "Successfully enabled HuskSync v" + getPluginVersion());
- } else {
- log(Level.SEVERE, "Failed to initialize HuskSync. The plugin will now be disabled");
- getServer().getPluginManager().disablePlugin(this);
- }
- }
+ // Hook into bStats and check for updates
+ initialize("metrics", (plugin) -> this.registerMetrics(METRICS_ID));
+ this.checkForUpdates();
}
@Override
public void onDisable() {
+ // Handle shutdown
if (this.eventListener != null) {
this.eventListener.handlePluginDisable();
}
+
+ // Unregister API and cancel tasks
+ BukkitHuskSyncAPI.unregister();
+ this.cancelTasks();
+
+ // Complete shutdown
log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
}
@Override
- public @NotNull Set getOnlineUsers() {
- return Bukkit.getOnlinePlayers().stream().map(BukkitPlayer::adapt).collect(Collectors.toSet());
+ @NotNull
+ public Set getOnlineUsers() {
+ return Bukkit.getOnlinePlayers().stream()
+ .map(player -> BukkitUser.adapt(player, this))
+ .collect(Collectors.toSet());
}
@Override
- public @NotNull Optional getOnlineUser(@NotNull UUID uuid) {
+ @NotNull
+ public Optional getOnlineUser(@NotNull UUID uuid) {
final Player player = Bukkit.getPlayer(uuid);
if (player == null) {
return Optional.empty();
}
- return Optional.of(BukkitPlayer.adapt(player));
+ return Optional.of(BukkitUser.adapt(player, this));
}
@Override
- public @NotNull Database getDatabase() {
+ @NotNull
+ public Database getDatabase() {
return database;
}
@Override
- public @NotNull RedisManager getRedisManager() {
+ @NotNull
+ public RedisManager getRedisManager() {
return redisManager;
}
+ @NotNull
@Override
- public @NotNull DataAdapter getDataAdapter() {
+ public DataAdapter getDataAdapter() {
return dataAdapter;
}
+ @NotNull
@Override
- public @NotNull EventCannon getEventCannon() {
- return eventCannon;
+ public Map> getSerializers() {
+ return serializers;
}
@NotNull
@@ -274,16 +236,57 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return availableMigrators;
}
+ @NotNull
@Override
- public @NotNull Settings getSettings() {
+ public Map getPlayerCustomDataStore(@NotNull OnlineUser user) {
+ if (playerCustomDataStore.containsKey(user.getUuid())) {
+ return playerCustomDataStore.get(user.getUuid());
+ }
+ final Map data = new HashMap<>();
+ playerCustomDataStore.put(user.getUuid(), data);
+ return data;
+ }
+
+ @Override
+ @NotNull
+ public Settings getSettings() {
return settings;
}
@Override
- public @NotNull Locales getLocales() {
+ public void setSettings(@NotNull Settings settings) {
+ this.settings = settings;
+ }
+
+ @Override
+ @NotNull
+ public Locales getLocales() {
return locales;
}
+ @Override
+ public void setLocales(@NotNull Locales locales) {
+ this.locales = locales;
+ }
+
+ @Override
+ public boolean isDependencyLoaded(@NotNull String name) {
+ return Bukkit.getPluginManager().getPlugin(name) != null;
+ }
+
+ // Register bStats metrics
+ public void registerMetrics(int metricsId) {
+ if (!getPluginVersion().getMetadata().isBlank()) {
+ return;
+ }
+
+ try {
+ new Metrics(this, metricsId);
+ } catch (Throwable e) {
+ log(Level.WARNING, "Failed to register bStats metrics (" + e.getMessage() + ")");
+ }
+ }
+
@Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
if (throwable.length > 0) {
@@ -293,6 +296,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
}
}
+ @NotNull
+ @Override
+ public ConsoleUser getConsole() {
+ return new ConsoleUser(audiences.console());
+ }
+
@NotNull
@Override
public Version getPluginVersion() {
@@ -305,39 +314,65 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return Version.fromString(Bukkit.getBukkitVersion());
}
- /**
- * Returns the adventure Bukkit audiences
- *
- * @return The adventure Bukkit audiences
- */
@NotNull
- public BukkitAudiences getAudiences() {
- return audiences;
+ @Override
+ public String getPlatformType() {
+ return PLATFORM_TYPE_ID;
+ }
+
+ @Override
+ public Optional getLegacyConverter() {
+ return Optional.of(legacyConverter);
}
+ @NotNull
@Override
public Set getLockedPlayers() {
return this.eventListener.getLockedPlayers();
}
+ @NotNull
@Override
- public CompletableFuture reload() {
- return CompletableFuture.supplyAsync(() -> {
- try {
- // Load plugin settings
- this.settings = Annotaml.create(new File(getDataFolder(), "config.yml"), new Settings()).get();
-
- // Load locales from language preset default
- final Locales languagePresets = Annotaml.create(Locales.class,
- Objects.requireNonNull(getResource("locales/" + settings.getLanguage() + ".yml"))).get();
- this.locales = Annotaml.create(new File(getDataFolder(), "messages_" + settings.getLanguage() + ".yml"),
- languagePresets).get();
- return true;
- } catch (IOException | NullPointerException | InvocationTargetException | IllegalAccessException |
- InstantiationException e) {
- log(Level.SEVERE, "Failed to load data from the config", e);
- return false;
- }
- });
+ public Gson getGson() {
+ return gson;
}
+
+ @NotNull
+ public Map getMapViews() {
+ return mapViews;
+ }
+
+ @NotNull
+ public GracefulScheduling getScheduler() {
+ return paperLib.scheduling();
+ }
+
+ @NotNull
+ public AsynchronousScheduler getAsyncScheduler() {
+ return asyncScheduler == null
+ ? asyncScheduler = getScheduler().asyncScheduler() : asyncScheduler;
+ }
+
+ @NotNull
+ public RegionalScheduler getRegionalScheduler() {
+ return regionalScheduler == null
+ ? regionalScheduler = getScheduler().globalRegionalScheduler() : regionalScheduler;
+ }
+
+ @NotNull
+ public BukkitAudiences getAudiences() {
+ return audiences;
+ }
+
+ @NotNull
+ public CommandRegistration getCommandRegistrar() {
+ return paperLib.commandRegistration();
+ }
+
+ @Override
+ @NotNull
+ public HuskSync getPlugin() {
+ return this;
+ }
+
}
diff --git a/bukkit/src/main/java/net/william278/husksync/api/BukkitHuskSyncAPI.java b/bukkit/src/main/java/net/william278/husksync/api/BukkitHuskSyncAPI.java
new file mode 100644
index 00000000..6abf75c0
--- /dev/null
+++ b/bukkit/src/main/java/net/william278/husksync/api/BukkitHuskSyncAPI.java
@@ -0,0 +1,270 @@
+/*
+ * This file is part of HuskSync, licensed under the Apache License 2.0.
+ *
+ * Copyright (c) William278
+ * 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
+ *
+ * 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;
+
+ /**
+ * (Internal use only) - 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;
+ }
+
+ /**
+ * (Internal use only) - 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);
+ }
+
+ /**
+ * (Internal use only) - 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> 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> 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 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 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> 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> 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 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 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);
+ }
+
+}
diff --git a/bukkit/src/main/java/net/william278/husksync/api/HuskSyncAPI.java b/bukkit/src/main/java/net/william278/husksync/api/HuskSyncAPI.java
deleted file mode 100644
index 0b5f7116..00000000
--- a/bukkit/src/main/java/net/william278/husksync/api/HuskSyncAPI.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * This file is part of HuskSync, licensed under the Apache License 2.0.
- *
- * Copyright (c) William278
- * 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.
- *
- * Retrieve an instance of the API class via {@link #getInstance()}.
- */
-@SuppressWarnings("unused")
-public class HuskSyncAPI extends BaseHuskSyncAPI {
-
- /**
- * (Internal use only) - Instance of the API class
- */
- private static final HuskSyncAPI INSTANCE = new HuskSyncAPI();
-
- /**
- * (Internal use only) - 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 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 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 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> 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> 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 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 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 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 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 serializePotionEffectArray(@NotNull PotionEffect[] potionEffects)
- throws DataSerializationException {
- return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializePotionEffectArray(potionEffects).join());
- }
-
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/command/BrigadierUtil.java b/bukkit/src/main/java/net/william278/husksync/command/BrigadierUtil.java
index fbac0c9d..8b35ac38 100644
--- a/bukkit/src/main/java/net/william278/husksync/command/BrigadierUtil.java
+++ b/bukkit/src/main/java/net/william278/husksync/command/BrigadierUtil.java
@@ -22,28 +22,38 @@ package net.william278.husksync.command;
import me.lucko.commodore.CommodoreProvider;
import me.lucko.commodore.file.CommodoreFileReader;
import net.william278.husksync.BukkitHuskSync;
-import org.bukkit.command.PluginCommand;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
-/**
- * Used for registering Brigadier hooks on platforms that support commodore for rich command syntax
- */
public class BrigadierUtil {
- protected static void registerCommodore(@NotNull BukkitHuskSync plugin, @NotNull PluginCommand pluginCommand,
- @NotNull CommandBase command) {
- // Register command descriptions via commodore (brigadier wrapper)
- try (InputStream pluginFile = plugin.getResource("commodore/" + command.command + ".commodore")) {
- CommodoreProvider.getCommodore(plugin).register(pluginCommand,
- CommodoreFileReader.INSTANCE.parse(pluginFile),
- player -> player.hasPermission(command.permission));
+ /**
+ * Uses commodore to register command completions.
+ *
+ * @param plugin instance of the registering Bukkit plugin
+ * @param bukkitCommand the Bukkit PluginCommand to register completions for
+ * @param command the {@link Command} to register completions for
+ */
+ protected static void registerCommodore(@NotNull BukkitHuskSync plugin,
+ @NotNull org.bukkit.command.Command bukkitCommand,
+ @NotNull Command command) {
+ final InputStream commodoreFile = plugin.getResource(
+ "commodore/" + bukkitCommand.getName() + ".commodore"
+ );
+ if (commodoreFile == null) {
+ return;
+ }
+ try {
+ CommodoreProvider.getCommodore(plugin).register(bukkitCommand,
+ CommodoreFileReader.INSTANCE.parse(commodoreFile),
+ player -> player.hasPermission(command.getPermission()));
} catch (IOException e) {
- plugin.log(Level.SEVERE,
- "Failed to load " + command.command + ".commodore command definitions", e);
+ plugin.log(Level.SEVERE, String.format(
+ "Failed to read command commodore completions for %s", bukkitCommand.getName()), e
+ );
}
}
diff --git a/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java b/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java
index 178ac885..5229d11b 100644
--- a/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java
+++ b/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java
@@ -19,77 +19,146 @@
package net.william278.husksync.command;
+
import me.lucko.commodore.CommodoreProvider;
import net.william278.husksync.BukkitHuskSync;
-import net.william278.husksync.player.BukkitPlayer;
-import org.bukkit.command.*;
+import net.william278.husksync.user.BukkitUser;
+import net.william278.husksync.user.CommandUser;
+import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
+import org.bukkit.permissions.Permission;
+import org.bukkit.permissions.PermissionDefault;
+import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Bukkit executor that implements and executes {@link CommandBase}s
- */
-public class BukkitCommand implements CommandExecutor, TabExecutor {
+import java.util.*;
+import java.util.function.Function;
- /**
- * The {@link CommandBase} that will be executed
- */
- protected final CommandBase command;
+public class BukkitCommand extends org.bukkit.command.Command {
- /**
- * The implementing plugin
- */
private final BukkitHuskSync plugin;
+ private final Command command;
- public BukkitCommand(@NotNull CommandBase command, @NotNull BukkitHuskSync implementor) {
+ public BukkitCommand(@NotNull Command command, @NotNull BukkitHuskSync plugin) {
+ super(command.getName(), command.getDescription(), command.getUsage(), command.getAliases());
this.command = command;
- this.plugin = implementor;
+ this.plugin = plugin;
}
- /**
- * Registers a {@link PluginCommand} to this implementation
- *
- * @param pluginCommand {@link PluginCommand} to register
- */
- public void register(@NotNull PluginCommand pluginCommand) {
- pluginCommand.setExecutor(this);
- pluginCommand.setTabCompleter(this);
- pluginCommand.setPermission(command.permission);
- pluginCommand.setDescription(command.getDescription());
- if (CommodoreProvider.isSupported()) {
- BrigadierUtil.registerCommodore(plugin, pluginCommand, command);
- }
+ @Override
+ public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
+ this.command.onExecuted(sender instanceof Player p ? BukkitUser.adapt(p, plugin) : plugin.getConsole(), args);
+ return true;
}
+ @NotNull
@Override
- public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
- @NotNull String label, @NotNull String[] args) {
- if (sender instanceof Player player) {
- this.command.onExecute(BukkitPlayer.adapt(player), args);
- } else {
- if (this.command instanceof ConsoleExecutable consoleExecutable) {
- consoleExecutable.onConsoleExecute(args);
- } else {
- plugin.getLocales().getLocale("error_in_game_command_only")
- .ifPresent(locale -> plugin.getAudiences().sender(sender)
- .sendMessage(locale.toComponent()));
- }
+ public List tabComplete(@NotNull CommandSender sender, @NotNull String alias,
+ @NotNull String[] args) throws IllegalArgumentException {
+ if (!(this.command instanceof TabProvider provider)) {
+ return List.of();
+ }
+ final CommandUser user = sender instanceof Player p ? BukkitUser.adapt(p, plugin) : plugin.getConsole();
+ if (getPermission() == null || user.hasPermission(getPermission())) {
+ return provider.getSuggestions(user, args);
+ }
+ return List.of();
+ }
+
+ public void register() {
+ // Register with bukkit
+ plugin.getCommandRegistrar().getServerCommandMap().register("husksync", this);
+
+ // Register permissions
+ BukkitCommand.addPermission(
+ plugin,
+ command.getPermission(),
+ command.getUsage(),
+ BukkitCommand.getPermissionDefault(command.isOperatorCommand())
+ );
+ final List childNodes = command.getAdditionalPermissions()
+ .entrySet().stream()
+ .map((entry) -> BukkitCommand.addPermission(
+ plugin,
+ entry.getKey(),
+ "",
+ BukkitCommand.getPermissionDefault(entry.getValue()))
+ )
+ .filter(Objects::nonNull)
+ .toList();
+ if (!childNodes.isEmpty()) {
+ BukkitCommand.addPermission(
+ plugin,
+ command.getPermission("*"),
+ command.getUsage(),
+ PermissionDefault.FALSE,
+ childNodes.toArray(new Permission[0])
+ );
+ }
+
+ // Register commodore TAB completion
+ if (CommodoreProvider.isSupported() && plugin.getSettings().doBrigadierTabCompletion()) {
+ BrigadierUtil.registerCommodore(plugin, this, command);
}
- return true;
}
@Nullable
- @Override
- public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
- @NotNull String alias, @NotNull String[] args) {
- if (this.command instanceof TabCompletable tabCompletable) {
- return tabCompletable.onTabComplete(args);
+ protected static Permission addPermission(@NotNull BukkitHuskSync plugin, @NotNull String node,
+ @NotNull String description, @NotNull PermissionDefault permissionDefault,
+ @NotNull Permission... children) {
+ final Map childNodes = Arrays.stream(children)
+ .map(Permission::getName)
+ .collect(HashMap::new, (map, child) -> map.put(child, true), HashMap::putAll);
+
+ final PluginManager manager = plugin.getServer().getPluginManager();
+ if (manager.getPermission(node) != null) {
+ return null;
+ }
+
+ Permission permission;
+ if (description.isEmpty()) {
+ permission = new Permission(node, permissionDefault, childNodes);
+ } else {
+ permission = new Permission(node, description, permissionDefault, childNodes);
}
- return Collections.emptyList();
+ manager.addPermission(permission);
+
+ return permission;
}
+ @NotNull
+ protected static PermissionDefault getPermissionDefault(boolean isOperatorCommand) {
+ return isOperatorCommand ? PermissionDefault.OP : PermissionDefault.TRUE;
+ }
+
+ /**
+ * Commands available on the Bukkit HuskSync implementation
+ */
+ public enum Type {
+
+ HUSKSYNC_COMMAND(HuskSyncCommand::new),
+ USERDATA_COMMAND(UserDataCommand::new),
+ INVENTORY_COMMAND(InventoryCommand::new),
+ ENDER_CHEST_COMMAND(EnderChestCommand::new);
+
+ public final Function commandSupplier;
+
+ Type(@NotNull Function supplier) {
+ this.commandSupplier = supplier;
+ }
+
+ @NotNull
+ public Command createCommand(@NotNull BukkitHuskSync plugin) {
+ return commandSupplier.apply(plugin);
+ }
+
+ public static void registerCommands(@NotNull BukkitHuskSync plugin) {
+ Arrays.stream(values())
+ .map((type) -> type.createCommand(plugin))
+ .forEach((command) -> new BukkitCommand(command, plugin).register());
+ }
+
+
+ }
}
diff --git a/bukkit/src/main/java/net/william278/husksync/command/BukkitCommandType.java b/bukkit/src/main/java/net/william278/husksync/command/BukkitCommandType.java
deleted file mode 100644
index a69e0229..00000000
--- a/bukkit/src/main/java/net/william278/husksync/command/BukkitCommandType.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * This file is part of HuskSync, licensed under the Apache License 2.0.
- *
- * Copyright (c) William278
- * 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;
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java
new file mode 100644
index 00000000..7416306f
--- /dev/null
+++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java
@@ -0,0 +1,1093 @@
+/*
+ * This file is part of HuskSync, licensed under the Apache License 2.0.
+ *
+ * Copyright (c) William278
+ * 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 de.tr7zw.changeme.nbtapi.NBTCompound;
+import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
+import net.william278.desertwell.util.ThrowingConsumer;
+import net.william278.husksync.BukkitHuskSync;
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.adapter.Adaptable;
+import net.william278.husksync.user.BukkitUser;
+import org.apache.commons.lang.NotImplementedException;
+import org.bukkit.Bukkit;
+import org.bukkit.GameRule;
+import org.bukkit.Material;
+import org.bukkit.Statistic;
+import org.bukkit.advancement.AdvancementProgress;
+import org.bukkit.attribute.Attribute;
+import org.bukkit.attribute.AttributeInstance;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+public abstract class BukkitData implements Data {
+
+ @Override
+ public final void apply(@NotNull UserDataHolder dataHolder, @NotNull HuskSync plugin) {
+ final BukkitUser user = (BukkitUser) dataHolder;
+ try {
+ this.apply(user, (BukkitHuskSync) plugin);
+ } catch (Throwable e) {
+ plugin.log(Level.WARNING, String.format("[%s] Failed to apply %s data object; skipping",
+ user.getUsername(), this.getClass().getSimpleName()), e);
+ }
+ }
+
+ public abstract void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException;
+
+ public static abstract class Items extends BukkitData implements Data.Items {
+
+ private final ItemStack[] contents;
+
+ private Items(@NotNull ItemStack[] contents) {
+ this.contents = Arrays.stream(contents)
+ .map(i -> i == null || i.getType() == Material.AIR ? null : i)
+ .toArray(ItemStack[]::new);
+ }
+
+ @NotNull
+ @Override
+ public Stack[] getStack() {
+ return Arrays.stream(contents)
+ .map(stack -> stack != null ? new Stack(
+ stack.getType().getKey().toString(),
+ stack.getAmount(),
+ stack.hasItemMeta() ? (Objects.requireNonNull(
+ stack.getItemMeta()).hasDisplayName() ? stack.getItemMeta().getDisplayName() : null)
+ : null,
+ stack.hasItemMeta() ? (Objects.requireNonNull(
+ stack.getItemMeta()).hasLore() ? stack.getItemMeta().getLore() : null)
+ : null,
+ stack.hasItemMeta() && Objects.requireNonNull(stack.getItemMeta()).hasEnchants() ?
+ stack.getItemMeta().getEnchants().keySet().stream()
+ .map(enchantment -> enchantment.getKey().getKey())
+ .toList()
+ : List.of()
+ ) : null)
+ .toArray(Stack[]::new);
+ }
+
+ @Override
+ public void clear() {
+ Arrays.fill(contents, null);
+ }
+
+ @Override
+ public void setContents(@NotNull Data.Items contents) {
+ System.arraycopy(
+ ((BukkitData.Items) contents).getContents(),
+ 0, this.contents,
+ 0, this.contents.length
+ );
+ }
+
+ @SuppressWarnings("unused")
+ public void setContents(@NotNull ItemStack[] contents) {
+ System.arraycopy(contents, 0, this.contents, 0, this.contents.length);
+ }
+
+ @NotNull
+ public ItemStack[] getContents() {
+ return contents;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof BukkitData.Items items) {
+ return Arrays.equals(contents, items.getContents());
+ }
+ return false;
+ }
+
+ public static class Inventory extends BukkitData.Items implements Data.Items.Inventory {
+
+ public static final int INVENTORY_SLOT_COUNT = 41;
+ private int heldItemSlot;
+
+ private Inventory(@NotNull ItemStack[] contents, int heldItemSlot) {
+ super(contents);
+ this.heldItemSlot = heldItemSlot;
+ }
+
+ @NotNull
+ public static BukkitData.Items.Inventory from(@NotNull ItemStack[] contents, int heldItemSlot) {
+ return new BukkitData.Items.Inventory(contents, heldItemSlot);
+ }
+
+ @NotNull
+ public static BukkitData.Items.Inventory empty() {
+ return new BukkitData.Items.Inventory(new ItemStack[INVENTORY_SLOT_COUNT], 0);
+ }
+
+ @Override
+ public int getSlotCount() {
+ return INVENTORY_SLOT_COUNT;
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ final Player player = user.getPlayer();
+ this.clearInventoryCraftingSlots(player);
+ player.setItemOnCursor(null);
+ player.getInventory().setContents(plugin.setMapViews(getContents()));
+ player.updateInventory();
+ player.getInventory().setHeldItemSlot(heldItemSlot);
+ }
+
+ private void clearInventoryCraftingSlots(@NotNull Player player) {
+ final org.bukkit.inventory.Inventory inventory = player.getOpenInventory().getTopInventory();
+ if (inventory.getType() == InventoryType.CRAFTING) {
+ for (int slot = 0; slot < 5; slot++) {
+ inventory.setItem(slot, null);
+ }
+ }
+ }
+
+ @Override
+ public int getHeldItemSlot() {
+ return heldItemSlot;
+ }
+
+ @Override
+ public void setHeldItemSlot(int heldItemSlot) throws IllegalArgumentException {
+ if (heldItemSlot < 0 || heldItemSlot > 8) {
+ throw new IllegalArgumentException("Held item slot must be between 0 and 8");
+ }
+ this.heldItemSlot = heldItemSlot;
+ }
+
+ }
+
+ public static class EnderChest extends BukkitData.Items implements Data.Items.EnderChest {
+
+ public static final int ENDER_CHEST_SLOT_COUNT = 27;
+
+ private EnderChest(@NotNull ItemStack[] contents) {
+ super(contents);
+ }
+
+ @NotNull
+ public static BukkitData.Items.EnderChest adapt(@NotNull ItemStack[] items) {
+ return new BukkitData.Items.EnderChest(items);
+ }
+
+ @NotNull
+ public static BukkitData.Items.EnderChest empty() {
+ return new BukkitData.Items.EnderChest(new ItemStack[ENDER_CHEST_SLOT_COUNT]);
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ user.getPlayer().getEnderChest().setContents(plugin.setMapViews(getContents()));
+ }
+
+ }
+
+ public static class ItemArray extends BukkitData.Items implements Data.Items {
+
+ private ItemArray(@NotNull ItemStack[] contents) {
+ super(contents);
+ }
+
+ @NotNull
+ public static ItemArray adapt(@NotNull Collection drops) {
+ return new ItemArray(drops.toArray(ItemStack[]::new));
+ }
+
+ @NotNull
+ public static ItemArray adapt(@NotNull ItemStack[] drops) {
+ return new ItemArray(drops);
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ throw new NotImplementedException("A generic item array cannot be applied to a player");
+ }
+
+ }
+
+ }
+
+ public static class PotionEffects extends BukkitData implements Data.PotionEffects {
+
+ private final Collection effects;
+
+ private PotionEffects(@NotNull Collection effects) {
+ this.effects = effects;
+ }
+
+ @NotNull
+ public static BukkitData.PotionEffects from(@NotNull Collection effects) {
+ return new BukkitData.PotionEffects(effects);
+ }
+
+ @NotNull
+ public static BukkitData.PotionEffects adapt(@NotNull Collection effects) {
+ return from(
+ effects.stream()
+ .map(effect -> new PotionEffect(
+ Objects.requireNonNull(
+ PotionEffectType.getByName(effect.type()),
+ "Invalid potion effect type"
+ ),
+ effect.duration(),
+ effect.amplifier(),
+ effect.isAmbient(),
+ effect.showParticles(),
+ effect.hasIcon()
+ ))
+ .toList()
+ );
+ }
+
+ @NotNull
+ @SuppressWarnings("unused")
+ public static BukkitData.PotionEffects empty() {
+ return new BukkitData.PotionEffects(List.of());
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ final Player player = user.getPlayer();
+ for (PotionEffect effect : player.getActivePotionEffects()) {
+ player.removePotionEffect(effect.getType());
+ }
+ for (PotionEffect effect : this.getEffects()) {
+ player.addPotionEffect(effect);
+ }
+ }
+
+ @NotNull
+ @Override
+ public List getActiveEffects() {
+ return effects.stream()
+ .map(potionEffect -> new Effect(
+ potionEffect.getType().getName().toLowerCase(Locale.ENGLISH),
+ potionEffect.getAmplifier(),
+ potionEffect.getDuration(),
+ potionEffect.isAmbient(),
+ potionEffect.hasParticles(),
+ potionEffect.hasIcon()
+ ))
+ .toList();
+ }
+
+ @NotNull
+ public Collection getEffects() {
+ return effects;
+ }
+
+ }
+
+ public static class Advancements extends BukkitData implements Data.Advancements {
+
+ private List completed;
+
+ private Advancements(@NotNull List advancements) {
+ this.completed = advancements;
+ }
+
+ // Iterate through the server advancement set and add all advancements to the list
+ @NotNull
+ public static BukkitData.Advancements adapt(@NotNull Player player) {
+ final List advancements = new ArrayList<>();
+ forEachAdvancement(advancement -> {
+ final AdvancementProgress advancementProgress = player.getAdvancementProgress(advancement);
+ final Map 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()) {
+ advancements.add(Advancement.adapt(advancement.getKey().toString(), awardedCriteria));
+ }
+ });
+ return new BukkitData.Advancements(advancements);
+ }
+
+ @NotNull
+ public static BukkitData.Advancements from(@NotNull List advancements) {
+ return new BukkitData.Advancements(advancements);
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ plugin.runAsync(() -> forEachAdvancement(advancement -> {
+ final Player player = user.getPlayer();
+ final AdvancementProgress progress = player.getAdvancementProgress(advancement);
+ final Optional record = completed.stream()
+ .filter(r -> r.getKey().equals(advancement.getKey().toString()))
+ .findFirst();
+ if (record.isEmpty()) {
+ this.setAdvancement(plugin, advancement, player, List.of(), progress.getAwardedCriteria());
+ return;
+ }
+
+ final Map criteria = record.get().getCompletedCriteria();
+ this.setAdvancement(
+ plugin, advancement, player,
+ criteria.keySet().stream().filter(key -> !progress.getAwardedCriteria().contains(key)).toList(),
+ progress.getAwardedCriteria().stream().filter(key -> !criteria.containsKey(key)).toList()
+ );
+ }));
+ }
+
+ private void setAdvancement(@NotNull HuskSync plugin,
+ @NotNull org.bukkit.advancement.Advancement advancement, @NotNull Player player,
+ @NotNull Collection toAward, @NotNull Collection toRevoke) {
+ plugin.runSync(() -> {
+ // Track player exp level & progress
+ final int expLevel = player.getLevel();
+ final float expProgress = player.getExp();
+ boolean gameRuleUpdated = false;
+ if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
+ player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
+ gameRuleUpdated = true;
+ }
+
+ // Award and revoke advancement criteria
+ final AdvancementProgress progress = player.getAdvancementProgress(advancement);
+ toAward.forEach(progress::awardCriteria);
+ toRevoke.forEach(progress::revokeCriteria);
+
+ // Set player experience and level (prevent advancement awards applying twice), reset game rule
+ if (!toAward.isEmpty() && player.getLevel() != expLevel || player.getExp() != expProgress) {
+ player.setLevel(expLevel);
+ player.setExp(expProgress);
+ }
+ if (gameRuleUpdated) {
+ player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
+ }
+ });
+ }
+
+ // Performs a consuming function for every advancement registered on the server
+ private static void forEachAdvancement(@NotNull ThrowingConsumer consumer) {
+ Bukkit.getServer().advancementIterator().forEachRemaining(consumer);
+ }
+
+ @NotNull
+ @Override
+ public List getCompleted() {
+ return completed;
+ }
+
+ @Override
+ public void setCompleted(@NotNull List completed) {
+ this.completed = completed;
+ }
+
+ }
+
+ public static class Location extends BukkitData implements Data.Location, Adaptable {
+ @SerializedName("x")
+ private double x;
+ @SerializedName("y")
+ private double y;
+ @SerializedName("z")
+ private double z;
+ @SerializedName("yaw")
+ private float yaw;
+ @SerializedName("pitch")
+ private float pitch;
+ @SerializedName("world")
+ private World world;
+
+ private Location(double x, double y, double z, float yaw, float pitch, @NotNull World world) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.yaw = yaw;
+ this.pitch = pitch;
+ this.world = world;
+ }
+
+ @SuppressWarnings("unused")
+ private Location() {
+ }
+
+ @NotNull
+ public static BukkitData.Location from(double x, double y, double z,
+ float yaw, float pitch, @NotNull World world) {
+ return new BukkitData.Location(x, y, z, yaw, pitch, world);
+ }
+
+ @NotNull
+ public static BukkitData.Location adapt(@NotNull org.bukkit.Location location) {
+ return from(
+ location.getX(),
+ location.getY(),
+ location.getZ(),
+ location.getYaw(),
+ location.getPitch(),
+ new World(
+ Objects.requireNonNull(location.getWorld(), "World is null").getName(),
+ location.getWorld().getUID(),
+ location.getWorld().getEnvironment().name()
+ )
+ );
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ try {
+ final org.bukkit.Location location = new org.bukkit.Location(
+ Bukkit.getWorld(world.name()), x, y, z, yaw, pitch
+ );
+ user.getPlayer().teleport(location);
+ } catch (Throwable e) {
+ throw new IllegalStateException("Failed to apply location", e);
+ }
+ }
+
+ @Override
+ public double getX() {
+ return x;
+ }
+
+ @Override
+ public void setX(double x) {
+ this.x = x;
+ }
+
+ @Override
+ public double getY() {
+ return y;
+ }
+
+ @Override
+ public void setY(double y) {
+ this.y = y;
+ }
+
+ @Override
+ public double getZ() {
+ return z;
+ }
+
+ @Override
+ public void setZ(double z) {
+ this.z = z;
+ }
+
+ @Override
+ public float getYaw() {
+ return yaw;
+ }
+
+ @Override
+ public void setYaw(float yaw) {
+ this.yaw = yaw;
+ }
+
+ @Override
+ public float getPitch() {
+ return pitch;
+ }
+
+ @Override
+ public void setPitch(float pitch) {
+ this.pitch = pitch;
+ }
+
+ @NotNull
+ @Override
+ public World getWorld() {
+ return world;
+ }
+
+ @Override
+ public void setWorld(@NotNull World world) {
+ this.world = world;
+ }
+
+ }
+
+ public static class Statistics extends BukkitData implements Data.Statistics {
+ private Map untypedStatistics;
+ private Map> blockStatistics;
+ private Map> itemStatistics;
+ private Map> entityStatistics;
+
+ private Statistics(@NotNull Map genericStatistics,
+ @NotNull Map> blockStatistics,
+ @NotNull Map> itemStatistics,
+ @NotNull Map> entityStatistics) {
+ this.untypedStatistics = genericStatistics;
+ this.blockStatistics = blockStatistics;
+ this.itemStatistics = itemStatistics;
+ this.entityStatistics = entityStatistics;
+ }
+
+ @SuppressWarnings("unused")
+ private Statistics() {
+ }
+
+ @NotNull
+ public static BukkitData.Statistics adapt(@NotNull Player player) {
+ return new BukkitData.Statistics(
+ // Generic (untyped) stats
+ Arrays.stream(Statistic.values())
+ .filter(stat -> stat.getType() == Statistic.Type.UNTYPED)
+ .filter(stat -> player.getStatistic(stat) != 0)
+ .map(stat -> Map.entry(stat, player.getStatistic(stat)))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
+
+ // Block stats
+ Arrays.stream(Statistic.values())
+ .filter(stat -> stat.getType() == Statistic.Type.BLOCK)
+ .map(stat -> Map.entry(stat, Arrays.stream(Material.values())
+ .filter(Material::isBlock)
+ .filter(material -> player.getStatistic(stat, material) != 0)
+ .map(material -> Map.entry(material, player.getStatistic(stat, material)))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))))
+ .filter(entry -> !entry.getValue().isEmpty())
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
+
+ // Item stats
+ Arrays.stream(Statistic.values())
+ .filter(stat -> stat.getType() == Statistic.Type.ITEM)
+ .map(stat -> Map.entry(stat, Arrays.stream(Material.values())
+ .filter(Material::isItem)
+ .filter(material -> player.getStatistic(stat, material) != 0)
+ .map(material -> Map.entry(material, player.getStatistic(stat, material)))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))))
+ .filter(entry -> !entry.getValue().isEmpty())
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
+
+ // Entity stats
+ Arrays.stream(Statistic.values())
+ .filter(stat -> stat.getType() == Statistic.Type.ENTITY)
+ .map(stat -> Map.entry(stat, Arrays.stream(EntityType.values())
+ .filter(EntityType::isAlive)
+ .filter(entityType -> player.getStatistic(stat, entityType) != 0)
+ .map(entityType -> Map.entry(entityType, player.getStatistic(stat, entityType)))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))))
+ .filter(entry -> !entry.getValue().isEmpty())
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
+ );
+ }
+
+ @NotNull
+ public static BukkitData.Statistics from(@NotNull StatisticsMap stats) {
+ return new BukkitData.Statistics(
+ stats.genericStats().entrySet().stream().collect(Collectors.toMap(
+ entry -> matchStatistic(entry.getKey()),
+ Map.Entry::getValue
+ )),
+ stats.blockStats().entrySet().stream().collect(Collectors.toMap(
+ entry -> matchStatistic(entry.getKey()),
+ entry -> entry.getValue().entrySet().stream().collect(Collectors.toMap(
+ blockEntry -> Material.matchMaterial(blockEntry.getKey()),
+ Map.Entry::getValue
+ ))
+ )),
+ stats.itemStats().entrySet().stream().collect(Collectors.toMap(
+ entry -> matchStatistic(entry.getKey()),
+ entry -> entry.getValue().entrySet().stream().collect(Collectors.toMap(
+ itemEntry -> Material.matchMaterial(itemEntry.getKey()),
+ Map.Entry::getValue
+ ))
+ )),
+ stats.entityStats().entrySet().stream().collect(Collectors.toMap(
+ entry -> matchStatistic(entry.getKey()),
+ entry -> entry.getValue().entrySet().stream().collect(Collectors.toMap(
+ entityEntry -> matchEntityType(entityEntry.getKey()),
+ Map.Entry::getValue
+ ))
+ ))
+ );
+ }
+
+ @NotNull
+ public static BukkitData.Statistics from(@NotNull Map genericStats,
+ @NotNull Map> blockStats,
+ @NotNull Map> itemStats,
+ @NotNull Map> entityStats) {
+ return new BukkitData.Statistics(genericStats, blockStats, itemStats, entityStats);
+ }
+
+ @NotNull
+ @ApiStatus.Internal
+ public static StatisticsMap createStatisticsMap(@NotNull Map genericStats,
+ @NotNull Map> blockStats,
+ @NotNull Map> itemStats,
+ @NotNull Map> entityStats) {
+ return new StatisticsMap(genericStats, blockStats, itemStats, entityStats);
+ }
+
+ @NotNull
+ private static Statistic matchStatistic(@NotNull String key) {
+ return Arrays.stream(Statistic.values())
+ .filter(stat -> stat.getKey().toString().equals(key))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException(String.format("Invalid statistic key: %s", key)));
+ }
+
+ @NotNull
+ private static EntityType matchEntityType(@NotNull String key) {
+ return Arrays.stream(EntityType.values())
+ .filter(entityType -> entityType.getKey().toString().equals(key))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException(String.format("Invalid entity type key: %s", key)));
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ untypedStatistics.forEach((stat, value) -> applyStat(user, stat, null, value));
+ blockStatistics.forEach((stat, m) -> m.forEach((block, value) -> applyStat(user, stat, block, value)));
+ itemStatistics.forEach((stat, m) -> m.forEach((item, value) -> applyStat(user, stat, item, value)));
+ entityStatistics.forEach((stat, m) -> m.forEach((entity, value) -> applyStat(user, stat, entity, value)));
+ }
+
+ private void applyStat(@NotNull UserDataHolder user, @NotNull Statistic stat, @Nullable Object type, int value) {
+ try {
+ final Player player = ((BukkitUser) user).getPlayer();
+ if (type == null) {
+ player.setStatistic(stat, value);
+ } else if (type instanceof Material) {
+ player.setStatistic(stat, (Material) type, value);
+ } else if (type instanceof EntityType) {
+ player.setStatistic(stat, (EntityType) type, value);
+ }
+ } catch (IllegalArgumentException ignored) {
+ }
+ }
+
+ @NotNull
+ @Override
+ public Map getGenericStatistics() {
+ return untypedStatistics.entrySet().stream().collect(
+ TreeMap::new,
+ (m, e) -> m.put(e.getKey().getKey().toString(), e.getValue()), TreeMap::putAll
+ );
+ }
+
+ @NotNull
+ @Override
+ public Map> getBlockStatistics() {
+ return blockStatistics.entrySet().stream().collect(
+ TreeMap::new,
+ (m, e) -> m.put(e.getKey().getKey().toString(), e.getValue().entrySet().stream().collect(
+ TreeMap::new,
+ (m2, e2) -> m2.put(e2.getKey().getKey().toString(), e2.getValue()), TreeMap::putAll
+ )), TreeMap::putAll
+ );
+ }
+
+ @NotNull
+ @Override
+ public Map> getItemStatistics() {
+ return itemStatistics.entrySet().stream().collect(
+ TreeMap::new,
+ (m, e) -> m.put(e.getKey().getKey().toString(), e.getValue().entrySet().stream().collect(
+ TreeMap::new,
+ (m2, e2) -> m2.put(e2.getKey().getKey().toString(), e2.getValue()), TreeMap::putAll
+ )), TreeMap::putAll
+ );
+ }
+
+ @NotNull
+ @Override
+ public Map> getEntityStatistics() {
+ return entityStatistics.entrySet().stream().collect(
+ TreeMap::new,
+ (m, e) -> m.put(e.getKey().getKey().toString(), e.getValue().entrySet().stream().collect(
+ TreeMap::new,
+ (m2, e2) -> m2.put(e2.getKey().getKey().toString(), e2.getValue()), TreeMap::putAll
+ )), TreeMap::putAll
+ );
+ }
+
+ @NotNull
+ protected StatisticsMap getStatisticsSet() {
+ return new StatisticsMap(
+ getGenericStatistics(),
+ getBlockStatistics(),
+ getItemStatistics(),
+ getEntityStatistics()
+ );
+ }
+
+ public record StatisticsMap(
+ @SerializedName("generic") @NotNull Map genericStats,
+ @SerializedName("blocks") @NotNull Map> blockStats,
+ @SerializedName("items") @NotNull Map> itemStats,
+ @SerializedName("entities") @NotNull Map> entityStats
+ ) {
+ }
+
+ }
+
+ public static class PersistentData extends BukkitData implements Data.PersistentData {
+ private final NBTCompound persistentData;
+
+ private PersistentData(@NotNull NBTCompound persistentData) {
+ this.persistentData = persistentData;
+ }
+
+ @NotNull
+ public static BukkitData.PersistentData adapt(@NotNull PersistentDataContainer persistentData) {
+ return new BukkitData.PersistentData(new NBTPersistentDataContainer(persistentData));
+ }
+
+ @NotNull
+ public static BukkitData.PersistentData from(@NotNull NBTCompound compound) {
+ return new BukkitData.PersistentData(compound);
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ final NBTPersistentDataContainer container = new NBTPersistentDataContainer(
+ user.getPlayer().getPersistentDataContainer()
+ );
+ container.clearNBT();
+ container.mergeCompound(persistentData);
+ }
+
+ @NotNull
+ public NBTCompound getPersistentData() {
+ return persistentData;
+ }
+
+ }
+
+ public static class Health extends BukkitData implements Data.Health, Adaptable {
+ @SerializedName("health")
+ private double health;
+ @SerializedName("max_health")
+ private double maxHealth;
+ @SerializedName("health_scale")
+ private double healthScale;
+
+ private Health(double health, double maxHealth, double healthScale) {
+ this.health = health;
+ this.maxHealth = maxHealth;
+ this.healthScale = healthScale;
+ }
+
+ @SuppressWarnings("unused")
+ private Health() {
+ }
+
+ @NotNull
+ public static BukkitData.Health from(double health, double maxHealth, double healthScale) {
+ return new BukkitData.Health(health, maxHealth, healthScale);
+ }
+
+ @NotNull
+ public static BukkitData.Health adapt(@NotNull Player player) {
+ return from(
+ player.getHealth(),
+ Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH),
+ "Missing max health attribute").getValue(),
+ player.getHealthScale()
+ );
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ final Player player = user.getPlayer();
+
+ // Set base max health
+ final AttributeInstance maxHealthAttribute = Objects.requireNonNull(
+ player.getAttribute(Attribute.GENERIC_MAX_HEALTH), "Missing max health attribute");
+ double currentMaxHealth = maxHealthAttribute.getBaseValue();
+ if (maxHealth != 0d) {
+ maxHealthAttribute.setBaseValue(maxHealth);
+ currentMaxHealth = maxHealth;
+ }
+
+ // Set health
+ final double currentHealth = player.getHealth();
+ if (health != currentHealth) {
+ final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : health;
+ try {
+ player.setHealth(Math.min(healthToSet, currentMaxHealth));
+ } catch (IllegalArgumentException e) {
+ plugin.log(Level.WARNING, "Failed to set player health", e);
+ }
+ }
+
+ // Set health scale
+ try {
+ if (healthScale != 0d) {
+ player.setHealthScale(healthScale);
+ } else {
+ player.setHealthScale(maxHealth);
+ }
+ player.setHealthScaled(healthScale != 0D);
+ } catch (IllegalArgumentException e) {
+ plugin.log(Level.WARNING, "Failed to set player health scale", e);
+ }
+ }
+
+ @Override
+ public double getHealth() {
+ return health;
+ }
+
+ @Override
+ public void setHealth(double health) {
+ this.health = health;
+ }
+
+ @Override
+ public double getMaxHealth() {
+ return maxHealth;
+ }
+
+ @Override
+ public void setMaxHealth(double maxHealth) {
+ this.maxHealth = maxHealth;
+ }
+
+ @Override
+ public double getHealthScale() {
+ return healthScale;
+ }
+
+ @Override
+ public void setHealthScale(double healthScale) {
+ this.healthScale = healthScale;
+ }
+
+ }
+
+ public static class Hunger extends BukkitData implements Data.Hunger, Adaptable {
+
+ @SerializedName("food_level")
+ private int foodLevel;
+ @SerializedName("saturation")
+ private float saturation;
+ @SerializedName("exhaustion")
+ private float exhaustion;
+
+ private Hunger(int foodLevel, float saturation, float exhaustion) {
+ this.foodLevel = foodLevel;
+ this.saturation = saturation;
+ this.exhaustion = exhaustion;
+ }
+
+ @SuppressWarnings("unused")
+ private Hunger() {
+ }
+
+ @NotNull
+ public static BukkitData.Hunger adapt(@NotNull Player player) {
+ return from(player.getFoodLevel(), player.getSaturation(), player.getExhaustion());
+ }
+
+ @NotNull
+ public static BukkitData.Hunger from(int foodLevel, float saturation, float exhaustion) {
+ return new BukkitData.Hunger(foodLevel, saturation, exhaustion);
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ final Player player = user.getPlayer();
+ player.setFoodLevel(foodLevel);
+ player.setSaturation(saturation);
+ player.setExhaustion(exhaustion);
+ }
+
+ @Override
+ public int getFoodLevel() {
+ return foodLevel;
+ }
+
+ @Override
+ public void setFoodLevel(int foodLevel) {
+ this.foodLevel = foodLevel;
+ }
+
+ @Override
+ public float getSaturation() {
+ return saturation;
+ }
+
+ @Override
+ public void setSaturation(float saturation) {
+ this.saturation = saturation;
+ }
+
+ @Override
+ public float getExhaustion() {
+ return exhaustion;
+ }
+
+ @Override
+ public void setExhaustion(float exhaustion) {
+ this.exhaustion = exhaustion;
+ }
+ }
+
+ public static class Experience extends BukkitData implements Data.Experience, Adaptable {
+
+ @SerializedName("total_experience")
+ private int totalExperience;
+
+ @SerializedName("exp_level")
+ private int expLevel;
+
+ @SerializedName("exp_progress")
+ private float expProgress;
+
+ private Experience(int totalExperience, int expLevel, float expProgress) {
+ this.totalExperience = totalExperience;
+ this.expLevel = expLevel;
+ this.expProgress = expProgress;
+ }
+
+ @SuppressWarnings("unused")
+ private Experience() {
+ }
+
+ @NotNull
+ public static BukkitData.Experience from(int totalExperience, int expLevel, float expProgress) {
+ return new BukkitData.Experience(totalExperience, expLevel, expProgress);
+ }
+
+ @NotNull
+ public static BukkitData.Experience adapt(@NotNull Player player) {
+ return from(player.getTotalExperience(), player.getLevel(), player.getExp());
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ final Player player = user.getPlayer();
+ player.setTotalExperience(totalExperience);
+ player.setLevel(expLevel);
+ player.setExp(expProgress);
+ }
+
+ @Override
+ public int getTotalExperience() {
+ return totalExperience;
+ }
+
+ @Override
+ public void setTotalExperience(int totalExperience) {
+ this.totalExperience = totalExperience;
+ }
+
+ @Override
+ public int getExpLevel() {
+ return expLevel;
+ }
+
+ @Override
+ public void setExpLevel(int expLevel) {
+ this.expLevel = expLevel;
+ }
+
+ @Override
+ public float getExpProgress() {
+ return expProgress;
+ }
+
+ @Override
+ public void setExpProgress(float expProgress) {
+ this.expProgress = expProgress;
+ }
+
+ }
+
+ public static class GameMode extends BukkitData implements Data.GameMode, Adaptable {
+
+ @SerializedName("game_mode")
+ private String gameMode;
+ @SerializedName("allow_flight")
+ private boolean allowFlight;
+ @SerializedName("is_flying")
+ private boolean isFlying;
+
+ private GameMode(@NotNull String gameMode, boolean allowFlight, boolean isFlying) {
+ this.gameMode = gameMode;
+ this.allowFlight = allowFlight;
+ this.isFlying = isFlying;
+ }
+
+ @NotNull
+ public static BukkitData.GameMode from(@NotNull String gameMode, boolean allowFlight, boolean isFlying) {
+ return new BukkitData.GameMode(gameMode, allowFlight, isFlying);
+ }
+
+ @NotNull
+ public static BukkitData.GameMode adapt(@NotNull Player player) {
+ return from(player.getGameMode().name(), player.getAllowFlight(), player.isFlying());
+ }
+
+ @Override
+ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
+ final Player player = user.getPlayer();
+ player.setGameMode(org.bukkit.GameMode.valueOf(gameMode));
+ player.setAllowFlight(allowFlight);
+ player.setFlying(isFlying);
+ }
+
+ @NotNull
+ @Override
+ public String getGameMode() {
+ return gameMode;
+ }
+
+ @Override
+ public void setGameMode(@NotNull String gameMode) {
+ this.gameMode = gameMode;
+ }
+
+ @Override
+ public boolean getAllowFlight() {
+ return allowFlight;
+ }
+
+ @Override
+ public void setAllowFlight(boolean allowFlight) {
+ this.allowFlight = allowFlight;
+ }
+
+ @Override
+ public boolean getIsFlying() {
+ return isFlying;
+ }
+
+ @Override
+ public void setIsFlying(boolean isFlying) {
+ this.isFlying = isFlying;
+ }
+
+ }
+
+}
diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitInventoryMap.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitInventoryMap.java
deleted file mode 100644
index 7175e1c4..00000000
--- a/bukkit/src/main/java/net/william278/husksync/data/BukkitInventoryMap.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * This file is part of HuskSync, licensed under the Apache License 2.0.
- *
- * Copyright (c) William278
- * 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 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 getOffHand() {
- return getItemAt(40);
- }
-
- public Optional getHelmet() {
- return getItemAt(39);
- }
-
- public Optional getChestplate() {
- return getItemAt(38);
- }
-
- public Optional getLeggings() {
- return getItemAt(37);
- }
-
- public Optional 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;
- }
-
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitMapHandler.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitMapHandler.java
deleted file mode 100644
index f603aac4..00000000
--- a/bukkit/src/main/java/net/william278/husksync/data/BukkitMapHandler.java
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * This file is part of HuskSync, licensed under the Apache License 2.0.
- *
- * Copyright (c) William278
- * 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);
- }
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitPersistentTypeMapping.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitPersistentTypeMapping.java
deleted file mode 100644
index 4102842f..00000000
--- a/bukkit/src/main/java/net/william278/husksync/data/BukkitPersistentTypeMapping.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * This file is part of HuskSync, licensed under the Apache License 2.0.
- *
- * Copyright (c) William278
- * 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(PersistentDataTagType type, PersistentDataType 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 bukkitType) {
- this.type = type;
- this.bukkitType = bukkitType;
- }
-
- @NotNull
- public PersistentDataTag 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> getMapping(@NotNull PersistentDataTagType type) {
- for (BukkitPersistentTypeMapping, ?> mapping : PRIMITIVE_TYPE_MAPPINGS) {
- if (mapping.type().equals(type)) {
- return Optional.of(mapping);
- }
- }
- return Optional.empty();
- }
-
-
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java
index 1c4b57ac..0f6e0232 100644
--- a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java
+++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java
@@ -19,235 +19,247 @@
package net.william278.husksync.data;
-import net.william278.husksync.BukkitHuskSync;
-import net.william278.husksync.config.Settings;
+import com.google.gson.reflect.TypeToken;
+import de.tr7zw.changeme.nbtapi.NBT;
+import de.tr7zw.changeme.nbtapi.NBTContainer;
+import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.adapter.Adaptable;
+import net.william278.husksync.api.HuskSyncAPI;
import org.bukkit.inventory.ItemStack;
-import org.bukkit.potion.PotionEffect;
-import org.bukkit.util.io.BukkitObjectInputStream;
-import org.bukkit.util.io.BukkitObjectOutputStream;
+import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.logging.Level;
+import java.util.List;
+
+import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
public class BukkitSerializer {
- /**
- * Returns a serialized array of {@link ItemStack}s
- *
- * @param inventoryContents The contents of the inventory
- * @return The serialized inventory contents
- */
- public static CompletableFuture serializeItemStackArray(@NotNull ItemStack[] inventoryContents)
- throws DataSerializationException {
- return CompletableFuture.supplyAsync(() -> {
- // Return an empty string if there is no inventory item data to serialize
- if (inventoryContents.length == 0) {
- return "";
- }
-
- // Create an output stream that will be encoded into base 64
- ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
-
- try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
- // Define the length of the inventory array to serialize
- bukkitOutputStream.writeInt(inventoryContents.length);
-
- // Write each serialize each ItemStack to the output stream
- final boolean persistLockedMaps = BukkitHuskSync.getInstance().getSettings().getSynchronizationFeature(Settings.SynchronizationFeature.LOCKED_MAPS);
- for (ItemStack inventoryItem : inventoryContents) {
- if (persistLockedMaps) {
- BukkitMapHandler.persistMapData(inventoryItem);
- }
- bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
- }
-
- // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
- return Base64Coder.encodeLines(byteOutputStream.toByteArray());
- } catch (IOException e) {
- BukkitHuskSync.getInstance().log(Level.SEVERE, "Failed to serialize item stack data", e);
- throw new DataSerializationException("Failed to serialize item stack data", e);
- }
- });
+ protected final HuskSync plugin;
+
+ private BukkitSerializer(@NotNull HuskSync plugin) {
+ this.plugin = plugin;
}
- /**
- * Returns a {@link BukkitInventoryMap} from a serialized array of ItemStacks representing the contents of a player's inventory.
- *
- * @param serializedPlayerInventory The serialized {@link ItemStack} inventory array
- * @return The deserialized ItemStacks, mapped for convenience as a {@link BukkitInventoryMap}
- * @throws DataSerializationException If the serialized item stack array could not be deserialized
- */
- public static CompletableFuture deserializeInventory(@NotNull String serializedPlayerInventory)
- throws DataSerializationException {
- return CompletableFuture.supplyAsync(() -> new BukkitInventoryMap(deserializeItemStackArray(serializedPlayerInventory).join()));
+ @SuppressWarnings("unused")
+ public BukkitSerializer(@NotNull HuskSyncAPI api) {
+ this.plugin = api.getPlugin();
}
- /**
- * Returns an array of ItemStacks from serialized inventory data.
- *
- * @param serializeItemStackArray The serialized {@link ItemStack} array
- * @return The deserialized array of {@link ItemStack}s
- * @throws DataSerializationException If the serialized item stack array could not be deserialized
- * @implNote Empty slots will be represented by {@code null}
- */
- public static CompletableFuture deserializeItemStackArray(@NotNull String serializeItemStackArray)
- throws DataSerializationException {
- return CompletableFuture.supplyAsync(() -> {
- // Return empty array if there is no inventory data (set the player as having an empty inventory)
- if (serializeItemStackArray.isEmpty()) {
- return new ItemStack[0];
- }
-
- // Create a byte input stream to read the serialized data
- try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(serializeItemStackArray))) {
- try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
- // Read the length of the Bukkit input stream and set the length of the array to this value
- ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
-
- // Set the ItemStacks in the array from deserialized ItemStack data
- int slotIndex = 0;
- final boolean persistLockedMaps = BukkitHuskSync.getInstance().getSettings().getSynchronizationFeature(Settings.SynchronizationFeature.LOCKED_MAPS);
- for (ItemStack ignored : inventoryContents) {
- final ItemStack deserialized = deserializeItemStack(bukkitInputStream.readObject());
- if (persistLockedMaps) {
- BukkitMapHandler.setMapRenderer(deserialized);
- }
- inventoryContents[slotIndex] = deserialized;
- slotIndex++;
- }
-
- // Return the finished, serialized inventory contents
- return inventoryContents;
- }
- } catch (IOException | ClassNotFoundException e) {
- BukkitHuskSync.getInstance().log(Level.SEVERE, "Failed to deserialize item stack data", e);
- throw new DataSerializationException("Failed to deserialize item stack data", e);
- }
- });
+ @ApiStatus.Internal
+ @NotNull
+ public HuskSync getPlugin() {
+ return plugin;
}
- /**
- * Returns the serialized version of an {@link ItemStack} as a string to object Map
- *
- * @param item The {@link ItemStack} to serialize
- * @return The serialized {@link ItemStack}
- */
- @Nullable
- private static Map serializeItemStack(@Nullable ItemStack item) {
- return item != null ? item.serialize() : null;
+ public static class Inventory extends BukkitSerializer implements Serializer {
+ private static final String ITEMS_TAG = "items";
+ private static final String HELD_ITEM_SLOT_TAG = "held_item_slot";
+
+ public Inventory(@NotNull HuskSync plugin) {
+ super(plugin);
+ }
+
+ @Override
+ public BukkitData.Items.Inventory deserialize(@NotNull String serialized) throws DeserializationException {
+ final ReadWriteNBT root = NBT.parseNBT(serialized);
+ final ItemStack[] items = root.getItemStackArray(ITEMS_TAG);
+ final int heldItemSlot = root.getInteger(HELD_ITEM_SLOT_TAG);
+ return BukkitData.Items.Inventory.from(
+ items == null ? new ItemStack[INVENTORY_SLOT_COUNT] : items,
+ heldItemSlot
+ );
+ }
+
+ @NotNull
+ @Override
+ public String serialize(@NotNull BukkitData.Items.Inventory data) throws SerializationException {
+ final ReadWriteNBT root = NBT.createNBTObject();
+ root.setItemStackArray(ITEMS_TAG, data.getContents());
+ root.setInteger(HELD_ITEM_SLOT_TAG, data.getHeldItemSlot());
+ return root.toString();
+ }
+
}
- /**
- * Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream}
- *
- * @param serializedItemStack The serialized item stack; a String-Object map
- * @return The deserialized {@link ItemStack}
- */
- @SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
- @Nullable
- private static ItemStack deserializeItemStack(@Nullable Object serializedItemStack) {
- return serializedItemStack != null ? ItemStack.deserialize((Map) serializedItemStack) : null;
+ public static class EnderChest extends BukkitSerializer implements Serializer {
+
+ public EnderChest(@NotNull HuskSync plugin) {
+ super(plugin);
+ }
+
+ @Override
+ public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) throws DeserializationException {
+ final ItemStack[] items = NBT.itemStackArrayFromNBT(NBT.parseNBT(serialized));
+ return items == null ? BukkitData.Items.EnderChest.empty() : BukkitData.Items.EnderChest.adapt(items);
+ }
+
+ @NotNull
+ @Override
+ public String serialize(@NotNull BukkitData.Items.EnderChest data) throws SerializationException {
+ return NBT.itemStackArrayToNBT(data.getContents()).toString();
+ }
}
- /**
- * Returns a serialized array of {@link PotionEffect}s
- *
- * @param potionEffects The potion effect array
- * @return The serialized potion effects
- */
- public static CompletableFuture serializePotionEffectArray(@NotNull PotionEffect[] potionEffects) throws DataSerializationException {
- return CompletableFuture.supplyAsync(() -> {
- // Return an empty string if there are no effects to serialize
- if (potionEffects.length == 0) {
- return "";
- }
-
- // Create an output stream that will be encoded into base 64
- ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
-
- try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
- // Define the length of the potion effect array to serialize
- bukkitOutputStream.writeInt(potionEffects.length);
-
- // Write each serialize each PotionEffect to the output stream
- for (PotionEffect potionEffect : potionEffects) {
- bukkitOutputStream.writeObject(serializePotionEffect(potionEffect));
- }
-
- // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
- return Base64Coder.encodeLines(byteOutputStream.toByteArray());
- } catch (IOException e) {
- BukkitHuskSync.getInstance().log(Level.SEVERE, "Failed to serialize potion effect data", e);
- throw new DataSerializationException("Failed to serialize potion effect data", e);
- }
- });
+ public static class PotionEffects extends BukkitSerializer implements Serializer {
+
+ private static final TypeToken> TYPE = new TypeToken<>() {
+ };
+
+ public PotionEffects(@NotNull HuskSync plugin) {
+ super(plugin);
+ }
+
+ @Override
+ public BukkitData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException {
+ return BukkitData.PotionEffects.adapt(
+ plugin.getGson().fromJson(serialized, TYPE.getType())
+ );
+ }
+
+ @NotNull
+ @Override
+ public String serialize(@NotNull BukkitData.PotionEffects element) throws SerializationException {
+ return plugin.getGson().toJson(element.getActiveEffects());
+ }
+
}
- /**
- * Returns an array of ItemStacks from serialized potion effect data
- *
- * @param potionEffectData The serialized {@link PotionEffect} array
- * @return The {@link PotionEffect}s
- */
- public static CompletableFuture deserializePotionEffectArray(@NotNull String potionEffectData) throws DataSerializationException {
- return CompletableFuture.supplyAsync(() -> {
- // Return empty array if there is no potion effect data (don't apply any effects to the player)
- if (potionEffectData.isEmpty()) {
- return new PotionEffect[0];
- }
-
- // Create a byte input stream to read the serialized data
- try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) {
- try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
- // Read the length of the Bukkit input stream and set the length of the array to this value
- PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()];
-
- // Set the potion effects in the array from deserialized PotionEffect data
- int potionIndex = 0;
- for (PotionEffect ignored : potionEffects) {
- potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject());
- potionIndex++;
- }
-
- // Return the finished, serialized potion effect array
- return potionEffects;
- }
- } catch (IOException | ClassNotFoundException e) {
- BukkitHuskSync.getInstance().log(Level.SEVERE, "Failed to deserialize potion effect data", e);
- throw new DataSerializationException("Failed to deserialize potion effects", e);
- }
- });
+ public static class Advancements extends BukkitSerializer implements Serializer {
+
+ private static final TypeToken> TYPE = new TypeToken<>() {
+ };
+
+ public Advancements(@NotNull HuskSync plugin) {
+ super(plugin);
+ }
+
+ @Override
+ public BukkitData.Advancements deserialize(@NotNull String serialized) throws DeserializationException {
+ return BukkitData.Advancements.from(
+ plugin.getGson().fromJson(serialized, TYPE.getType())
+ );
+ }
+
+ @NotNull
+ @Override
+ public String serialize(@NotNull BukkitData.Advancements element) throws SerializationException {
+ return plugin.getGson().toJson(element.getCompleted());
+ }
}
- /**
- * Returns the serialized version of an {@link ItemStack} as a string to object Map
- *
- * @param potionEffect The {@link ItemStack} to serialize
- * @return The serialized {@link ItemStack}
- */
- @Nullable
- private static Map serializePotionEffect(@Nullable PotionEffect potionEffect) {
- return potionEffect != null ? potionEffect.serialize() : null;
+ public static class Location extends BukkitSerializer implements Serializer {
+
+ public Location(@NotNull HuskSync plugin) {
+ super(plugin);
+ }
+
+ @Override
+ public BukkitData.Location deserialize(@NotNull String serialized) throws DeserializationException {
+ return plugin.getDataAdapter().fromJson(serialized, BukkitData.Location.class);
+ }
+
+ @NotNull
+ @Override
+ public String serialize(@NotNull BukkitData.Location element) throws SerializationException {
+ return plugin.getDataAdapter().toJson(element);
+ }
}
- /**
- * Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream}
- *
- * @param serializedPotionEffect The serialized potion effect; a String-Object map
- * @return The deserialized {@link PotionEffect}
- */
- @SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
- @Nullable
- private static PotionEffect deserializePotionEffect(@Nullable Object serializedPotionEffect) {
- return serializedPotionEffect != null ? new PotionEffect((Map) serializedPotionEffect) : null;
+ public static class Statistics extends BukkitSerializer implements Serializer {
+
+ public Statistics(@NotNull HuskSync plugin) {
+ super(plugin);
+ }
+
+ @Override
+ public BukkitData.Statistics deserialize(@NotNull String serialized) throws DeserializationException {
+ return BukkitData.Statistics.from(plugin.getGson().fromJson(
+ serialized,
+ BukkitData.Statistics.StatisticsMap.class
+ ));
+ }
+
+ @NotNull
+ @Override
+ public String serialize(@NotNull BukkitData.Statistics element) throws SerializationException {
+ return plugin.getGson().toJson(element.getStatisticsSet());
+ }
+
}
+ public static class PersistentData extends BukkitSerializer implements Serializer {
+
+ public PersistentData(@NotNull HuskSync plugin) {
+ super(plugin);
+ }
+
+ @Override
+ public BukkitData.PersistentData deserialize(@NotNull String serialized) throws DeserializationException {
+ return BukkitData.PersistentData.from(new NBTContainer(serialized));
+ }
+
+ @NotNull
+ @Override
+ public String serialize(@NotNull BukkitData.PersistentData element) throws SerializationException {
+ return element.getPersistentData().toString();
+ }
+
+ }
+
+ public static class Health extends Json implements Serializer {
+
+ public Health(@NotNull HuskSync plugin) {
+ super(plugin, BukkitData.Health.class);
+ }
+
+ }
+
+ public static class Hunger extends Json implements Serializer {
+
+ public Hunger(@NotNull HuskSync plugin) {
+ super(plugin, BukkitData.Hunger.class);
+ }
+
+ }
+
+ public static class Experience extends Json implements Serializer {
+
+ public Experience(@NotNull HuskSync plugin) {
+ super(plugin, BukkitData.Experience.class);
+ }
+
+ }
+
+ public static class GameMode extends Json implements Serializer {
+
+ public GameMode(@NotNull HuskSync plugin) {
+ super(plugin, BukkitData.GameMode.class);
+ }
+
+ }
+
+ public static abstract class Json extends BukkitSerializer implements Serializer {
+
+ private final Class type;
+
+ protected Json(@NotNull HuskSync plugin, Class type) {
+ super(plugin);
+ this.type = type;
+ }
+
+ @Override
+ public T deserialize(@NotNull String serialized) throws DeserializationException {
+ return plugin.getDataAdapter().fromJson(serialized, type);
+ }
+
+ @NotNull
+ @Override
+ public String serialize(@NotNull T element) throws SerializationException {
+ return plugin.getDataAdapter().toJson(element);
+ }
+
+ }
}
diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java
new file mode 100644
index 00000000..7754ad8d
--- /dev/null
+++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java
@@ -0,0 +1,151 @@
+/*
+ * This file is part of HuskSync, licensed under the Apache License 2.0.
+ *
+ * Copyright (c) William278
+ * 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 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 getEnderChest() {
+ return Optional.of(BukkitData.Items.EnderChest.adapt(
+ getMapPersister().persistLockedMaps(getBukkitPlayer().getEnderChest().getContents(), getBukkitPlayer())
+ ));
+ }
+
+ @NotNull
+ @Override
+ default Optional getPotionEffects() {
+ return Optional.of(BukkitData.PotionEffects.from(getBukkitPlayer().getActivePotionEffects()));
+ }
+
+ @NotNull
+ @Override
+ default Optional getAdvancements() {
+ return Optional.of(BukkitData.Advancements.adapt(getBukkitPlayer()));
+ }
+
+ @NotNull
+ @Override
+ default Optional getLocation() {
+ return Optional.of(BukkitData.Location.adapt(getBukkitPlayer().getLocation()));
+ }
+
+ @NotNull
+ @Override
+ default Optional getStatistics() {
+ return Optional.of(BukkitData.Statistics.adapt(getBukkitPlayer()));
+ }
+
+ @NotNull
+ @Override
+ default Optional getHealth() {
+ return Optional.of(BukkitData.Health.adapt(getBukkitPlayer()));
+ }
+
+ @NotNull
+ @Override
+ default Optional getHunger() {
+ return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
+ }
+
+ @NotNull
+ @Override
+ default Optional getExperience() {
+ return Optional.of(BukkitData.Experience.adapt(getBukkitPlayer()));
+ }
+
+ @NotNull
+ @Override
+ default Optional getGameMode() {
+ return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer()));
+ }
+
+ @NotNull
+ @Override
+ default Optional getPersistentData() {
+ return Optional.of(BukkitData.PersistentData.adapt(getBukkitPlayer().getPersistentDataContainer()));
+ }
+
+ boolean isDead();
+
+ @NotNull
+ Player getBukkitPlayer();
+
+ @NotNull
+ Map getCustomDataStore();
+
+ @NotNull
+ default BukkitMapPersister getMapPersister() {
+ return (BukkitHuskSync) getPlugin();
+ }
+
+
+}
diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSaveEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSaveEvent.java
index 2a8ef3cd..4159dcba 100644
--- a/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSaveEvent.java
+++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSaveEvent.java
@@ -19,9 +19,9 @@
package net.william278.husksync.event;
-import net.william278.husksync.data.DataSaveCause;
-import net.william278.husksync.data.UserData;
-import net.william278.husksync.player.User;
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.data.DataSnapshot;
+import net.william278.husksync.user.User;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
@@ -29,16 +29,15 @@ import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
- private boolean cancelled = false;
- private UserData userData;
+ private final HuskSync plugin;
+ private final DataSnapshot.Packed snapshot;
private final User user;
- private final DataSaveCause saveCause;
+ private boolean cancelled = false;
- protected BukkitDataSaveEvent(@NotNull User user, @NotNull UserData userData,
- @NotNull DataSaveCause saveCause) {
+ protected BukkitDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed snapshot, @NotNull HuskSync plugin) {
this.user = user;
- this.userData = userData;
- this.saveCause = saveCause;
+ this.snapshot = snapshot;
+ this.plugin = plugin;
}
@Override
@@ -58,18 +57,15 @@ public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, C
}
@Override
- public @NotNull UserData getUserData() {
- return userData;
- }
-
- @Override
- public void setUserData(@NotNull UserData userData) {
- this.userData = userData;
+ @NotNull
+ public DataSnapshot.Packed getData() {
+ return snapshot;
}
+ @NotNull
@Override
- public @NotNull DataSaveCause getSaveCause() {
- return saveCause;
+ public HuskSync getPlugin() {
+ return plugin;
}
@NotNull
diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java
index 5e3a54e2..29ec1ccf 100644
--- a/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java
+++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java
@@ -19,14 +19,10 @@
package net.william278.husksync.event;
-import net.william278.husksync.BukkitHuskSync;
-import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
-import java.util.concurrent.CompletableFuture;
-
@SuppressWarnings("unused")
public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event {
@@ -35,21 +31,6 @@ public abstract class BukkitEvent extends Event implements net.william278.husksy
protected BukkitEvent() {
}
- @Override
- public CompletableFuture fire() {
- final CompletableFuture eventFireFuture = new CompletableFuture<>();
- // Don't fire events while the server is shutting down
- if (!BukkitHuskSync.getInstance().isEnabled()) {
- eventFireFuture.complete(this);
- } else {
- Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
- Bukkit.getServer().getPluginManager().callEvent(this);
- eventFireFuture.complete(this);
- });
- }
- return eventFireFuture;
- }
-
@NotNull
@Override
public HandlerList getHandlers() {
diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java
deleted file mode 100644
index 5bf3c9c5..00000000
--- a/bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * This file is part of HuskSync, licensed under the Apache License 2.0.
- *
- * Copyright (c) William278
- * 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 firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData) {
- return new BukkitPreSyncEvent(((BukkitPlayer) user).getPlayer(), userData).fire();
- }
-
- @Override
- public CompletableFuture 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();
- }
-
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitEventDispatcher.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitEventDispatcher.java
new file mode 100644
index 00000000..28a78eae
--- /dev/null
+++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitEventDispatcher.java
@@ -0,0 +1,54 @@
+/*
+ * This file is part of HuskSync, licensed under the Apache License 2.0.
+ *
+ * Copyright (c) William278
+ * 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 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());
+ }
+
+}
diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java
index 8664a0f5..6b0df5bd 100644
--- a/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java
+++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java
@@ -19,40 +19,25 @@
package net.william278.husksync.event;
-import net.william278.husksync.BukkitHuskSync;
-import net.william278.husksync.player.BukkitPlayer;
-import net.william278.husksync.player.OnlineUser;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
+import net.william278.husksync.user.OnlineUser;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
-import java.util.concurrent.CompletableFuture;
-
@SuppressWarnings("unused")
public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
- protected final Player player;
+ protected final OnlineUser player;
- protected BukkitPlayerEvent(@NotNull Player player) {
+ protected BukkitPlayerEvent(@NotNull OnlineUser player) {
this.player = player;
}
@Override
+ @NotNull
public OnlineUser getUser() {
- return BukkitPlayer.adapt(player);
- }
-
- @Override
- public CompletableFuture fire() {
- final CompletableFuture eventFireFuture = new CompletableFuture<>();
- Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
- Bukkit.getServer().getPluginManager().callEvent(this);
- eventFireFuture.complete(this);
- });
- return eventFireFuture;
+ return player;
}
@NotNull
diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java
index a0ce94b8..9d5431e3 100644
--- a/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java
+++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java
@@ -19,8 +19,9 @@
package net.william278.husksync.event;
-import net.william278.husksync.data.UserData;
-import org.bukkit.entity.Player;
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.data.DataSnapshot;
+import net.william278.husksync.user.OnlineUser;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
@@ -28,12 +29,14 @@ import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
+ private final HuskSync plugin;
+ private final DataSnapshot.Packed data;
private boolean cancelled = false;
- private UserData userData;
- protected BukkitPreSyncEvent(@NotNull Player player, @NotNull UserData userData) {
+ protected BukkitPreSyncEvent(@NotNull OnlineUser player, @NotNull DataSnapshot.Packed data, @NotNull HuskSync plugin) {
super(player);
- this.userData = userData;
+ this.data = data;
+ this.plugin = plugin;
}
@Override
@@ -47,13 +50,15 @@ public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEven
}
@Override
- public @NotNull UserData getUserData() {
- return userData;
+ @NotNull
+ public DataSnapshot.Packed getData() {
+ return data;
}
+ @NotNull
@Override
- public void setUserData(@NotNull UserData userData) {
- this.userData = userData;
+ public HuskSync getPlugin() {
+ return plugin;
}
@NotNull
diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompleteEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompleteEvent.java
index 18abd1b5..840c08c8 100644
--- a/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompleteEvent.java
+++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompleteEvent.java
@@ -19,7 +19,8 @@
package net.william278.husksync.event;
-import org.bukkit.entity.Player;
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.user.OnlineUser;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
@@ -27,7 +28,7 @@ import org.jetbrains.annotations.NotNull;
public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
- protected BukkitSyncCompleteEvent(@NotNull Player player) {
+ protected BukkitSyncCompleteEvent(@NotNull OnlineUser player, @NotNull HuskSync plugin) {
super(player);
}
diff --git a/bukkit/src/main/java/net/william278/husksync/listener/BukkitDeathEventListener.java b/bukkit/src/main/java/net/william278/husksync/listener/BukkitDeathEventListener.java
index 5bb2db75..b6418a1e 100644
--- a/bukkit/src/main/java/net/william278/husksync/listener/BukkitDeathEventListener.java
+++ b/bukkit/src/main/java/net/william278/husksync/listener/BukkitDeathEventListener.java
@@ -19,7 +19,6 @@
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;
@@ -28,25 +27,25 @@ import org.jetbrains.annotations.NotNull;
public interface BukkitDeathEventListener extends Listener {
- boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority);
+ boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority priority);
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
default void onPlayerDeathHighest(@NotNull PlayerDeathEvent event) {
- if (handleEvent(Settings.EventType.DEATH_LISTENER, Settings.EventPriority.HIGHEST)) {
+ if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.HIGHEST)) {
handlePlayerDeath(event);
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
default void onPlayerDeath(@NotNull PlayerDeathEvent event) {
- if (handleEvent(Settings.EventType.DEATH_LISTENER, Settings.EventPriority.NORMAL)) {
+ if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.NORMAL)) {
handlePlayerDeath(event);
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
default void onPlayerDeathLowest(@NotNull PlayerDeathEvent event) {
- if (handleEvent(Settings.EventType.DEATH_LISTENER, Settings.EventPriority.LOWEST)) {
+ if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.LOWEST)) {
handlePlayerDeath(event);
}
}
diff --git a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java
index 516f6732..432a1b67 100644
--- a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java
+++ b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java
@@ -20,12 +20,10 @@
package net.william278.husksync.listener;
import net.william278.husksync.BukkitHuskSync;
-import net.william278.husksync.config.Settings;
-import net.william278.husksync.data.BukkitInventoryMap;
-import net.william278.husksync.data.BukkitSerializer;
-import net.william278.husksync.data.ItemData;
-import net.william278.husksync.player.BukkitPlayer;
-import net.william278.husksync.player.OnlineUser;
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.data.BukkitData;
+import net.william278.husksync.user.BukkitUser;
+import net.william278.husksync.user.OnlineUser;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
@@ -46,12 +44,10 @@ import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.world.WorldSaveEvent;
-import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Locale;
-import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
@@ -65,54 +61,57 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
}
@Override
- public boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority) {
+ public boolean handleEvent(@NotNull ListenerType type, @NotNull Priority priority) {
return plugin.getSettings().getEventPriority(type).equals(priority);
}
@Override
- public void handlePlayerQuit(@NotNull BukkitPlayer bukkitPlayer) {
- final Player player = bukkitPlayer.getPlayer();
- if (!bukkitPlayer.isLocked() && !player.getItemOnCursor().getType().isAir()) {
+ public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) {
+ final Player player = bukkitUser.getPlayer();
+ if (!bukkitUser.isLocked() && !player.getItemOnCursor().getType().isAir()) {
player.getWorld().dropItem(player.getLocation(), player.getItemOnCursor());
player.setItemOnCursor(null);
}
- super.handlePlayerQuit(bukkitPlayer);
+ super.handlePlayerQuit(bukkitUser);
}
@Override
- public void handlePlayerJoin(@NotNull BukkitPlayer bukkitPlayer) {
- super.handlePlayerJoin(bukkitPlayer);
+ public void handlePlayerJoin(@NotNull BukkitUser bukkitUser) {
+ super.handlePlayerJoin(bukkitUser);
}
@Override
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
- final OnlineUser user = BukkitPlayer.adapt(event.getEntity());
+ final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
// If the player is locked or the plugin disabling, clear their drops
- if (cancelPlayerEvent(user.uuid)) {
+ if (cancelPlayerEvent(user.getUuid())) {
event.getDrops().clear();
return;
}
// Handle saving player data snapshots on death
- if (!plugin.getSettings().doSaveOnDeath()) return;
+ if (!plugin.getSettings().doSaveOnDeath()) {
+ return;
+ }
- // Truncate the drops list to the inventory size and save the player's inventory
- final int maxInventorySize = BukkitInventoryMap.INVENTORY_SLOT_COUNT;
+ // Truncate the dropped items list to the inventory size and save the player's inventory
+ final int maxInventorySize = BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
if (event.getDrops().size() > maxInventorySize) {
event.getDrops().subList(maxInventorySize, event.getDrops().size()).clear();
}
- BukkitSerializer.serializeItemStackArray(event.getDrops().toArray(new ItemStack[0]))
- .thenAccept(serializedDrops -> super.saveOnPlayerDeath(user, new ItemData(serializedDrops)));
+ super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(event.getDrops()));
}
@EventHandler(ignoreCancelled = true)
public void onWorldSave(@NotNull WorldSaveEvent event) {
- // Handle saving player data snapshots when the world saves
- if (!plugin.getSettings().doSaveOnWorldSave()) return;
+ if (!plugin.getSettings().doSaveOnWorldSave()) {
+ return;
+ }
- CompletableFuture.runAsync(() -> super.saveOnWorldSave(event.getWorld().getPlayers()
- .stream().map(BukkitPlayer::adapt)
+ // Handle saving player data snapshots when the world saves
+ plugin.runAsync(() -> super.saveOnWorldSave(event.getWorld().getPlayers()
+ .stream().map(player -> BukkitUser.adapt(player, plugin))
.collect(Collectors.toList())));
}
@@ -186,12 +185,18 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onPermissionCommand(@NotNull PlayerCommandPreprocessEvent event) {
- String[] commandArgs = event.getMessage().substring(1).split(" ");
- String commandLabel = commandArgs[0].toLowerCase(Locale.ENGLISH);
+ final String[] commandArgs = event.getMessage().substring(1).split(" ");
+ final String commandLabel = commandArgs[0].toLowerCase(Locale.ENGLISH);
if (blacklistedCommands.contains("*") || blacklistedCommands.contains(commandLabel)) {
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
}
}
+ @NotNull
+ @Override
+ public HuskSync getPlugin() {
+ return plugin;
+ }
+
}
diff --git a/bukkit/src/main/java/net/william278/husksync/listener/BukkitJoinEventListener.java b/bukkit/src/main/java/net/william278/husksync/listener/BukkitJoinEventListener.java
index a2f69776..26144419 100644
--- a/bukkit/src/main/java/net/william278/husksync/listener/BukkitJoinEventListener.java
+++ b/bukkit/src/main/java/net/william278/husksync/listener/BukkitJoinEventListener.java
@@ -19,8 +19,8 @@
package net.william278.husksync.listener;
-import net.william278.husksync.config.Settings;
-import net.william278.husksync.player.BukkitPlayer;
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.user.BukkitUser;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@@ -29,29 +29,32 @@ import org.jetbrains.annotations.NotNull;
public interface BukkitJoinEventListener extends Listener {
- boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority);
+ boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority 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()));
+ if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.HIGHEST)) {
+ handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
@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()));
+ if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.NORMAL)) {
+ handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
@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()));
+ if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.LOWEST)) {
+ handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
- void handlePlayerJoin(@NotNull BukkitPlayer player);
+ void handlePlayerJoin(@NotNull BukkitUser player);
+
+ @NotNull
+ HuskSync getPlugin();
}
diff --git a/bukkit/src/main/java/net/william278/husksync/listener/BukkitQuitEventListener.java b/bukkit/src/main/java/net/william278/husksync/listener/BukkitQuitEventListener.java
index 133513f7..c0eec1c4 100644
--- a/bukkit/src/main/java/net/william278/husksync/listener/BukkitQuitEventListener.java
+++ b/bukkit/src/main/java/net/william278/husksync/listener/BukkitQuitEventListener.java
@@ -19,8 +19,8 @@
package net.william278.husksync.listener;
-import net.william278.husksync.config.Settings;
-import net.william278.husksync.player.BukkitPlayer;
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.user.BukkitUser;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@@ -29,29 +29,32 @@ import org.jetbrains.annotations.NotNull;
public interface BukkitQuitEventListener extends Listener {
- boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority);
+ boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority 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()));
+ if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.HIGHEST)) {
+ handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
@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()));
+ if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.NORMAL)) {
+ handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
@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()));
+ if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.LOWEST)) {
+ handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
- void handlePlayerQuit(@NotNull BukkitPlayer player);
+ void handlePlayerQuit(@NotNull BukkitUser player);
+
+ @NotNull
+ HuskSync getPlugin();
}
diff --git a/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java b/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java
index bd6e2335..5487d6e7 100644
--- a/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java
+++ b/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java
@@ -23,14 +23,16 @@ import com.zaxxer.hikari.HikariDataSource;
import me.william278.husksync.bukkit.data.DataSerializer;
import net.william278.hslmigrator.HSLConverter;
import net.william278.husksync.HuskSync;
-import net.william278.husksync.data.*;
-import net.william278.husksync.player.User;
+import net.william278.husksync.data.BukkitData;
+import net.william278.husksync.data.Data;
+import net.william278.husksync.data.DataSnapshot;
+import net.william278.husksync.user.User;
+import net.william278.husksync.util.BukkitLegacyConverter;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
-import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@@ -51,8 +53,6 @@ public class LegacyMigrator extends Migrator {
private String sourcePlayersTable;
private String sourceDataTable;
- private final String minecraftVersion;
-
public LegacyMigrator(@NotNull HuskSync plugin) {
super(plugin);
this.hslConverter = HSLConverter.getInstance();
@@ -63,17 +63,16 @@ public class LegacyMigrator extends Migrator {
this.sourceDatabase = plugin.getSettings().getMySqlDatabase();
this.sourcePlayersTable = "husksync_players";
this.sourceDataTable = "husksync_data";
- this.minecraftVersion = plugin.getMinecraftVersion().toString();
}
@Override
public CompletableFuture start() {
plugin.log(Level.INFO, "Starting migration of legacy HuskSync v1.x data...");
final long startTime = System.currentTimeMillis();
- return CompletableFuture.supplyAsync(() -> {
+ return plugin.supplyAsync(() -> {
// Wipe the existing database, preparing it for data import
plugin.log(Level.INFO, "Preparing existing database (wiping)...");
- plugin.getDatabase().wipeDatabase().join();
+ plugin.getDatabase().wipeDatabase();
plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
// Create jdbc driver connection url
@@ -135,23 +134,25 @@ public class LegacyMigrator extends Migrator {
plugin.log(Level.INFO, "Converting HuskSync 1.x data to the new user data format (this might take a while)...");
final AtomicInteger playersConverted = new AtomicInteger();
- dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData -> {
- plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
- plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION)
- .exceptionally(exception -> {
- plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage());
- return null;
- })).join();
+ dataToMigrate.forEach(data -> {
+ final DataSnapshot.Packed convertedData = data.toUserData(hslConverter, plugin);
+ plugin.getDatabase().ensureUser(data.user());
+ try {
+ plugin.getDatabase().addSnapshot(data.user(), convertedData);
+ } catch (Throwable e) {
+ plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getUsername() + ": " + e.getMessage());
+ return;
+ }
playersConverted.getAndIncrement();
if (playersConverted.get() % 50 == 0) {
plugin.log(Level.INFO, "Converted legacy data for " + playersConverted + " players...");
}
- }).join());
+ });
plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
return true;
- } catch (Exception e) {
- plugin.log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?");
+ } catch (Throwable e) {
+ plugin.log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?", e);
return false;
}
});
@@ -197,10 +198,10 @@ public class LegacyMigrator extends Migrator {
}) {
plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
- obfuscateDataString(args[1]));
+ obfuscateDataString(args[1]));
} else {
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
- obfuscateDataString(args[1]) + " (is it a valid option?)");
+ obfuscateDataString(args[1]) + " (is it a valid option?)");
}
} else {
plugin.log(Level.INFO, getHelpMenu());
@@ -227,11 +228,11 @@ public class LegacyMigrator extends Migrator {
This will migrate all user data from HuskSync v1.x to
HuskSync v2.x's new format. To perform the migration,
please follow the steps below carefully.
-
+
[!] Existing data in the database will be wiped. [!]
-
+
STEP 1] Please ensure no players are on any servers.
-
+
STEP 2] HuskSync will need to connect to the database
used to hold the existing, legacy HuskSync data.
If this is the same database as the one you are
@@ -251,12 +252,12 @@ public class LegacyMigrator extends Migrator {
using the command:
"husksync migrate legacy set "
(e.g.: "husksync migrate legacy set host 1.2.3.4")
-
+
STEP 3] HuskSync will migrate data into the database
tables configures in the config.yml file of this
server. Please make sure you're happy with this
before proceeding.
-
+
STEP 4] To start the migration, please run:
"husksync migrate legacy start"
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
@@ -277,54 +278,69 @@ public class LegacyMigrator extends Migrator {
@NotNull String serializedAdvancements, @NotNull String serializedLocation) {
@NotNull
- public CompletableFuture toUserData(@NotNull HSLConverter converter,
- @NotNull String minecraftVersion) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- final DataSerializer.StatisticData legacyStatisticData = converter
- .deserializeStatisticData(serializedStatistics);
- final StatisticsData convertedStatisticData = new StatisticsData(
- convertStatisticMap(legacyStatisticData.untypedStatisticValues()),
- convertMaterialStatisticMap(legacyStatisticData.blockStatisticValues()),
- convertMaterialStatisticMap(legacyStatisticData.itemStatisticValues()),
- convertEntityStatisticMap(legacyStatisticData.entityStatisticValues()));
-
- final List convertedAdvancements = converter
- .deserializeAdvancementData(serializedAdvancements)
- .stream().map(data -> new AdvancementData(data.key(), data.criteriaMap())).toList();
-
- final DataSerializer.PlayerLocation legacyLocationData = converter
- .deserializePlayerLocationData(serializedLocation);
- final LocationData convertedLocationData = new LocationData(
- legacyLocationData == null ? "world" : legacyLocationData.worldName(),
- UUID.randomUUID(),
- "NORMAL",
- legacyLocationData == null ? 0d : legacyLocationData.x(),
- legacyLocationData == null ? 64d : legacyLocationData.y(),
- legacyLocationData == null ? 0d : legacyLocationData.z(),
- legacyLocationData == null ? 90f : legacyLocationData.yaw(),
- legacyLocationData == null ? 180f : legacyLocationData.pitch());
-
- return UserData.builder(minecraftVersion)
- .setStatus(new StatusData(health, maxHealth, healthScale, hunger, saturation,
- saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying))
- .setInventory(new ItemData(serializedInventory))
- .setEnderChest(new ItemData(serializedEnderChest))
- .setPotionEffects(new PotionEffectData(serializedPotionEffects))
- .setAdvancements(convertedAdvancements)
- .setStatistics(convertedStatisticData)
- .setLocation(convertedLocationData)
- .build();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- });
+ public DataSnapshot.Packed toUserData(@NotNull HSLConverter converter, @NotNull HuskSync plugin) {
+ try {
+ final DataSerializer.StatisticData stats = converter.deserializeStatisticData(serializedStatistics);
+ final DataSerializer.PlayerLocation loc = converter.deserializePlayerLocationData(serializedLocation);
+ final BukkitLegacyConverter adapter = (BukkitLegacyConverter) plugin.getLegacyConverter()
+ .orElseThrow(() -> new IllegalStateException("Legacy converter not present"));
+
+ return DataSnapshot.builder(plugin)
+ // Inventory
+ .inventory(BukkitData.Items.Inventory.from(
+ adapter.deserializeLegacyItemStacks(serializedInventory),
+ selectedSlot
+ ))
+
+ // Ender chest
+ .enderChest(BukkitData.Items.EnderChest.adapt(
+ adapter.deserializeLegacyItemStacks(serializedEnderChest)
+ ))
+
+ // Location
+ .location(BukkitData.Location.from(
+ loc == null ? 0d : loc.x(),
+ loc == null ? 64d : loc.y(),
+ loc == null ? 0d : loc.z(),
+ loc == null ? 90f : loc.yaw(),
+ loc == null ? 180f : loc.pitch(),
+ new Data.Location.World(
+ loc == null ? "world" : loc.worldName(),
+ UUID.randomUUID(), "NORMAL"
+ )))
+
+ // Advancements
+ .advancements(BukkitData.Advancements.from(converter
+ .deserializeAdvancementData(serializedAdvancements).stream()
+ .map(data -> Data.Advancements.Advancement.adapt(data.key(), data.criteriaMap()))
+ .toList()))
+
+ // Stats
+ .statistics(BukkitData.Statistics.from(
+ BukkitData.Statistics.createStatisticsMap(
+ convertStatisticMap(stats.untypedStatisticValues()),
+ convertMaterialStatisticMap(stats.blockStatisticValues()),
+ convertMaterialStatisticMap(stats.itemStatisticValues()),
+ convertEntityStatisticMap(stats.entityStatisticValues())
+ )))
+
+ // Health, hunger, experience & game mode
+ .health(BukkitData.Health.from(health, maxHealth, healthScale))
+ .hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
+ .experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
+ .gameMode(BukkitData.GameMode.from(gameMode, isFlying, isFlying))
+
+ // Build & pack into new format
+ .saveCause(DataSnapshot.SaveCause.LEGACY_MIGRATION).buildAndPack();
+ } catch (Throwable e) {
+ throw new IllegalStateException(e);
+ }
}
private Map convertStatisticMap(@NotNull HashMap rawMap) {
final HashMap convertedMap = new HashMap<>();
for (Map.Entry entry : rawMap.entrySet()) {
- convertedMap.put(entry.getKey().toString(), entry.getValue());
+ convertedMap.put(entry.getKey().getKey().toString(), entry.getValue());
}
return convertedMap;
}
@@ -333,8 +349,8 @@ public class LegacyMigrator extends Migrator {
final Map> convertedMap = new HashMap<>();
for (Map.Entry> entry : rawMap.entrySet()) {
for (Map.Entry materialEntry : entry.getValue().entrySet()) {
- convertedMap.computeIfAbsent(entry.getKey().toString(), k -> new HashMap<>())
- .put(materialEntry.getKey().toString(), materialEntry.getValue());
+ convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
+ .put(materialEntry.getKey().getKey().toString(), materialEntry.getValue());
}
}
return convertedMap;
@@ -344,8 +360,8 @@ public class LegacyMigrator extends Migrator {
final Map> convertedMap = new HashMap<>();
for (Map.Entry> entry : rawMap.entrySet()) {
for (Map.Entry materialEntry : entry.getValue().entrySet()) {
- convertedMap.computeIfAbsent(entry.getKey().toString(), k -> new HashMap<>())
- .put(materialEntry.getKey().toString(), materialEntry.getValue());
+ convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
+ .put(materialEntry.getKey().getKey().toString(), materialEntry.getValue());
}
}
return convertedMap;
diff --git a/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java b/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java
index 8b929c88..c039d13b 100644
--- a/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java
+++ b/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java
@@ -21,30 +21,28 @@ package net.william278.husksync.migrator;
import com.zaxxer.hikari.HikariDataSource;
import net.william278.husksync.BukkitHuskSync;
-import net.william278.husksync.data.*;
-import net.william278.husksync.player.User;
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.data.BukkitData;
+import net.william278.husksync.data.DataSnapshot;
+import net.william278.husksync.user.User;
import net.william278.mpdbconverter.MPDBConverter;
import org.bukkit.Bukkit;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
-import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.UUID;
+import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.regex.Pattern;
/**
- * A migrator for migrating MySQLPlayerDataBridge data to HuskSync {@link UserData}
+ * A migrator for migrating MySQLPlayerDataBridge data to HuskSync {@link DataSnapshot}s
*/
public class MpdbMigrator extends Migrator {
@@ -57,11 +55,13 @@ public class MpdbMigrator extends Migrator {
private String sourceInventoryTable;
private String sourceEnderChestTable;
private String sourceExperienceTable;
- private final String minecraftVersion;
- public MpdbMigrator(@NotNull BukkitHuskSync plugin, @NotNull Plugin mySqlPlayerDataBridge) {
+ public MpdbMigrator(@NotNull BukkitHuskSync plugin) {
super(plugin);
- this.mpdbConverter = MPDBConverter.getInstance(mySqlPlayerDataBridge);
+ this.mpdbConverter = MPDBConverter.getInstance(Objects.requireNonNull(
+ Bukkit.getPluginManager().getPlugin("MySQLPlayerDataBridge"),
+ "MySQLPlayerDataBridge dependency not found!"
+ ));
this.sourceHost = plugin.getSettings().getMySqlHost();
this.sourcePort = plugin.getSettings().getMySqlPort();
this.sourceUsername = plugin.getSettings().getMySqlUsername();
@@ -70,7 +70,6 @@ public class MpdbMigrator extends Migrator {
this.sourceInventoryTable = "mpdb_inventory";
this.sourceEnderChestTable = "mpdb_enderchest";
this.sourceExperienceTable = "mpdb_experience";
- this.minecraftVersion = plugin.getMinecraftVersion().toString();
}
@@ -78,10 +77,10 @@ public class MpdbMigrator extends Migrator {
public CompletableFuture start() {
plugin.log(Level.INFO, "Starting migration from MySQLPlayerDataBridge to HuskSync...");
final long startTime = System.currentTimeMillis();
- return CompletableFuture.supplyAsync(() -> {
+ return plugin.supplyAsync(() -> {
// Wipe the existing database, preparing it for data import
plugin.log(Level.INFO, "Preparing existing database (wiping)...");
- plugin.getDatabase().wipeDatabase().join();
+ plugin.getDatabase().wipeDatabase();
plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
// Create jdbc driver connection url
@@ -133,18 +132,15 @@ public class MpdbMigrator extends Migrator {
plugin.log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data (this might take a while)...");
final AtomicInteger playersConverted = new AtomicInteger();
- dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData -> {
- plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
- plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION))
- .exceptionally(exception -> {
- plugin.log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage());
- return null;
- }).join();
+ dataToMigrate.forEach(data -> {
+ final DataSnapshot.Packed convertedData = data.toUserData(mpdbConverter, plugin);
+ plugin.getDatabase().ensureUser(data.user());
+ plugin.getDatabase().addSnapshot(data.user(), convertedData);
playersConverted.getAndIncrement();
if (playersConverted.get() % 50 == 0) {
plugin.log(Level.INFO, "Converted MySQLPlayerDataBridge data for " + playersConverted + " players...");
}
- }).join());
+ });
plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
return true;
} catch (Exception e) {
@@ -198,10 +194,10 @@ public class MpdbMigrator extends Migrator {
}) {
plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
- obfuscateDataString(args[1]));
+ obfuscateDataString(args[1]));
} else {
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
- obfuscateDataString(args[1]) + " (is it a valid option?)");
+ obfuscateDataString(args[1]) + " (is it a valid option?)");
}
} else {
plugin.log(Level.INFO, getHelpMenu());
@@ -227,14 +223,14 @@ public class MpdbMigrator extends Migrator {
=== MySQLPlayerDataBridge Migration Wizard ==========
This will migrate inventories, ender chests and XP
from the MySQLPlayerDataBridge plugin to HuskSync.
-
+
To prevent excessive migration times, other non-vital
data will not be transferred.
-
+
[!] Existing data in the database will be wiped. [!]
-
+
STEP 1] Please ensure no players are on any servers.
-
+
STEP 2] HuskSync will need to connect to the database
used to hold the source MySQLPlayerDataBridge data.
Please check these database parameters are OK:
@@ -250,12 +246,12 @@ public class MpdbMigrator extends Migrator {
using the command:
"husksync migrate mpdb set "
(e.g.: "husksync migrate mpdb set host 1.2.3.4")
-
+
STEP 3] HuskSync will migrate data into the database
tables configures in the config.yml file of this
server. Please make sure you're happy with this
before proceeding.
-
+
STEP 4] To start the migration, please run:
"husksync migrate mpdb start"
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
@@ -279,38 +275,43 @@ public class MpdbMigrator extends Migrator {
* @param expProgress The player's current XP progress
* @param totalExp The player's total XP score
*/
- private record MpdbData(@NotNull User user, @NotNull String serializedInventory,
- @NotNull String serializedArmor, @NotNull String serializedEnderChest,
- int expLevel, float expProgress, int totalExp) {
+ private record MpdbData(
+ @NotNull User user,
+ @NotNull String serializedInventory,
+ @NotNull String serializedArmor,
+ @NotNull String serializedEnderChest,
+ int expLevel,
+ float expProgress,
+ int totalExp
+ ) {
+
/**
- * Converts exported MySQLPlayerDataBridge data into HuskSync's {@link UserData} object format
+ * Converts exported MySQLPlayerDataBridge data into HuskSync's {@link DataSnapshot} object format
*
* @param converter The {@link MPDBConverter} to use for converting to {@link ItemStack}s
- * @return A {@link CompletableFuture} that will resolve to the converted {@link UserData} object
+ * @return A {@link CompletableFuture} that will resolve to the converted {@link DataSnapshot} object
*/
@NotNull
- public CompletableFuture toUserData(@NotNull MPDBConverter converter,
- @NotNull String minecraftVersion) {
- return CompletableFuture.supplyAsync(() -> {
- // Combine inventory and armour
- final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
- inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory));
- final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone();
- for (int i = 36; i < 36 + armor.length; i++) {
- inventory.setItem(i, armor[i - 36]);
- }
+ public DataSnapshot.Packed toUserData(@NotNull MPDBConverter converter, @NotNull HuskSync plugin) {
+ // Combine inventory and armor
+ final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
+ inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory));
+ final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone();
+ for (int i = 36; i < 36 + armor.length; i++) {
+ inventory.setItem(i, armor[i - 36]);
+ }
+ final ItemStack[] enderChest = converter.getItemStackFromSerializedData(serializedEnderChest);
- // Create user data record
- return UserData.builder(minecraftVersion)
- .setStatus(new StatusData(20, 20, 0, 20, 10,
- 1, 0, totalExp, expLevel, expProgress, "SURVIVAL",
- false))
- .setInventory(new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()))
- .setEnderChest(new ItemData(BukkitSerializer.serializeItemStackArray(converter
- .getItemStackFromSerializedData(serializedEnderChest)).join()))
- .build();
- });
+ // Create user data record
+ return DataSnapshot.builder(plugin)
+ .inventory(BukkitData.Items.Inventory.from(inventory.getContents(), 0))
+ .enderChest(BukkitData.Items.EnderChest.adapt(enderChest))
+ .experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
+ .gameMode(BukkitData.GameMode.from("SURVIVAL", false, false))
+ .saveCause(DataSnapshot.SaveCause.MPDB_MIGRATION)
+ .buildAndPack();
}
+
}
}
diff --git a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java
deleted file mode 100644
index a44beb13..00000000
--- a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java
+++ /dev/null
@@ -1,655 +0,0 @@
-/*
- * This file is part of HuskSync, licensed under the Apache License 2.0.
- *
- * Copyright (c) William278
- * 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 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 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 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 setInventory(@NotNull ItemData itemData) {
- return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> {
- final CompletableFuture 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 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 setEnderChest(@NotNull ItemData enderChestData) {
- return BukkitSerializer.deserializeItemStackArray(enderChestData.serializedItems).thenApplyAsync(contents -> {
- final CompletableFuture enderChestSetFuture = new CompletableFuture<>();
- Bukkit.getScheduler().runTask(plugin, () -> {
- player.getEnderChest().setContents(contents);
- enderChestSetFuture.complete(null);
- });
- return enderChestSetFuture.join();
- });
- }
-
- @Override
- public CompletableFuture getPotionEffects() {
- return BukkitSerializer.serializePotionEffectArray(player.getActivePotionEffects()
- .toArray(new PotionEffect[0])).thenApply(PotionEffectData::new);
- }
-
- @Override
- public CompletableFuture setPotionEffects(@NotNull PotionEffectData potionEffectData) {
- return BukkitSerializer.deserializePotionEffectArray(potionEffectData.serializedPotionEffects)
- .thenApplyAsync(effects -> {
- final CompletableFuture 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> getAdvancements() {
- return CompletableFuture.supplyAsync(() -> {
- final Iterator serverAdvancements = Bukkit.getServer().advancementIterator();
- final ArrayList 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 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 setAdvancements(@NotNull List 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 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 getStatistics() {
- return CompletableFuture.supplyAsync(() -> {
- final Map untypedStatisticValues = new HashMap<>();
- final Map> blockStatisticValues = new HashMap<>();
- final Map> itemStatisticValues = new HashMap<>();
- final Map> entityStatisticValues = new HashMap<>();
-
- for (Statistic statistic : Statistic.values()) {
- switch (statistic.getType()) {
- case ITEM -> {
- final Map 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 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 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 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 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 setLocation(@NotNull LocationData locationData) {
- final CompletableFuture teleportFuture = new CompletableFuture<>();
- AtomicReference 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 getPersistentDataContainer() {
- final Map> 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 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> showMenu(@NotNull ItemData itemData, boolean editable,
- int minimumRows, @NotNull MineDown title) {
- final CompletableFuture> 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");
- }
-
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java b/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java
new file mode 100644
index 00000000..27c07d36
--- /dev/null
+++ b/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java
@@ -0,0 +1,152 @@
+/*
+ * This file is part of HuskSync, licensed under the Apache License 2.0.
+ *
+ * Copyright (c) William278
+ * 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 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;
+ }
+}
diff --git a/bukkit/src/main/java/net/william278/husksync/util/BukkitLegacyConverter.java b/bukkit/src/main/java/net/william278/husksync/util/BukkitLegacyConverter.java
new file mode 100644
index 00000000..56be8c18
--- /dev/null
+++ b/bukkit/src/main/java/net/william278/husksync/util/BukkitLegacyConverter.java
@@ -0,0 +1,279 @@
+/*
+ * This file is part of HuskSync, licensed under the Apache License 2.0.
+ *
+ * Copyright (c) William278
+ * 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 readStatusData(@NotNull JSONObject object) {
+ if (!object.has("status_data")) {
+ return Map.of();
+ }
+
+ final JSONObject status = object.getJSONObject("status_data");
+ final HashMap 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 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 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 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 readAdvancements(@NotNull JSONObject object) {
+ if (!object.has("advancements") || !shouldImport(Identifier.ADVANCEMENTS)) {
+ return Optional.empty();
+ }
+
+ final JSONArray advancements = object.getJSONArray("advancements");
+ final List 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 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 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 genericStats = new HashMap<>();
+ untyped.keys().forEachRemaining(stat -> genericStats.put(Statistic.valueOf(stat), untyped.getInt(stat)));
+
+ final Map> blockStats = new HashMap<>();
+ blocks.keys().forEachRemaining(stat -> {
+ final JSONObject blockStat = blocks.getJSONObject(stat);
+ final Map blockMap = new HashMap<>();
+ blockStat.keys().forEachRemaining(block -> blockMap.put(Material.valueOf(block), blockStat.getInt(block)));
+ blockStats.put(Statistic.valueOf(stat), blockMap);
+ });
+
+ final Map> itemStats = new HashMap<>();
+ items.keys().forEachRemaining(stat -> {
+ final JSONObject itemStat = items.getJSONObject(stat);
+ final Map itemMap = new HashMap<>();
+ itemStat.keys().forEachRemaining(item -> itemMap.put(Material.valueOf(item), itemStat.getInt(item)));
+ itemStats.put(Statistic.valueOf(stat), itemMap);
+ });
+
+ final Map> entityStats = new HashMap<>();
+ entities.keys().forEachRemaining(stat -> {
+ final JSONObject entityStat = entities.getJSONObject(stat);
+ final Map 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) 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();
+ }
+ }
+
+}
diff --git a/bukkit/src/main/java/net/william278/husksync/util/BukkitMapPersister.java b/bukkit/src/main/java/net/william278/husksync/util/BukkitMapPersister.java
new file mode 100644
index 00000000..aa98fd9b
--- /dev/null
+++ b/bukkit/src/main/java/net/william278/husksync/util/BukkitMapPersister.java
@@ -0,0 +1,378 @@
+/*
+ * This file is part of HuskSync, licensed under the Apache License 2.0.
+ *
+ * Copyright (c) William278
+ * 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 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 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 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 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 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 getMapViews();
+
+ @ApiStatus.Internal
+ @NotNull
+ HuskSync getPlugin();
+
+}
diff --git a/bukkit/src/main/java/net/william278/husksync/util/BukkitTask.java b/bukkit/src/main/java/net/william278/husksync/util/BukkitTask.java
new file mode 100644
index 00000000..a9381425
--- /dev/null
+++ b/bukkit/src/main/java/net/william278/husksync/util/BukkitTask.java
@@ -0,0 +1,172 @@
+/*
+ * This file is part of HuskSync, licensed under the Apache License 2.0.
+ *
+ * Copyright (c) William278
+ * 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();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml
index b337a9eb..07051d89 100644
--- a/bukkit/src/main/resources/plugin.yml
+++ b/bukkit/src/main/resources/plugin.yml
@@ -12,19 +12,4 @@ libraries:
- 'redis.clients:jedis:${jedis_version}'
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
- - 'org.xerial.snappy:snappy-java:${snappy_version}'
- - 'org.apache.commons:commons-text:${commons_text_version}'
-
-commands:
- husksync:
- usage: '/ '
- description: 'Manage the HuskSync plugin'
- userdata:
- usage: '/ [version_uuid]'
- description: 'View, manage & restore player userdata'
- inventory:
- usage: '/ [version_uuid]'
- description: 'View & edit a player''s inventory'
- enderchest:
- usage: '/ [version_uuid]'
- description: 'View & edit a player''s Ender Chest'
\ No newline at end of file
+ - 'org.xerial.snappy:snappy-java:${snappy_version}'
\ No newline at end of file
diff --git a/common/build.gradle b/common/build.gradle
index 81f85f22..a21899eb 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -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'
}
\ No newline at end of file
diff --git a/common/src/main/java/net/william278/husksync/HuskSync.java b/common/src/main/java/net/william278/husksync/HuskSync.java
index a579ff57..6c8324b0 100644
--- a/common/src/main/java/net/william278/husksync/HuskSync.java
+++ b/common/src/main/java/net/william278/husksync/HuskSync.java
@@ -19,31 +19,40 @@
package net.william278.husksync;
+import com.fatboyindustrial.gsonjavatime.Converters;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import net.william278.annotaml.Annotaml;
+import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.desertwell.util.Version;
+import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Settings;
-import net.william278.husksync.data.DataAdapter;
+import net.william278.husksync.data.Data;
+import net.william278.husksync.data.Identifier;
+import net.william278.husksync.data.Serializer;
import net.william278.husksync.database.Database;
-import net.william278.husksync.event.EventCannon;
+import net.william278.husksync.event.EventDispatcher;
import net.william278.husksync.migrator.Migrator;
-import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager;
+import net.william278.husksync.user.ConsoleUser;
+import net.william278.husksync.user.OnlineUser;
+import net.william278.husksync.util.LegacyConverter;
+import net.william278.husksync.util.Task;
import org.jetbrains.annotations.NotNull;
import java.io.File;
+import java.io.IOException;
import java.io.InputStream;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
+import java.lang.reflect.InvocationTargetException;
+import java.util.*;
import java.util.logging.Level;
/**
* Abstract implementation of the HuskSync plugin.
*/
-public interface HuskSync {
+public interface HuskSync extends Task.Supplier, EventDispatcher {
int SPIGOT_RESOURCE_ID = 97144;
@@ -81,21 +90,45 @@ public interface HuskSync {
@NotNull
RedisManager getRedisManager();
+ @NotNull
+ DataAdapter getDataAdapter();
+
/**
- * Returns the data adapter implementation
- *
- * @return the {@link DataAdapter} implementation
+ * Returns the data serializer for the given {@link Identifier}
*/
@NotNull
- DataAdapter getDataAdapter();
+ Map> getSerializers();
/**
- * Returns the event firing cannon
+ * Register a data serializer for the given {@link Identifier}
*
- * @return the {@link EventCannon} implementation
+ * @param identifier the {@link Identifier}
+ * @param serializer the {@link Serializer}
+ */
+ default void registerSerializer(@NotNull Identifier identifier,
+ @NotNull Serializer extends Data> serializer) {
+ if (identifier.isCustom()) {
+ log(Level.INFO, String.format("Registered custom data type: %s", identifier));
+ }
+ getSerializers().put(identifier, (Serializer) serializer);
+ }
+
+ /**
+ * Get the {@link Identifier} for the given key
+ */
+ default Optional getIdentifier(@NotNull String key) {
+ return getSerializers().keySet().stream().filter(identifier -> identifier.toString().equals(key)).findFirst();
+ }
+
+ /**
+ * Get the set of registered data types
+ *
+ * @return the set of registered data types
*/
@NotNull
- EventCannon getEventCannon();
+ default Set getRegisteredDataTypes() {
+ return getSerializers().keySet();
+ }
/**
* Returns a list of available data {@link Migrator}s
@@ -105,6 +138,25 @@ public interface HuskSync {
@NotNull
List getAvailableMigrators();
+ @NotNull
+ Map getPlayerCustomDataStore(@NotNull OnlineUser user);
+
+ /**
+ * Initialize a faucet of the plugin.
+ *
+ * @param name the name of the faucet
+ * @param runner a runnable for initializing the faucet
+ */
+ default void initialize(@NotNull String name, @NotNull ThrowingConsumer runner) {
+ log(Level.INFO, "Initializing " + name + "...");
+ try {
+ runner.accept(this);
+ } catch (Throwable e) {
+ throw new FailedToLoadException("Failed to initialize " + name, e);
+ }
+ log(Level.INFO, "Successfully initialized " + name);
+ }
+
/**
* Returns the plugin {@link Settings}
*
@@ -113,6 +165,8 @@ public interface HuskSync {
@NotNull
Settings getSettings();
+ void setSettings(@NotNull Settings settings);
+
/**
* Returns the plugin {@link Locales}
*
@@ -121,6 +175,16 @@ public interface HuskSync {
@NotNull
Locales getLocales();
+ void setLocales(@NotNull Locales locales);
+
+ /**
+ * Returns if a dependency is loaded
+ *
+ * @param name the name of the dependency
+ * @return {@code true} if the dependency is loaded, {@code false} otherwise
+ */
+ boolean isDependencyLoaded(@NotNull String name);
+
/**
* Get a resource as an {@link InputStream} from the plugin jar
*
@@ -129,6 +193,14 @@ public interface HuskSync {
*/
InputStream getResource(@NotNull String name);
+ /**
+ * Returns the plugin data folder
+ *
+ * @return the plugin data folder as a {@link File}
+ */
+ @NotNull
+ File getDataFolder();
+
/**
* Log a message to the console
*
@@ -146,10 +218,18 @@ public interface HuskSync {
*/
default void debug(@NotNull String message, @NotNull Throwable... throwable) {
if (getSettings().doDebugLogging()) {
- log(Level.INFO, "[DEBUG] " + message, throwable);
+ log(Level.INFO, String.format("[DEBUG] %s", message), throwable);
}
}
+ /**
+ * Get the console user
+ *
+ * @return the {@link ConsoleUser}
+ */
+ @NotNull
+ ConsoleUser getConsole();
+
/**
* Returns the plugin version
*
@@ -159,44 +239,103 @@ public interface HuskSync {
Version getPluginVersion();
/**
- * Returns the plugin data folder
+ * Returns the Minecraft version implementation
*
- * @return the plugin data folder as a {@link File}
+ * @return the Minecraft {@link Version}
*/
@NotNull
- File getDataFolder();
+ Version getMinecraftVersion();
/**
- * Returns a future returning the latest plugin {@link Version} if the plugin is out-of-date
+ * Returns the platform type
*
- * @return a {@link CompletableFuture} returning the latest {@link Version} if the current one is out-of-date
+ * @return the platform type
*/
- default CompletableFuture