diff --git a/build.gradle b/build.gradle index 16af413d..9a50a718 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,11 @@ repositories { name = "viaversion" url = uri("https://repo.viaversion.com") } + + maven { + name = "discordsrvc" + url = uri("https://nexus.scarsz.me/content/groups/public/") + } } configure(apiDependencies) { @@ -48,11 +53,10 @@ dependencies { // Caching shadowed("com.github.ben-manes.caffeine:caffeine:3.2.2") - // PlaceholderAPI + // Plugin Hooks externalPlugin 'me.clip:placeholderapi:2.11.6' - - // Luckperms for group context compileOnly 'net.luckperms:api:5.4' + compileOnly 'com.discordsrv:discordsrv:1.28.0' // hk2 for annotation processing only compileOnly('org.glassfish.hk2:hk2-api:3.1.1') { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/AffectedProfiles.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/AffectedProfiles.java index 2ab8e55d..3dcb5810 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/AffectedProfiles.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/AffectedProfiles.java @@ -1,6 +1,8 @@ package org.mvplugins.multiverse.inventories.handleshare; import org.mvplugins.multiverse.inventories.profile.key.ProfileKey; +import org.mvplugins.multiverse.inventories.share.Sharable; +import org.mvplugins.multiverse.inventories.share.Sharables; import org.mvplugins.multiverse.inventories.share.Shares; import java.util.LinkedList; @@ -10,6 +12,7 @@ public final class AffectedProfiles { private final List writeProfiles = new LinkedList<>(); private final List readProfiles = new LinkedList<>(); + private final Shares sharesToRead = Sharables.noneOf(); AffectedProfiles() { } @@ -28,6 +31,7 @@ void addWriteProfile(ProfileKey profileKey, Shares shares) { */ void addReadProfile(ProfileKey profileKey, Shares shares) { readProfiles.add(new PersistingProfile(shares, profileKey)); + sharesToRead.addAll(shares); } public List getWriteProfiles() { @@ -37,4 +41,8 @@ public List getWriteProfiles() { public List getReadProfiles() { return readProfiles; } + + boolean isShareToRead(Sharable sharable) { + return sharesToRead.contains(sharable); + } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/PlayerShareHandlingState.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/PlayerShareHandlingState.java new file mode 100644 index 00000000..f0ed4f90 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/PlayerShareHandlingState.java @@ -0,0 +1,54 @@ +package org.mvplugins.multiverse.inventories.handleshare; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.inventories.share.Sharable; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Keeps track of players who are currently having their sharable handling processed. + *
+ * This is used to prevent infinite loops when updating sharables that may trigger events themselves or + * when suppressing notifications during the handling process. + * + * @since 5.3 + */ +@ApiStatus.AvailableSince("5.3") +@Service +public final class PlayerShareHandlingState { + + private final Map playerAffectedProfiles; + + @Inject + PlayerShareHandlingState() { + this.playerAffectedProfiles = new HashMap<>(); + } + + void setPlayerAffectedProfiles(Player player, AffectedProfiles status) { + this.playerAffectedProfiles.put(player.getUniqueId(), status); + } + + void removePlayerAffectedProfiles(Player player) { + this.playerAffectedProfiles.remove(player.getUniqueId()); + } + + /** + * Checks if the given player is currently having the given sharable handled. + * + * @param player The player to check. + * @param sharable The sharable to check. + * @return True if the player is having the sharable handled, false otherwise. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public boolean isHandlingSharable(Player player, Sharable sharable) { + AffectedProfiles status = this.playerAffectedProfiles.get(player.getUniqueId()); + return status != null && status.isShareToRead(sharable); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandler.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandler.java index 060e13fe..3d83b7e2 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandler.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ShareHandler.java @@ -29,6 +29,7 @@ sealed abstract class ShareHandler permits GameModeShareHandler, ReadOnlyShareHa protected final InventoriesConfig inventoriesConfig; protected final WorldGroupManager worldGroupManager; protected final ProfileContainerStore worldProfileContainerStore; + private final PlayerShareHandlingState playerShareHandlingState; ShareHandler(MultiverseInventories inventories, Player player) { this.player = player; @@ -41,6 +42,7 @@ sealed abstract class ShareHandler permits GameModeShareHandler, ReadOnlyShareHa this.worldProfileContainerStore = inventories.getServiceLocator() .getService(ProfileContainerStoreProvider.class) .getStore(ContainerType.WORLD); + this.playerShareHandlingState = inventories.getServiceLocator().getService(PlayerShareHandlingState.class); } /** @@ -87,9 +89,11 @@ private ProfileDataSnapshot getSnapshot() { } private void updatePlayer() { + playerShareHandlingState.setPlayerAffectedProfiles(player, affectedProfiles); for (PersistingProfile readProfile : affectedProfiles.getReadProfiles()) { ShareHandlingUpdater.updatePlayer(inventories, player, readProfile); } + playerShareHandlingState.removePlayerAffectedProfiles(player); } private CompletableFuture updateProfiles(ProfileDataSnapshot snapshot) { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java index 88518e82..0e4f3ded 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java @@ -2,8 +2,7 @@ import org.jvnet.hk2.annotations.Contract; import org.mvplugins.multiverse.core.dynamiclistener.DynamicListener; -import org.mvplugins.multiverse.inventories.view.ReadOnlyInventoryHolder; @Contract -public sealed interface MVInvListener extends DynamicListener permits InventoryViewListener, MVEventsListener, RespawnListener, ShareHandleListener, SpawnChangeListener { +public sealed interface MVInvListener extends DynamicListener permits InventoryViewListener, MVEventsListener, RespawnListener, ShareHandleListener, SilentGrantsListener, SpawnChangeListener { } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/SilentGrantsListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/SilentGrantsListener.java new file mode 100644 index 00000000..b45c1648 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/SilentGrantsListener.java @@ -0,0 +1,85 @@ +package org.mvplugins.multiverse.inventories.listeners; + +import com.dumptruckman.minecraft.util.Logging; +import github.scarsz.discordsrv.DiscordSRV; +import github.scarsz.discordsrv.api.Subscribe; +import github.scarsz.discordsrv.api.events.AchievementMessagePreProcessEvent; +import org.bukkit.Bukkit; +import org.bukkit.event.player.PlayerAdvancementDoneEvent; +import org.bukkit.event.player.PlayerRecipeDiscoverEvent; +import org.bukkit.event.server.PluginEnableEvent; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod; +import org.mvplugins.multiverse.core.utils.ReflectHelper; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.inventories.handleshare.PlayerShareHandlingState; +import org.mvplugins.multiverse.inventories.share.Sharables; + +@Service +final class SilentGrantsListener implements MVInvListener { + + private final PlayerShareHandlingState playerShareHandlingState; + + private final boolean hasShouldShowNotificationMethod; + private final boolean hasPlayerAdvancementDoneMessageMethod; + + @Inject + SilentGrantsListener(PlayerShareHandlingState playerShareHandlingState) { + this.playerShareHandlingState = playerShareHandlingState; + + this.hasShouldShowNotificationMethod = ReflectHelper.getMethod( + PlayerRecipeDiscoverEvent.class,"shouldShowNotification") != null; + this.hasPlayerAdvancementDoneMessageMethod = ReflectHelper.getMethod( + PlayerAdvancementDoneEvent.class,"message") != null; + + if (Bukkit.getPluginManager().isPluginEnabled("DiscordSRV")) { + Logging.fine("Registering DiscordSRV advancement grant hook."); + DiscordSRV.api.subscribe(new DiscordSrvHook()); + } + } + + @EventMethod + void onPlayerRecipeDiscover(PlayerRecipeDiscoverEvent event) { + if (!this.hasShouldShowNotificationMethod) { + // spigot does not have the method to suppress notifications + return; + } + if (playerShareHandlingState.isHandlingSharable(event.getPlayer(), Sharables.RECIPES)) { + Logging.finest("Suppressing recipe discover notification for player %s due to share handling.", + event.getPlayer().getName()); + event.shouldShowNotification(false); + } + } + + @EventMethod + void onPlayerAdvancementDone(PlayerAdvancementDoneEvent event) { + if (!this.hasPlayerAdvancementDoneMessageMethod) { + // paper does not have the method to suppress notifications + return; + } + if (playerShareHandlingState.isHandlingSharable(event.getPlayer(), Sharables.ADVANCEMENTS)) { + Logging.finest("Suppressing advancement done message for player %s due to share handling.", + event.getPlayer().getName()); + event.message(null); + } + } + + @EventMethod + void onPluginEnable(PluginEnableEvent event) { + if (event.getPlugin().getName().equals("DiscordSRV")) { + Logging.fine("Registering DiscordSRV advancement grant hook."); + DiscordSRV.api.subscribe(new DiscordSrvHook()); + } + } + + private class DiscordSrvHook { + @Subscribe + public void onAchievementMessage(AchievementMessagePreProcessEvent event) { + if (playerShareHandlingState.isHandlingSharable(event.getPlayer(), Sharables.ADVANCEMENTS)) { + Logging.finest("Suppressing DiscordSRV advancement grant message for player %s due to share handling.", + event.getPlayer().getName()); + event.setCancelled(true); + } + } + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/SpawnChangeListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/SpawnChangeListener.java index 1063066b..c89ae77c 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/SpawnChangeListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/SpawnChangeListener.java @@ -1,6 +1,7 @@ package org.mvplugins.multiverse.inventories.listeners; import com.destroystokyo.paper.event.player.PlayerSetSpawnEvent; +import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.EventPriority; @@ -12,6 +13,7 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.SkipIfEventExist; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.handleshare.PlayerShareHandlingState; import org.mvplugins.multiverse.inventories.handleshare.SingleShareWriter; import org.mvplugins.multiverse.inventories.share.Sharables; import org.mvplugins.multiverse.inventories.util.RespawnLocation; @@ -26,23 +28,27 @@ final class SpawnChangeListener implements MVInvListener { private final MultiverseInventories inventories; + private final PlayerShareHandlingState playerShareHandlingState; @Inject - public SpawnChangeListener(MultiverseInventories inventories) { + public SpawnChangeListener(MultiverseInventories inventories, PlayerShareHandlingState playerShareHandlingState) { this.inventories = inventories; + this.playerShareHandlingState = playerShareHandlingState; } @EventClass("com.destroystokyo.paper.event.player.PlayerSetSpawnEvent") @DefaultEventPriority(EventPriority.MONITOR) - EventRunnable onPlayerSetSpawn() { + EventRunnable onPlayerSetSpawn() { return new EventRunnable() { @Override public void onEvent(PlayerSetSpawnEvent event) { - if (Sharables.isIgnoringSpawnListener(event.getPlayer())) { + Player player = event.getPlayer(); + if (playerShareHandlingState.isHandlingSharable(player, Sharables.BED_SPAWN)) { + Logging.finest("Setting new spawn location silently for player %s due to share handling.", + player.getName()); event.setNotifyPlayer(false); return; } - Player player = event.getPlayer(); Location newSpawnLoc = event.getLocation(); if (newSpawnLoc == null) { updatePlayerSpawn(player, null); @@ -71,14 +77,14 @@ public void onEvent(PlayerSetSpawnEvent event) { @EventClass("org.bukkit.event.player.PlayerSpawnChangeEvent") @SkipIfEventExist("com.destroystokyo.paper.event.player.PlayerSetSpawnEvent") @DefaultEventPriority(EventPriority.MONITOR) - EventRunnable onPlayerSpawnChange() { + EventRunnable onPlayerSpawnChange() { return new EventRunnable() { @Override public void onEvent(PlayerSpawnChangeEvent event) { - if (Sharables.isIgnoringSpawnListener(event.getPlayer())) { + Player player = event.getPlayer(); + if (playerShareHandlingState.isHandlingSharable(player, Sharables.BED_SPAWN)) { return; } - Player player = event.getPlayer(); Location newSpawnLoc = event.getNewSpawn(); if (event.getCause() == PlayerSpawnChangeEvent.Cause.BED) { updatePlayerSpawn(player, new RespawnLocation( diff --git a/src/main/java/org/mvplugins/multiverse/inventories/share/Sharables.java b/src/main/java/org/mvplugins/multiverse/inventories/share/Sharables.java index cee4e426..fd017338 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/share/Sharables.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/share/Sharables.java @@ -7,7 +7,6 @@ import org.bukkit.World; import org.bukkit.advancement.AdvancementProgress; import org.bukkit.inventory.Inventory; -import org.jetbrains.annotations.ApiStatus; import org.mvplugins.multiverse.core.economy.MVEconomist; import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter; import org.mvplugins.multiverse.core.utils.ReflectHelper; @@ -33,6 +32,7 @@ import org.bukkit.potion.PotionEffect; import org.mvplugins.multiverse.inventories.util.RespawnLocation; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -45,7 +45,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Collectors; import static org.mvplugins.multiverse.inventories.util.MinecraftTools.findBedFromRespawnLocation; @@ -610,23 +610,23 @@ public boolean updatePlayer(Player player, ProfileData profile) { Location loc = profile.get(BED_SPAWN); if (loc == null) { Logging.finer("No respawn location saved"); - setSpawnLocation(player, player.getWorld().getSpawnLocation()); + player.setBedSpawnLocation(player.getWorld().getSpawnLocation(), true); return false; } World loclWorld = Try.of(loc::getWorld).getOrNull(); if (loclWorld == null) { Logging.warning("Respawn location has invalid world!"); - setSpawnLocation(player, player.getWorld().getSpawnLocation()); + player.setBedSpawnLocation(player.getWorld().getSpawnLocation(), true); return false; } if (inventoriesConfig.getValidateBedAnchorRespawnLocation() && loc instanceof RespawnLocation respawnLocation && !respawnLocation.isValidRespawnLocation()) { Logging.finer("Respawn location validation failed for respawn type: " + respawnLocation.getRespawnType()); - setSpawnLocation(player, player.getWorld().getSpawnLocation()); + player.setBedSpawnLocation(player.getWorld().getSpawnLocation(), true); return false; } - setSpawnLocation(player, loc); + player.setBedSpawnLocation(loc, true); Logging.finer("updated respawn location: " + player.getBedSpawnLocation()); return true; } @@ -634,22 +634,9 @@ public boolean updatePlayer(Player player, ProfileData profile) { new LocationSerializer.RespawnLocationSerializer()) .altName("bedspawn").altName("bed").altName("beds").altName("bedspawns").build(); - // todo: handle this somewhere better - private static final List ignoreSpawnListener = new ArrayList<>(); private static final boolean hasSetSpawnEvent = ReflectHelper.hasClass("org.bukkit.event.player.PlayerSpawnChangeEvent") || ReflectHelper.hasClass("com.destroystokyo.paper.event.player.PlayerSetSpawnEvent"); - private static void setSpawnLocation(Player player, Location loc) { - ignoreSpawnListener.add(player.getUniqueId()); - player.setBedSpawnLocation(loc, true); - ignoreSpawnListener.remove(player.getUniqueId()); - } - - @ApiStatus.Internal - public static boolean isIgnoringSpawnListener(Player player) { - return ignoreSpawnListener.contains(player.getUniqueId()); - } - /** * Sharing Last Location. */ @@ -789,6 +776,7 @@ public boolean updatePlayer(Player player, ProfileData profile) { player.setExp(exp); player.setLevel(level); player.setTotalExperience(totalExperience); + sendAdvancementUpdateWithoutToast.accept(player); if (announceAdvancements) { player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true); } @@ -797,6 +785,27 @@ public boolean updatePlayer(Player player, ProfileData profile) { } }).defaultSerializer(new ProfileEntry(false, "advancements")).altName("achievements").optional().build(); + private static final Consumer sendAdvancementUpdateWithoutToast; + + static { + // use reflection to send advancement update without client toast + // should work 1.21.5+ papermc unless Mojang changes things again + Option> craftPlayerClass = Option.of(ReflectHelper.getClass("org.bukkit.craftbukkit.entity.CraftPlayer")); + Option getHandleMethod = craftPlayerClass.map(cls -> ReflectHelper.getMethod(cls, "getHandle")); + Option> serverPlayerClass = Option.of(ReflectHelper.getClass("net.minecraft.server.level.ServerPlayer")); + Option getAdvancementsMethod = serverPlayerClass.map(cls -> ReflectHelper.getMethod(cls, "getAdvancements")); + Option> playerAdvancementsClass = Option.of(ReflectHelper.getClass("net.minecraft.server.PlayerAdvancements")); + Option flushDirtyMethod = playerAdvancementsClass.flatMap(cls -> + serverPlayerClass.map(cls2 -> ReflectHelper.getMethod(cls, "flushDirty", cls2, boolean.class))); + + sendAdvancementUpdateWithoutToast = player -> getHandleMethod.flatMap(method -> + Try.of(() -> method.invoke(player)).toOption()) + .flatMap(serverPlayer -> getAdvancementsMethod.flatMap(method -> + Try.of(() -> method.invoke(serverPlayer)).toOption()) + .flatMap(playerAdvancements -> flushDirtyMethod.flatMap(method -> + Try.of(() -> method.invoke(playerAdvancements, serverPlayer, false)).toOption()))); + } + /** * Sharing Statistics. */ diff --git a/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt index 6466267e..3932f036 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt @@ -25,7 +25,7 @@ class InjectionTest : TestWithMockBukkit() { @Test fun `InventoriesListener is available as a service`() { - assertEquals(5, serviceLocator.getAllActiveServices(MVInvListener::class.java).size) + assertEquals(6, serviceLocator.getAllActiveServices(MVInvListener::class.java).size) } @Test