diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java index 519205f2..41a76117 100644 --- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java +++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java @@ -19,6 +19,7 @@ import net.william278.husksync.database.MySqlDatabase; import net.william278.husksync.editor.DataEditor; import net.william278.husksync.event.BukkitEventCannon; import net.william278.husksync.event.EventCannon; +import net.william278.husksync.hook.PlanHook; import net.william278.husksync.listener.BukkitEventListener; import net.william278.husksync.listener.EventListener; import net.william278.husksync.migrator.LegacyMigrator; @@ -186,6 +187,13 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { getLoggingAdapter().log(Level.INFO, "Successfully registered permissions & commands"); } return succeeded; + }).thenApply(succeeded -> { + if (succeeded && Bukkit.getPluginManager().getPlugin("Plan") != null) { + getLoggingAdapter().log(Level.INFO, "Enabling Plan integration..."); + new PlanHook(database, logger).hookIntoPlan(); + getLoggingAdapter().log(Level.INFO, "Plan integration enabled!"); + } + return succeeded; }).thenApply(succeeded -> { // Check for updates if (succeeded && settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES)) { diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml index b4b684e2..072a32eb 100644 --- a/bukkit/src/main/resources/plugin.yml +++ b/bukkit/src/main/resources/plugin.yml @@ -5,7 +5,9 @@ api-version: 1.16 author: William278 description: 'A modern, cross-server player data synchronization system' website: 'https://william278.net' -softdepend: [ MysqlPlayerDataBridge ] +softdepend: + - MysqlPlayerDataBridge + - Plan libraries: - 'mysql:mysql-connector-java:8.0.29' - 'org.xerial.snappy:snappy-java:1.1.8.4' diff --git a/common/build.gradle b/common/build.gradle index acd05469..92bd5344 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -12,6 +12,11 @@ dependencies { compileOnly 'dev.dejvokep:boosted-yaml:1.2' compileOnly 'org.xerial.snappy:snappy-java:1.1.8.4' compileOnly 'org.jetbrains:annotations:23.0.0' + compileOnly 'com.github.plan-player-analytics:Plan:5.4.1690' + + testImplementation 'org.xerial.snappy:snappy-java:1.1.8.4' + testImplementation 'com.github.plan-player-analytics:Plan:5.4.1690' + testCompileOnly 'org.jetbrains:annotations:23.0.0' } shadowJar { diff --git a/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java b/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java new file mode 100644 index 00000000..fc1cbb06 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java @@ -0,0 +1,98 @@ +package net.william278.husksync.hook; + +import com.djrapitops.plan.extension.CallEvents; +import com.djrapitops.plan.extension.DataExtension; +import com.djrapitops.plan.extension.FormatType; +import com.djrapitops.plan.extension.annotation.NumberProvider; +import com.djrapitops.plan.extension.annotation.PluginInfo; +import com.djrapitops.plan.extension.annotation.StringProvider; +import com.djrapitops.plan.extension.icon.Color; +import com.djrapitops.plan.extension.icon.Family; +import net.william278.husksync.data.VersionedUserData; +import net.william278.husksync.database.Database; +import net.william278.husksync.player.User; +import org.jetbrains.annotations.NotNull; + +import java.util.Date; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +@PluginInfo( + name = "HuskSync", + iconName = "arrow-right-arrow-left", + iconFamily = Family.SOLID, + color = Color.LIGHT_BLUE +) +public class PlanDataExtension implements DataExtension { + + private Database database; + + //todo add more providers + protected PlanDataExtension(@NotNull Database database) { + this.database = database; + } + + protected PlanDataExtension() { + } + + @Override + public CallEvents[] callExtensionMethodsOn() { + return new CallEvents[]{ + CallEvents.PLAYER_JOIN, + CallEvents.PLAYER_LEAVE + }; + } + + private CompletableFuture> getCurrentUserData(@NotNull UUID uuid) { + return CompletableFuture.supplyAsync(() -> { + final Optional optionalUser = database.getUser(uuid).join(); + if (optionalUser.isPresent()) { + return database.getCurrentUserData(optionalUser.get()).join(); + } + return Optional.empty(); + }); + } + + @NumberProvider( + text = "Data Sync Time", + description = "The last time the user had their data synced with the server.", + iconName = "clock", + iconFamily = Family.SOLID, + format = FormatType.TIME_MILLISECONDS, + priority = 1 + ) + public long getCurrentDataTimestamp(@NotNull UUID uuid) { + return getCurrentUserData(uuid).join().map( + versionedUserData -> versionedUserData.versionTimestamp().getTime()) + .orElse(new Date().getTime()); + } + + @StringProvider( + text = "Data Version ID", + description = "ID of the data version that the user is currently using.", + iconName = "bolt", + iconFamily = Family.SOLID, + priority = 2 + ) + public String getCurrentDataId(@NotNull UUID uuid) { + return getCurrentUserData(uuid).join().map( + versionedUserData -> versionedUserData.versionUUID().toString() + .split(Pattern.quote("-"))[0]) + .orElse("unknown"); + } + + @NumberProvider( + text = "Advancements", + description = "The number of advancements & recipes the player has progressed in", + iconName = "award", + iconFamily = Family.SOLID, + priority = 3 + ) + public long getAdvancementsCompleted(@NotNull UUID playerUUID) { + return getCurrentUserData(playerUUID).join().map( + versionedUserData -> (long) versionedUserData.userData().getAdvancementData().size()) + .orElse(0L); + } +} diff --git a/common/src/main/java/net/william278/husksync/hook/PlanHook.java b/common/src/main/java/net/william278/husksync/hook/PlanHook.java new file mode 100644 index 00000000..fca0f480 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/hook/PlanHook.java @@ -0,0 +1,55 @@ +package net.william278.husksync.hook; + +import com.djrapitops.plan.capability.CapabilityService; +import com.djrapitops.plan.extension.ExtensionService; +import net.william278.husksync.database.Database; +import net.william278.husksync.util.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Level; + +public class PlanHook { + + private final Database database; + private final Logger logger; + + public PlanHook(@NotNull Database database, @NotNull Logger logger) { + this.database = database; + this.logger = logger; + } + + public void hookIntoPlan() { + if (!areAllCapabilitiesAvailable()) { + return; + } + registerDataExtension(); + handlePlanReload(); + } + + private boolean areAllCapabilitiesAvailable() { + CapabilityService capabilities = CapabilityService.getInstance(); + return capabilities.hasCapability("DATA_EXTENSION_VALUES"); + } + + private void registerDataExtension() { + try { + ExtensionService.getInstance().register(new PlanDataExtension(database)); + } catch (IllegalStateException planIsNotEnabled) { + logger.log(Level.SEVERE, "Plan extension hook failed to register. Plan is not enabled.", planIsNotEnabled); + // Plan is not enabled, handle exception + } catch (IllegalArgumentException dataExtensionImplementationIsInvalid) { + logger.log(Level.SEVERE, "Plan extension hook failed to register. Data hook implementation is invalid.", dataExtensionImplementationIsInvalid); + // The DataExtension implementation has an implementation error, handle exception + } + } + + // Re-register the extension when plan enables + private void handlePlanReload() { + CapabilityService.getInstance().registerEnableListener(isPlanEnabled -> { + if (isPlanEnabled) { + registerDataExtension(); + } + }); + } + +} diff --git a/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java b/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java index 3487de52..dd2e5c76 100644 --- a/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java +++ b/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java @@ -7,6 +7,9 @@ import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; +/** + * Tests for the data system {@link DataAdapter} + */ public class DataAdaptionTests { @Test @@ -42,7 +45,7 @@ public class DataAdaptionTests { final DataAdapter dataAdapter = new JsonDataAdapter(); final byte[] data = dataAdapter.toBytes(dummyUserData); final String json = new String(data, StandardCharsets.UTF_8); - final String expectedJson = "{\"status\":{\"health\":20.0,\"max_health\":20.0,\"health_scale\":0.0,\"hunger\":20,\"saturation\":5.0,\"saturation_exhaustion\":5.0,\"selected_item_slot\":1,\"total_experience\":100,\"experience_level\":1,\"experience_progress\":1.0,\"game_mode\":\"SURVIVAL\",\"is_flying\":false},\"inventory\":{\"serialized_inventory\":\"\"},\"ender_chest\":{\"serialized_inventory\":\"\"},\"potion_effects\":{\"serialized_potion_effects\":\"\"},\"advancements\":[],\"statistics\":{\"untyped_statistics\":{},\"block_statistics\":{},\"item_statistics\":{},\"entity_statistics\":{}},\"location\":{\"world_name\":\"dummy_world\",\"world_uuid\":\"00000000-0000-0000-0000-000000000000\",\"world_environment\":\"NORMAL\",\"x\":0.0,\"y\":64.0,\"z\":0.0,\"yaw\":90.0,\"pitch\":180.0},\"persistent_data_container\":{\"persistent_data_map\":{}},\"format_version\":1}"; + final String expectedJson = "{\"status\":{\"health\":20.0,\"max_health\":20.0,\"health_scale\":0.0,\"hunger\":20,\"saturation\":5.0,\"saturation_exhaustion\":5.0,\"selected_item_slot\":1,\"total_experience\":100,\"experience_level\":1,\"experience_progress\":1.0,\"game_mode\":\"SURVIVAL\",\"is_flying\":false},\"inventory\":{\"serialized_items\":\"\"},\"ender_chest\":{\"serialized_items\":\"\"},\"potion_effects\":{\"serialized_potion_effects\":\"\"},\"advancements\":[],\"statistics\":{\"untyped_statistics\":{},\"block_statistics\":{},\"item_statistics\":{},\"entity_statistics\":{}},\"location\":{\"world_name\":\"dummy_world\",\"world_uuid\":\"00000000-0000-0000-0000-000000000000\",\"world_environment\":\"NORMAL\",\"x\":0.0,\"y\":64.0,\"z\":0.0,\"yaw\":90.0,\"pitch\":180.0},\"persistent_data_container\":{\"persistent_data_map\":{}},\"format_version\":1}"; Assertions.assertEquals(expectedJson, json); } diff --git a/common/src/test/java/net/william278/husksync/hook/PlanHookTests.java b/common/src/test/java/net/william278/husksync/hook/PlanHookTests.java new file mode 100644 index 00000000..db47c52a --- /dev/null +++ b/common/src/test/java/net/william278/husksync/hook/PlanHookTests.java @@ -0,0 +1,19 @@ +package net.william278.husksync.hook; + +import com.djrapitops.plan.extension.extractor.ExtensionExtractor; +import org.junit.jupiter.api.Test; + +/** + * Tests for the {@link PlanHook} and {@link PlanDataExtension} implementation + */ +public class PlanHookTests { + + /** + * Throws IllegalArgumentException if there is an implementation error or warning. + */ + @Test + public void testExtensionImplementationErrors() { + new ExtensionExtractor(new PlanDataExtension()).validateAnnotations(); + } + +}