From da9c2ac4cc825a3a567cd0d5ac547941cb6c1822 Mon Sep 17 00:00:00 2001 From: Daniel Walsh Date: Wed, 7 Feb 2024 09:26:09 +0000 Subject: [PATCH 01/22] Move PlayerProfile saving off the main thread (#4119) * Move PlayerProfile off main thread, add debugs and improve tab completion for debug Moved the PlayerProfile saving off the main thread, we generally load this off-thread but now we also save off-thread. I thought we were already doing this but apparently not, especially with our current YAML stuff this should definitely be done Also done a small change to ensure that we don't remove the PlayerProfile from memory if the player is still online. I don't think we ever had a reported issue from this but it's kinda weird behaviour Finally, added some debug logs to the saving logic, this can be enabled with `sf debug slimefun_player_profile_data`. Also added auto-complete to /sf debug because it's nice, this only works for Slimefun test cases rather than addons but that's fine. Mostly internal anyway * Update src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java --------- Co-authored-by: Alessio Colombo <37039432+Sfiguz7@users.noreply.github.com> --- .../slimefun4/api/player/PlayerProfile.java | 5 ++++- .../core/commands/SlimefunTabCompleter.java | 8 +++++++ .../slimefun4/core/debug/TestCase.java | 12 ++++++++++- .../core/services/AutoSavingService.java | 21 ++++++++++++++++--- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java index 96290f4849..7185471363 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java @@ -8,7 +8,6 @@ import java.util.Set; import java.util.UUID; import java.util.function.Consumer; -import java.util.stream.IntStream; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -35,6 +34,8 @@ import io.github.thebusybiscuit.slimefun4.api.researches.Research; import io.github.thebusybiscuit.slimefun4.core.attributes.ProtectionType; import io.github.thebusybiscuit.slimefun4.core.attributes.ProtectiveArmor; +import io.github.thebusybiscuit.slimefun4.core.debug.Debug; +import io.github.thebusybiscuit.slimefun4.core.debug.TestCase; import io.github.thebusybiscuit.slimefun4.core.guide.GuideHistory; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.implementation.items.armor.SlimefunArmorPiece; @@ -237,6 +238,7 @@ public void removeWaypoint(@Nonnull Waypoint waypoint) { * The profile can then be removed from RAM. */ public final void markForDeletion() { + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Marking {} ({}) profile for deletion", name, ownerId); markedForDeletion = true; } @@ -244,6 +246,7 @@ public final void markForDeletion() { * Call this method if this Profile has unsaved changes. */ public final void markDirty() { + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Marking {} ({}) profile as dirty", name, ownerId); dirty = true; } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/SlimefunTabCompleter.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/SlimefunTabCompleter.java index 07ca70bf0c..50a665f308 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/SlimefunTabCompleter.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/SlimefunTabCompleter.java @@ -16,6 +16,7 @@ import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; import io.github.thebusybiscuit.slimefun4.api.researches.Research; +import io.github.thebusybiscuit.slimefun4.core.debug.TestCase; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; class SlimefunTabCompleter implements TabCompleter { @@ -33,6 +34,13 @@ public SlimefunTabCompleter(@Nonnull SlimefunCommand command) { public List onTabComplete(CommandSender sender, Command cmd, String label, String[] args) { if (args.length == 1) { return createReturnList(command.getSubCommandNames(), args[0]); + } else if (args.length == 2) { + if (args[0].equalsIgnoreCase("debug")) { + return createReturnList(TestCase.VALUES_LIST, args[1]); + } else { + // Returning null will make it fallback to the default arguments (all online players) + return null; + } } else if (args.length == 3) { if (args[0].equalsIgnoreCase("give")) { return createReturnList(getSlimefunItems(), args[2]); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/TestCase.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/TestCase.java index 00b3bbf70c..dec31592d2 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/TestCase.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/TestCase.java @@ -1,5 +1,7 @@ package io.github.thebusybiscuit.slimefun4.core.debug; +import java.util.Arrays; +import java.util.List; import java.util.Locale; import javax.annotation.Nonnull; @@ -17,7 +19,15 @@ public enum TestCase { * being checked and why it is comparing IDs or meta. * This is helpful for us to check into why input nodes are taking a while for servers. */ - CARGO_INPUT_TESTING; + CARGO_INPUT_TESTING, + + /** + * Debug information regarding player profile loading, saving and handling. + * This is an area we're currently changing quite a bit and this will help ensure we're doing it safely + */ + PLAYER_PROFILE_DATA; + + public static final List VALUES_LIST = Arrays.stream(values()).map(TestCase::toString).toList(); TestCase() {} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AutoSavingService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AutoSavingService.java index a0455323f6..060ce0d772 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AutoSavingService.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AutoSavingService.java @@ -13,6 +13,8 @@ import org.bukkit.entity.Player; import io.github.thebusybiscuit.slimefun4.api.player.PlayerProfile; +import io.github.thebusybiscuit.slimefun4.core.debug.Debug; +import io.github.thebusybiscuit.slimefun4.core.debug.TestCase; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import me.mrCookieSlime.Slimefun.api.BlockStorage; @@ -39,9 +41,8 @@ public class AutoSavingService { public void start(@Nonnull Slimefun plugin, int interval) { this.interval = interval; - plugin.getServer().getScheduler().runTaskTimer(plugin, this::saveAllPlayers, 2000L, interval * 60L * 20L); + plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, this::saveAllPlayers, 2000L, interval * 60L * 20L); plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, this::saveAllBlocks, 2000L, interval * 60L * 20L); - } /** @@ -52,16 +53,30 @@ private void saveAllPlayers() { Iterator iterator = PlayerProfile.iterator(); int players = 0; + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Saving all players data"); + while (iterator.hasNext()) { PlayerProfile profile = iterator.next(); if (profile.isDirty()) { players++; profile.save(); + + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Saved data for {} ({})", + profile.getPlayer() != null ? profile.getPlayer().getName() : "Unknown", profile.getUUID() + ); } - if (profile.isMarkedForDeletion()) { + // Remove the PlayerProfile from memory if the player has left the server (marked from removal) + // and they're still not on the server + // At this point, we've already saved their profile so we can safely remove it + // without worry for having a data sync issue (e.g. data is changed but then we try to re-load older data) + if (profile.isMarkedForDeletion() && profile.getPlayer() == null) { iterator.remove(); + + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Removed data from memory for {}", + profile.getUUID() + ); } } From f38601b20ed80a69d8988ff400d2327661684019 Mon Sep 17 00:00:00 2001 From: Daniel Walsh Date: Thu, 8 Feb 2024 04:27:42 +0000 Subject: [PATCH 02/22] Multi tools won't reset their modes after server restart Co-authored-by: womzil <28115365+womzil@users.noreply.github.com> Co-authored-by: Fury_Phoenix <64714532+Phoenix-Starlight@users.noreply.github.com> Co-authored-by: Daniel Walsh --- .../items/electric/gadgets/MultiTool.java | 51 ++++++++++++++----- .../items/electric/gadgets/MultiToolMode.java | 23 ++++++++- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/gadgets/MultiTool.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/gadgets/MultiTool.java index 56707206d4..d3087016aa 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/gadgets/MultiTool.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/gadgets/MultiTool.java @@ -1,17 +1,19 @@ package io.github.thebusybiscuit.slimefun4.implementation.items.electric.gadgets; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import io.github.bakedlibs.dough.common.ChatColors; +import io.github.bakedlibs.dough.data.persistent.PersistentDataAPI; import org.bukkit.ChatColor; +import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; @@ -26,18 +28,20 @@ /** * The {@link MultiTool} is an electric device which can mimic * the behaviour of any other {@link SlimefunItem}. - * - * @author TheBusyBiscuit * + * @author TheBusyBiscuit */ public class MultiTool extends SlimefunItem implements Rechargeable { private static final float COST = 0.3F; - private final Map selectedMode = new HashMap<>(); private final List modes = new ArrayList<>(); private final float capacity; + private static final NamespacedKey key = new NamespacedKey(Slimefun.instance(), "multitool_mode"); + private static final String LORE_PREFIX = ChatColors.color("&8\u21E8 &7Mode: "); + private static final Pattern REGEX = Pattern.compile(ChatColors.color("(&c&o)?" + LORE_PREFIX) + "(.+)"); + @ParametersAreNonnullByDefault public MultiTool(ItemGroup itemGroup, SlimefunItemStack item, RecipeType recipeType, ItemStack[] recipe, float capacity, String... items) { super(itemGroup, item, recipeType, recipe); @@ -73,17 +77,15 @@ protected ItemUseHandler getItemUseHandler() { return e -> { Player p = e.getPlayer(); ItemStack item = e.getItem(); + ItemMeta meta = item.getItemMeta(); e.cancel(); - int index = selectedMode.getOrDefault(p.getUniqueId(), 0); + int index = PersistentDataAPI.getInt(meta, key); + SlimefunItem sfItem = modes.get(index).getItem(); if (!p.isSneaking()) { - if (removeItemCharge(item, COST)) { - SlimefunItem sfItem = modes.get(index).getItem(); - - if (sfItem != null) { - sfItem.callItemHandler(ItemUseHandler.class, handler -> handler.onRightClick(e)); - } + if (sfItem != null && removeItemCharge(item, COST)) { + sfItem.callItemHandler(ItemUseHandler.class, handler -> handler.onRightClick(e)); } } else { index = nextIndex(index); @@ -91,7 +93,28 @@ protected ItemUseHandler getItemUseHandler() { SlimefunItem selectedItem = modes.get(index).getItem(); String itemName = selectedItem != null ? selectedItem.getItemName() : "Unknown"; Slimefun.getLocalization().sendMessage(p, "messages.multi-tool.mode-change", true, msg -> msg.replace("%device%", "Multi Tool").replace("%mode%", ChatColor.stripColor(itemName))); - selectedMode.put(p.getUniqueId(), index); + + PersistentDataAPI.setInt(meta, key, index); + + List lore = meta.hasLore() ? meta.getLore() : new ArrayList<>(); + + boolean regexMatchFound = false; + for (int i = 0; i < lore.size(); i++) { + String line = lore.get(i); + + if (REGEX.matcher(line).matches()) { + lore.set(i, LORE_PREFIX + ChatColor.stripColor(itemName)); + regexMatchFound = true; + break; + } + } + + if (!regexMatchFound) { + lore.add(2, LORE_PREFIX + ChatColor.stripColor(itemName)); + } + + meta.setLore(lore); + item.setItemMeta(meta); } }; } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/gadgets/MultiToolMode.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/gadgets/MultiToolMode.java index 7bf079771b..d3963496d4 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/gadgets/MultiToolMode.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/gadgets/MultiToolMode.java @@ -8,22 +8,43 @@ class MultiToolMode { + private final int id; + private final String itemId; private final ItemSetting item; private final ItemSetting enabled; + // TODO: Move "id" into some NamespacedKey MultiToolMode(@Nonnull MultiTool multiTool, int id, @Nonnull String itemId) { + this.id = id; + this.itemId = itemId; this.item = new ItemSetting<>(multiTool, "mode." + id + ".item", itemId); this.enabled = new ItemSetting<>(multiTool, "mode." + id + ".enabled", true); multiTool.addItemSetting(item, enabled); } + /** + * This method is deprecated and should not be used. + * + * + * @return The ID of this mode + */ + @Deprecated(since = "RC-37", forRemoval = true) + public int getId() { + return id; + } + @Nullable SlimefunItem getItem() { return SlimefunItem.getById(item.getValue()); } + @Nonnull + String getItemId() { + return itemId; + } + boolean isEnabled() { return enabled.getValue(); } -} +} \ No newline at end of file From 86fa6f890019eef8d474b485552dfb8f00a4415d Mon Sep 17 00:00:00 2001 From: Daniel Walsh Date: Sat, 10 Feb 2024 02:09:44 +0000 Subject: [PATCH 03/22] Add update warning to /sf versions (#4096) --- pom.xml | 2 +- .../commands/subcommands/VersionsCommand.java | 18 ++++++++++++-- .../core/services/UpdaterService.java | 24 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 48e7d9db95..73c46273c6 100644 --- a/pom.xml +++ b/pom.xml @@ -349,7 +349,7 @@ com.github.baked-libs.dough dough-api - fcdbd45aa0 + 1108163a49 compile diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/VersionsCommand.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/VersionsCommand.java index da4b8d4e06..24abb7364c 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/VersionsCommand.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/VersionsCommand.java @@ -65,11 +65,25 @@ public void onExecute(@Nonnull CommandSender sender, @Nonnull String[] args) { .append(serverSoftware) .color(ChatColor.GREEN) .append(" " + Bukkit.getVersion() + '\n') - .color(ChatColor.DARK_GREEN) + .color(ChatColor.DARK_GREEN); + + builder .append("Slimefun ") .color(ChatColor.GREEN) - .append(Slimefun.getVersion() + '\n') + .append(Slimefun.getVersion()) .color(ChatColor.DARK_GREEN); + if (!Slimefun.getUpdater().isLatestVersion()) { + builder + .append(" (").color(ChatColor.GRAY) + .append("Update available").color(ChatColor.RED).event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text( + "Your Slimefun version is out of date!\n" + + "Please update to get the latest bug fixes and performance improvements.\n" + + "Please do not report any bugs without updating first." + ))) + .append(")").color(ChatColor.GRAY); + } + + builder.append("\n"); // @formatter:on if (Slimefun.getMetricsService().getVersion() != null) { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/UpdaterService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/UpdaterService.java index b556789a91..ac21007fce 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/UpdaterService.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/UpdaterService.java @@ -1,6 +1,7 @@ package io.github.thebusybiscuit.slimefun4.core.services; import java.io.File; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import javax.annotation.Nonnull; @@ -110,6 +111,29 @@ public int getBuildNumber() { return -1; } + public int getLatestVersion() { + if (updater != null && updater.getLatestVersion().isDone()) { + PrefixedVersion version; + try { + version = updater.getLatestVersion().get(); + return version.getVersionNumber(); + } catch (InterruptedException | ExecutionException e) { + return -1; + } + } + + return -1; + } + + public boolean isLatestVersion() { + if (getBuildNumber() == -1 || getLatestVersion() == -1) { + // We don't know if we're latest so just report we are + return true; + } + + return getBuildNumber() == getLatestVersion(); + } + /** * This will start the {@link UpdaterService} and check for updates. * If it can find an update it will automatically be installed. From 98bc59efc91d2b66396822dc0228b89ec27382b1 Mon Sep 17 00:00:00 2001 From: Daniel Walsh Date: Sat, 10 Feb 2024 10:43:52 +0000 Subject: [PATCH 04/22] Fixes #4123 - Coal Generator will no longer be locked after researching (#4124) Due to a logic bug in the Legacy storage backend if there was a duplicate ID it would mark it as researched for the first, then see it researched already and remove it on the second. This was happening for the Coal Generator and Bio Reactor here. Both shared he same research ID 173. We're just doing this fix for now until we can move away from the legacy backend (work in progress). --- pom.xml | 13 +++++ .../storage/backend/legacy/LegacyStorage.java | 12 ++++- .../storage/backend/TestLegacyBackend.java | 49 ++++++++++++++++++- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 73c46273c6..411d3b015c 100644 --- a/pom.xml +++ b/pom.xml @@ -406,8 +406,21 @@ org.jetbrains annotations + + io.papermc.paper + paper-api + + + + + + io.papermc.paper + paper-api + 1.20.4-R0.1-20240205.114523-90 + test + diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java index d7981a5466..59b0c82b96 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java @@ -91,7 +91,17 @@ public void savePlayerData(@Nonnull UUID uuid, @Nonnull PlayerData data) { playerFile.setValue("researches." + research.getID(), true); // Remove the research if it's no longer researched - } else if (playerFile.contains("researches." + research.getID())) { + // ---- + // We have a duplicate ID (173) used for both Coal Gen and Bio Reactor + // If you researched the Goal Gen we would remove it on save if you didn't also have the Bio Reactor + // Due to the fact we would set it as researched (true in the branch above) on Coal Gen + // but then go into this branch and remove it if you didn't have Bio Reactor + // Sooooo we're gonna hack this for now while we move away from the Legacy Storage + // Let's make sure the user doesn't have _any_ research with this ID and _then_ remove it + } else if ( + playerFile.contains("researches." + research.getID()) + && !data.getResearches().stream().anyMatch((r) -> r.getID() == research.getID()) + ) { playerFile.setValue("researches." + research.getID(), null); } } diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/storage/backend/TestLegacyBackend.java b/src/test/java/io/github/thebusybiscuit/slimefun4/storage/backend/TestLegacyBackend.java index c8e1916f54..98653ee5b7 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/storage/backend/TestLegacyBackend.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/storage/backend/TestLegacyBackend.java @@ -17,6 +17,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -52,8 +53,6 @@ public static void load() { // within the class isn't being fired (where ItemStack and other classes are registered) ConfigurationSerialization.registerClass(ItemStack.class); ConfigurationSerialization.registerClass(ItemMeta.class); - - setupResearches(); } @AfterAll @@ -62,9 +61,16 @@ public static void unload() throws IOException { FileUtils.deleteDirectory(new File("data-storage")); } + @AfterEach + public void cleanup() { + Slimefun.getRegistry().getResearches().clear(); + } + // Test simple loading and saving of player data @Test void testLoadingResearches() throws IOException { + setupResearches(); + // Create a player file which we can load UUID uuid = UUID.randomUUID(); File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); @@ -184,6 +190,8 @@ void testLoadingWaypoints() throws IOException { @Test void testSavingResearches() throws InterruptedException { + setupResearches(); + // Create a player file which we can load UUID uuid = UUID.randomUUID(); File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); @@ -279,6 +287,8 @@ void testSavingWaypoints() throws InterruptedException { // Test realistic situations @Test void testResearchChanges() throws InterruptedException { + setupResearches(); + UUID uuid = UUID.randomUUID(); File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); @@ -372,6 +382,41 @@ void testWaypointChanges() throws InterruptedException { Assertions.assertEquals(1, assertion.getWaypoints().size()); } + @Test + void testDuplicateResearchesDontGetUnResearched() throws InterruptedException { + // Create a player file which we can load + UUID uuid = UUID.randomUUID(); + File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); + + OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); + PlayerProfile profile = TestUtilities.awaitProfile(player); + + // Setup initial research + NamespacedKey initialKey = new NamespacedKey(plugin, "test_1"); + Research initialResearch = new Research(initialKey, 1, "Test 1", 100); + initialResearch.register(); + + // Setup duplicate research + // Keep the ID as 1 but change name and key + NamespacedKey duplicateKey = new NamespacedKey(plugin, "test_2"); + Research duplicateResearch = new Research(duplicateKey, 1, "Test 2", 100); + duplicateResearch.register(); + + profile.setResearched(initialResearch, true); + + // Save the player data + LegacyStorage storage = new LegacyStorage(); + storage.savePlayerData(uuid, profile.getPlayerData()); + + // Assert the file exists and data is correct + Assertions.assertTrue(playerFile.exists()); + PlayerData assertion = storage.loadPlayerData(uuid); + // Will have both the initial and duplicate research + Assertions.assertEquals(2, assertion.getResearches().size()); + Assertions.assertTrue(assertion.getResearches().contains(initialResearch)); + Assertions.assertTrue(assertion.getResearches().contains(duplicateResearch)); + } + // Utils private static void setupResearches() { for (int i = 0; i < 10; i++) { From 5be47186843534f0792394573612b0a5d20f4e0a Mon Sep 17 00:00:00 2001 From: Daniel Walsh Date: Wed, 14 Feb 2024 14:56:33 +0000 Subject: [PATCH 05/22] Fixes exhaustion when loading large profiles (#4127) --- .../thebusybiscuit/slimefun4/Threads.java | 23 +++++++++ .../slimefun4/api/player/PlayerProfile.java | 51 ++++++++++++++++--- 2 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 src/main/java/io/github/thebusybiscuit/slimefun4/Threads.java diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/Threads.java b/src/main/java/io/github/thebusybiscuit/slimefun4/Threads.java new file mode 100644 index 0000000000..d109bcae9d --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/Threads.java @@ -0,0 +1,23 @@ +package io.github.thebusybiscuit.slimefun4; + +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.plugin.java.JavaPlugin; + +public class Threads { + + @ParametersAreNonnullByDefault + public static void newThread(JavaPlugin plugin, String name, Runnable runnable) { + // TODO: Change to thread pool + new Thread(runnable, plugin.getName() + " - " + name).start(); + } + + public static String getCaller() { + // First item will be getting the call stack + // Second item will be this call + // Third item will be the func we care about being called + // And finally will be the caller + StackTraceElement element = Thread.currentThread().getStackTrace()[3]; + return element.getClassName() + "." + element.getMethodName() + ":" + element.getLineNumber(); + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java index 7185471363..de5432fd1b 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java @@ -3,10 +3,12 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import javax.annotation.Nonnull; @@ -28,6 +30,7 @@ import io.github.bakedlibs.dough.common.ChatColors; import io.github.bakedlibs.dough.common.CommonPatterns; import io.github.bakedlibs.dough.config.Config; +import io.github.thebusybiscuit.slimefun4.Threads; import io.github.thebusybiscuit.slimefun4.api.events.AsyncProfileLoadEvent; import io.github.thebusybiscuit.slimefun4.api.gps.Waypoint; import io.github.thebusybiscuit.slimefun4.api.items.HashedArmorpiece; @@ -55,6 +58,8 @@ */ public class PlayerProfile { + private static final Map loading = new ConcurrentHashMap<>(); + private final UUID ownerId; private final String name; @@ -361,17 +366,37 @@ public static boolean fromUUID(@Nonnull UUID uuid, @Nonnull Consumer callback) { Validate.notNull(p, "Cannot get a PlayerProfile for: null!"); - UUID uuid = p.getUniqueId(); + + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Getting PlayerProfile for {}", uuid); + PlayerProfile profile = Slimefun.getRegistry().getPlayerProfiles().get(uuid); if (profile != null) { + Debug.log(TestCase.PLAYER_PROFILE_DATA, "PlayerProfile for {} was already loaded", uuid); callback.accept(profile); return true; } - Bukkit.getScheduler().runTaskAsynchronously(Slimefun.instance(), () -> { + // If we're already loading, we don't want to spin up a whole new thread and load the profile again/more + // This can very easily cause CPU, memory and thread exhaustion if the profile is large + // See #4011, #4116 + if (loading.containsKey(uuid)) { + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Attempted to get PlayerProfile ({}) while loading", uuid); + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Caller: {}", Threads.getCaller()); + + // We can't easily consume the callback so we will throw it away in this case + // This will mean that if a user has attempted to do an action like open a block while + // their profile is still loading. Instead of it opening after a second or whatever when the + // profile is loaded, they will have to explicitly re-click the block/item/etc. + // This isn't the best but I think it's totally reasonable. + return false; + } + + loading.put(uuid, true); + Threads.newThread(Slimefun.instance(), "PlayerProfile#get(" + uuid + ")", () -> { PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(p.getUniqueId()); + loading.remove(uuid); AsyncProfileLoadEvent event = new AsyncProfileLoadEvent(new PlayerProfile(p, data)); Bukkit.getPluginManager().callEvent(event); @@ -394,14 +419,28 @@ public static boolean get(@Nonnull OfflinePlayer p, @Nonnull Consumer { - PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(p.getUniqueId()); + Threads.newThread(Slimefun.instance(), "PlayerProfile#request(" + uuid + ")", () -> { + PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(uuid); + loading.remove(uuid); PlayerProfile pp = new PlayerProfile(p, data); - Slimefun.getRegistry().getPlayerProfiles().put(p.getUniqueId(), pp); + Slimefun.getRegistry().getPlayerProfiles().put(uuid, pp); }); return false; From 8666bbc3d162e49f328db0e85223bc2dcdd9c5a4 Mon Sep 17 00:00:00 2001 From: Daniel Walsh Date: Fri, 16 Feb 2024 21:52:10 +0000 Subject: [PATCH 06/22] Fixes guide search when using colored chat (#4125) --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 411d3b015c..d6798d504d 100644 --- a/pom.xml +++ b/pom.xml @@ -345,11 +345,10 @@ - com.github.baked-libs.dough dough-api - 1108163a49 + 0130f8d9ce compile From bf402068f7b08190da0ce0a74d9b278e5d125637 Mon Sep 17 00:00:00 2001 From: Daniel Walsh Date: Sat, 17 Feb 2024 16:23:39 +0000 Subject: [PATCH 07/22] Add new analytics service (#4067) Co-authored-by: Alessio Colombo <37039432+Sfiguz7@users.noreply.github.com> --- README.md | 8 + .../thebusybiscuit/slimefun4/Threads.java | 23 --- .../slimefun4/api/player/PlayerProfile.java | 7 +- .../slimefun4/core/debug/TestCase.java | 9 +- .../core/services/AnalyticsService.java | 158 ++++++++++++++++++ .../core/services/ThreadService.java | 99 +++++++++++ .../services/profiler/SlimefunProfiler.java | 29 ++++ .../slimefun4/implementation/Slimefun.java | 28 +++- .../storage/backend/legacy/LegacyStorage.java | 10 ++ src/main/resources/config.yml | 1 + 10 files changed, 342 insertions(+), 30 deletions(-) delete mode 100644 src/main/java/io/github/thebusybiscuit/slimefun4/Threads.java create mode 100644 src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AnalyticsService.java create mode 100644 src/main/java/io/github/thebusybiscuit/slimefun4/core/services/ThreadService.java diff --git a/README.md b/README.md index 46ce4c0096..affc3ed96c 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,14 @@ For more info see [bStats' Privacy Policy](https://bstats.org/privacy-policy) Our [bStats Module](https://github.com/Slimefun/MetricsModule) is downloaded automatically when installing this Plugin, this module will automatically update on server starts independently from the main plugin. This way we can automatically roll out updates to the bStats module, in cases of severe performance issues for example where live data and insight into what is impacting performance can be crucial. These updates can of course be disabled under `/plugins/Slimefun/config.yml`. To disable metrics collection as a whole, see the paragraph above. +--- + +Slimefun also uses its own analytics system to collect anonymous information about the performance of this plugin.
+This is solely for statistical purposes, as we are interested in how it's performing for all servers.
+All available data is anonymous and aggregated, at no point can we see individual server information.
+ +You can also disable this behaviour under `/plugins/Slimefun/config.yml`.
+
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/Threads.java b/src/main/java/io/github/thebusybiscuit/slimefun4/Threads.java deleted file mode 100644 index d109bcae9d..0000000000 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/Threads.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.thebusybiscuit.slimefun4; - -import javax.annotation.ParametersAreNonnullByDefault; - -import org.bukkit.plugin.java.JavaPlugin; - -public class Threads { - - @ParametersAreNonnullByDefault - public static void newThread(JavaPlugin plugin, String name, Runnable runnable) { - // TODO: Change to thread pool - new Thread(runnable, plugin.getName() + " - " + name).start(); - } - - public static String getCaller() { - // First item will be getting the call stack - // Second item will be this call - // Third item will be the func we care about being called - // And finally will be the caller - StackTraceElement element = Thread.currentThread().getStackTrace()[3]; - return element.getClassName() + "." + element.getMethodName() + ":" + element.getLineNumber(); - } -} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java index de5432fd1b..00cacd4cd9 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java @@ -30,7 +30,6 @@ import io.github.bakedlibs.dough.common.ChatColors; import io.github.bakedlibs.dough.common.CommonPatterns; import io.github.bakedlibs.dough.config.Config; -import io.github.thebusybiscuit.slimefun4.Threads; import io.github.thebusybiscuit.slimefun4.api.events.AsyncProfileLoadEvent; import io.github.thebusybiscuit.slimefun4.api.gps.Waypoint; import io.github.thebusybiscuit.slimefun4.api.items.HashedArmorpiece; @@ -383,7 +382,6 @@ public static boolean get(@Nonnull OfflinePlayer p, @Nonnull Consumer { + Slimefun.getThreadService().newThread(Slimefun.instance(), "PlayerProfile#get(" + uuid + ")", () -> { PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(p.getUniqueId()); loading.remove(uuid); @@ -428,14 +426,13 @@ public static boolean request(@Nonnull OfflinePlayer p) { // See #4011, #4116 if (loading.containsKey(uuid)) { Debug.log(TestCase.PLAYER_PROFILE_DATA, "Attempted to request PlayerProfile ({}) while loading", uuid); - Debug.log(TestCase.PLAYER_PROFILE_DATA, "Caller: {}", Threads.getCaller()); return false; } if (!Slimefun.getRegistry().getPlayerProfiles().containsKey(uuid)) { loading.put(uuid, true); // Should probably prevent multiple requests for the same profile in the future - Threads.newThread(Slimefun.instance(), "PlayerProfile#request(" + uuid + ")", () -> { + Slimefun.getThreadService().newThread(Slimefun.instance(), "PlayerProfile#request(" + uuid + ")", () -> { PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(uuid); loading.remove(uuid); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/TestCase.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/TestCase.java index dec31592d2..e41ecddc42 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/TestCase.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/TestCase.java @@ -6,6 +6,8 @@ import javax.annotation.Nonnull; +import io.github.thebusybiscuit.slimefun4.core.services.AnalyticsService; + /** * Test cases in Slimefun. These are very useful for debugging why behavior is happening. * Server owners can enable these with {@code /sf debug } @@ -25,7 +27,12 @@ public enum TestCase { * Debug information regarding player profile loading, saving and handling. * This is an area we're currently changing quite a bit and this will help ensure we're doing it safely */ - PLAYER_PROFILE_DATA; + PLAYER_PROFILE_DATA, + + /** + * Debug information regarding our {@link AnalyticsService}. + */ + ANALYTICS; public static final List VALUES_LIST = Arrays.stream(values()).map(TestCase::toString).toList(); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AnalyticsService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AnalyticsService.java new file mode 100644 index 0000000000..b0afd40657 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AnalyticsService.java @@ -0,0 +1,158 @@ +package io.github.thebusybiscuit.slimefun4.core.services; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import io.github.thebusybiscuit.slimefun4.core.debug.Debug; +import io.github.thebusybiscuit.slimefun4.core.debug.TestCase; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; + +/** + * This class represents an analytics service that sends data. + * This data is used to analyse performance of this {@link Plugin}. + *

+ * You can find more info in the README file of this Project on GitHub. + * + * @author WalshyDev + */ +public class AnalyticsService { + + private static final int VERSION = 1; + private static final String API_URL = "https://analytics.slimefun.dev/ingest"; + + private final JavaPlugin plugin; + private final HttpClient client = HttpClient.newHttpClient(); + + private boolean enabled; + + public AnalyticsService(JavaPlugin plugin) { + this.plugin = plugin; + } + + public void start() { + this.enabled = Slimefun.getCfg().getBoolean("metrics.analytics"); + + if (enabled) { + plugin.getLogger().info("Enabled Analytics Service"); + + // Send the timings data every minute + Slimefun.getThreadService().newScheduledThread( + plugin, + "AnalyticsService - Timings", + sendTimingsAnalytics(), + 1, + 1, + TimeUnit.MINUTES + ); + } + } + + // We'll send some timing data every minute. + // To date, we collect the tick interval, the avg timing per tick and avg timing per machine + @Nonnull + private Runnable sendTimingsAnalytics() { + return () -> { + double tickInterval = Slimefun.getTickerTask().getTickRate(); + // This is currently used by bStats in a ranged way, we'll move this + double totalTimings = Slimefun.getProfiler().getAndResetAverageNanosecondTimings(); + double avgPerMachine = Slimefun.getProfiler().getAverageTimingsPerMachine(); + + if (totalTimings == 0 || avgPerMachine == 0) { + Debug.log(TestCase.ANALYTICS, "Ignoring analytics data for server_timings as no data was found" + + " - total: " + totalTimings + ", avg: " + avgPerMachine); + // Ignore if no data + return; + } + + send("server_timings", new double[]{ + // double1 is schema version + tickInterval, // double2 + totalTimings, // double3 + avgPerMachine // double4 + }, null); + }; + } + + public void recordPlayerProfileDataTime(@Nonnull String backend, boolean load, long nanoseconds) { + send( + "player_profile_data_load_time", + new double[]{ + // double1 is schema version + nanoseconds, // double2 + load ? 1 : 0 // double3 - 1 if load, 0 if save + }, + new String[]{ + // blob1 is version + backend // blob2 + } + ); + } + + // Important: Keep the order of these doubles and blobs the same unless you increment the version number + // If a value is no longer used, just send null or replace it with a new value - don't shift the order + @ParametersAreNonnullByDefault + private void send(String id, double[] doubles, String[] blobs) { + // If not enabled or not official build (e.g. local build) or a unit test, just ignore. + if ( + !enabled + || !Slimefun.getUpdater().getBranch().isOfficial() + || Slimefun.instance().isUnitTest() + ) return; + + JsonObject object = new JsonObject(); + // Up to 1 index + JsonArray indexes = new JsonArray(); + indexes.add(id); + object.add("indexes", indexes); + + // Up to 20 doubles (including the version) + JsonArray doublesArray = new JsonArray(); + doublesArray.add(VERSION); + if (doubles != null) { + for (double d : doubles) { + doublesArray.add(d); + } + } + object.add("doubles", doublesArray); + + // Up to 20 blobs (including the version) + JsonArray blobsArray = new JsonArray(); + blobsArray.add(Slimefun.getVersion()); + if (blobs != null) { + for (String s : blobs) { + blobsArray.add(s); + } + } + object.add("blobs", blobsArray); + + Debug.log(TestCase.ANALYTICS, "Sending analytics data for " + id); + Debug.log(TestCase.ANALYTICS, object.toString()); + + // Send async, we do not care about the result. If it fails, that's fine. + client.sendAsync(HttpRequest.newBuilder() + .uri(URI.create(API_URL)) + .header("User-Agent", "Mozilla/5.0 Slimefun4 AnalyticsService") + .POST(HttpRequest.BodyPublishers.ofString(object.toString())) + .build(), + HttpResponse.BodyHandlers.discarding() + ).thenAcceptAsync((res) -> { + if (res.statusCode() == 200) { + Debug.log(TestCase.ANALYTICS, "Analytics data for " + id + " sent successfully"); + } else { + Debug.log(TestCase.ANALYTICS, "Analytics data for " + id + " failed to send - " + res.statusCode()); + } + }); + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/ThreadService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/ThreadService.java new file mode 100644 index 0000000000..772b65d3fe --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/ThreadService.java @@ -0,0 +1,99 @@ +package io.github.thebusybiscuit.slimefun4.core.services; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitScheduler; + +public final class ThreadService { + + private final ThreadGroup group; + private final ExecutorService cachedPool; + private final ScheduledExecutorService scheduledPool; + + public ThreadService(JavaPlugin plugin) { + this.group = new ThreadGroup(plugin.getName()); + this.cachedPool = Executors.newCachedThreadPool(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(group, r, plugin.getName() + " - ThreadService"); + } + }); + + this.scheduledPool = Executors.newScheduledThreadPool(1, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(group, r, plugin.getName() + " - ScheduledThreadService"); + } + }); + } + + /** + * Invoke a new thread from the cached thread pool with the given name. + * This is a much better alternative to using + * {@link BukkitScheduler#runTaskAsynchronously(org.bukkit.plugin.Plugin, Runnable)} + * as this will show not only the plugin but a useful name. + * By default, Bukkit will use "Craft Scheduler Thread - - " which is nice to show the plugin but + * it's impossible to track exactly what thread that is. + * + * @param plugin The {@link JavaPlugin} that is creating this thread + * @param name The name of this thread, this will be prefixed with the plugin's name + * @param runnable The {@link Runnable} to execute + */ + @ParametersAreNonnullByDefault + public void newThread(JavaPlugin plugin, String name, Runnable runnable) { + cachedPool.submit(() -> { + // This is a bit of a hack, but it's the only way to have the thread name be as desired + Thread.currentThread().setName(plugin.getName() + " - " + name); + runnable.run(); + }); + } + + /** + * Invoke a new scheduled thread from the cached thread pool with the given name. + * This is a much better alternative to using + * {@link BukkitScheduler#runTaskTimerAsynchronously(org.bukkit.plugin.Plugin, Runnable, long, long)} + * as this will show not only the plugin but a useful name. + * By default, Bukkit will use "Craft Scheduler Thread - - " which is nice to show the plugin but + * it's impossible to track exactly what thread that is. + * + * @param plugin The {@link JavaPlugin} that is creating this thread + * @param name The name of this thread, this will be prefixed with the plugin's name + * @param runnable The {@link Runnable} to execute + */ + @ParametersAreNonnullByDefault + public void newScheduledThread( + JavaPlugin plugin, + String name, + Runnable runnable, + long delay, + long period, + TimeUnit unit + ) { + this.scheduledPool.scheduleWithFixedDelay(() -> { + // This is a bit of a hack, but it's the only way to have the thread name be as desired + Thread.currentThread().setName(plugin.getName() + " - " + name); + runnable.run(); + }, delay, delay, unit); + } + + /** + * Get the caller of a given method, this should only be used for debugging purposes and is not performant. + * + * @return The caller of the method that called this method. + */ + public static String getCaller() { + // First item will be getting the call stack + // Second item will be this call + // Third item will be the func we care about being called + // And finally will be the caller + StackTraceElement element = Thread.currentThread().getStackTrace()[3]; + return element.getClassName() + "." + element.getMethodName() + ":" + element.getLineNumber(); + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/profiler/SlimefunProfiler.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/profiler/SlimefunProfiler.java index 2a1292225a..408fdc439e 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/profiler/SlimefunProfiler.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/profiler/SlimefunProfiler.java @@ -22,6 +22,8 @@ import org.bukkit.block.Block; import org.bukkit.scheduler.BukkitScheduler; +import com.google.common.util.concurrent.AtomicDouble; + import io.github.thebusybiscuit.slimefun4.api.SlimefunAddon; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; @@ -87,6 +89,8 @@ public class SlimefunProfiler { private final AtomicLong totalMsTicked = new AtomicLong(); private final AtomicInteger ticksPassed = new AtomicInteger(); + private final AtomicLong totalNsTicked = new AtomicLong(); + private final AtomicDouble averageTimingsPerMachine = new AtomicDouble(); /** * This method terminates the {@link SlimefunProfiler}. @@ -222,11 +226,14 @@ private void finishReport() { totalElapsedTime = timings.values().stream().mapToLong(Long::longValue).sum(); + averageTimingsPerMachine.getAndSet(timings.values().stream().mapToLong(Long::longValue).average().orElse(0)); + /* * We log how many milliseconds have been ticked, and how many ticks have passed * This is so when bStats requests the average timings, they're super quick to figure out */ totalMsTicked.addAndGet(TimeUnit.NANOSECONDS.toMillis(totalElapsedTime)); + totalNsTicked.addAndGet(totalElapsedTime); ticksPassed.incrementAndGet(); if (!requests.isEmpty()) { @@ -416,4 +423,26 @@ public long getAndResetAverageTimings() { return l; } + + /** + * Get and reset the average nanosecond timing for this {@link SlimefunProfiler}. + * + * @return The average nanosecond timing for this {@link SlimefunProfiler}. + */ + public double getAndResetAverageNanosecondTimings() { + long l = totalNsTicked.get() / ticksPassed.get(); + totalNsTicked.set(0); + ticksPassed.set(0); + + return l; + } + + /** + * Get and reset the average millisecond timing for each machine. + * + * @return The average millisecond timing for each machine. + */ + public double getAverageTimingsPerMachine() { + return averageTimingsPerMachine.getAndSet(0); + } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java index 28233ea741..ae065bc062 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java @@ -42,6 +42,7 @@ import io.github.thebusybiscuit.slimefun4.core.SlimefunRegistry; import io.github.thebusybiscuit.slimefun4.core.commands.SlimefunCommand; import io.github.thebusybiscuit.slimefun4.core.networks.NetworkManager; +import io.github.thebusybiscuit.slimefun4.core.services.AnalyticsService; import io.github.thebusybiscuit.slimefun4.core.services.AutoSavingService; import io.github.thebusybiscuit.slimefun4.core.services.BackupService; import io.github.thebusybiscuit.slimefun4.core.services.BlockDataService; @@ -52,6 +53,7 @@ import io.github.thebusybiscuit.slimefun4.core.services.MinecraftRecipeService; import io.github.thebusybiscuit.slimefun4.core.services.PerWorldSettingsService; import io.github.thebusybiscuit.slimefun4.core.services.PermissionsService; +import io.github.thebusybiscuit.slimefun4.core.services.ThreadService; import io.github.thebusybiscuit.slimefun4.core.services.UpdaterService; import io.github.thebusybiscuit.slimefun4.core.services.github.GitHubService; import io.github.thebusybiscuit.slimefun4.core.services.holograms.HologramsService; @@ -182,6 +184,8 @@ public class Slimefun extends JavaPlugin implements SlimefunAddon { private final MinecraftRecipeService recipeService = new MinecraftRecipeService(this); private final HologramsService hologramsService = new HologramsService(this); private final SoundService soundService = new SoundService(this); + private final ThreadService threadService = new ThreadService(this); + private final AnalyticsService analyticsService = new AnalyticsService(this); // Some other things we need private final IntegrationsManager integrations = new IntegrationsManager(this); @@ -309,8 +313,9 @@ private void onPluginStart() { playerStorage = new LegacyStorage(); logger.log(Level.INFO, "Using legacy storage for player data"); - // Setting up bStats + // Setting up bStats and analytics new Thread(metricsService::start, "Slimefun Metrics").start(); + analyticsService.start(); // Starting the Auto-Updater if (config.getBoolean("options.auto-update")) { @@ -901,6 +906,17 @@ public static SoundService getSoundService() { return instance.metricsService; } + /** + * This method returns the {@link AnalyticsService} of Slimefun. + * It is used to handle sending analytic information. + * + * @return The {@link AnalyticsService} for Slimefun + */ + public static @Nonnull AnalyticsService getAnalyticsService() { + validateInstance(); + return instance.analyticsService; + } + /** * This method returns the {@link GitHubService} of Slimefun. * It is used to retrieve data from GitHub repositories. @@ -1068,4 +1084,14 @@ public static boolean isNewlyInstalled() { public static @Nonnull Storage getPlayerStorage() { return instance().playerStorage; } + + /** + * This method returns the {@link ThreadService} of Slimefun. + * Do not use this if you're an addon. Please make your own {@link ThreadService}. + * + * @return The {@link ThreadService} for Slimefun + */ + public static @Nonnull ThreadService getThreadService() { + return instance().threadService; + } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java index 59b0c82b96..f051a3b846 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java @@ -27,6 +27,8 @@ public class LegacyStorage implements Storage { @Override public PlayerData loadPlayerData(@Nonnull UUID uuid) { + long start = System.nanoTime(); + Config playerFile = new Config("data-storage/Slimefun/Players/" + uuid + ".yml"); // Not too sure why this is its own file Config waypointsFile = new Config("data-storage/Slimefun/waypoints/" + uuid + ".yml"); @@ -73,12 +75,17 @@ public PlayerData loadPlayerData(@Nonnull UUID uuid) { } } + long end = System.nanoTime(); + Slimefun.getAnalyticsService().recordPlayerProfileDataTime("legacy", true, end - start); + return new PlayerData(researches, backpacks, waypoints); } // The current design of saving all at once isn't great, this will be refined. @Override public void savePlayerData(@Nonnull UUID uuid, @Nonnull PlayerData data) { + long start = System.nanoTime(); + Config playerFile = new Config("data-storage/Slimefun/Players/" + uuid + ".yml"); // Not too sure why this is its own file Config waypointsFile = new Config("data-storage/Slimefun/waypoints/" + uuid + ".yml"); @@ -133,5 +140,8 @@ public void savePlayerData(@Nonnull UUID uuid, @Nonnull PlayerData data) { // Save files playerFile.save(); waypointsFile.save(); + + long end = System.nanoTime(); + Slimefun.getAnalyticsService().recordPlayerProfileDataTime("legacy", false, end - start); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index cb133170e4..5e36b700b7 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -50,6 +50,7 @@ talismans: metrics: auto-update: true + analytics: true research-ranks: - Chicken From 3301a77ea977c89cf52f8d2d6573d98e39996557 Mon Sep 17 00:00:00 2001 From: J3fftw <44972470+J3fftw1@users.noreply.github.com> Date: Sat, 17 Feb 2024 19:32:00 +0100 Subject: [PATCH 08/22] fix dupe glitch with backpacks (#4134) --- .../slimefun4/api/player/PlayerProfile.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java index 00cacd4cd9..8dfc7414f8 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java @@ -394,12 +394,18 @@ public static boolean get(@Nonnull OfflinePlayer p, @Nonnull Consumer { PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(p.getUniqueId()); - loading.remove(uuid); AsyncProfileLoadEvent event = new AsyncProfileLoadEvent(new PlayerProfile(p, data)); Bukkit.getPluginManager().callEvent(event); Slimefun.getRegistry().getPlayerProfiles().put(uuid, event.getProfile()); + + // Make sure we call this after we put the PlayerProfile into the registry. + // Otherwise, we end up with a race condition where the profile is not in the map just _yet_ + // but the loading flag is gone and we can end up loading it a second time (and thus can dupe items) + // Fixes https://github.com/Slimefun/Slimefun4/issues/4130 + loading.remove(uuid); + callback.accept(event.getProfile()); }); @@ -434,10 +440,15 @@ public static boolean request(@Nonnull OfflinePlayer p) { // Should probably prevent multiple requests for the same profile in the future Slimefun.getThreadService().newThread(Slimefun.instance(), "PlayerProfile#request(" + uuid + ")", () -> { PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(uuid); - loading.remove(uuid); PlayerProfile pp = new PlayerProfile(p, data); Slimefun.getRegistry().getPlayerProfiles().put(uuid, pp); + + // Make sure we call this after we put the PlayerProfile into the registry. + // Otherwise, we end up with a race condition where the profile is not in the map just _yet_ + // but the loading flag is gone and we can end up loading it a second time (and thus can dupe items) + // Fixes https://github.com/Slimefun/Slimefun4/issues/4130 + loading.remove(uuid); }); return false; From a94ab1829b0d62f9245b7ca2669ae0fad75578da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:11:31 +0100 Subject: [PATCH 09/22] [CI skip] Update pascalgn/automerge-action action to v0.16.2 --- .github/workflows/auto-squash.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-squash.yml b/.github/workflows/auto-squash.yml index 050acaec2e..5357b8464c 100644 --- a/.github/workflows/auto-squash.yml +++ b/.github/workflows/auto-squash.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Auto squash - uses: pascalgn/automerge-action@v0.15.6 + uses: pascalgn/automerge-action@v0.16.2 env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} UPDATE_RETRIES: 0 @@ -42,7 +42,7 @@ jobs: steps: - name: Auto squash - uses: pascalgn/automerge-action@v0.15.6 + uses: pascalgn/automerge-action@v0.16.2 env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} UPDATE_RETRIES: 0 From 629a6f66052125208a7aa73cc47cc8f430641081 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:11:40 +0100 Subject: [PATCH 10/22] [CI skip] Update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.12.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d6798d504d..4191834abd 100644 --- a/pom.xml +++ b/pom.xml @@ -115,7 +115,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.12.0 + 3.12.1 From 29bafa56f1ab74a2332971df199401242b817678 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:11:52 +0100 Subject: [PATCH 11/22] [CI skip] Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.2.5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4191834abd..9c19eb9675 100644 --- a/pom.xml +++ b/pom.xml @@ -146,7 +146,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.3 + 3.2.5 org.junit.jupiter:junit-jupiter From 4dcd73bed32143a0409d55839ee4688a6d9d95b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:12:04 +0100 Subject: [PATCH 12/22] [CI skip] Update dependency org.mockito:mockito-core to v5.10.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9c19eb9675..34ca030d4a 100644 --- a/pom.xml +++ b/pom.xml @@ -382,7 +382,7 @@ org.mockito mockito-core - 5.9.0 + 5.10.0 test From c794301ac84cff2125ec71453d28c40b432d4fd0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:12:20 +0100 Subject: [PATCH 13/22] [CI skip] Update thollander/actions-comment-pull-request action to v2.5.0 --- .github/workflows/pr-labels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 0636851f90..b857b2faa6 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -31,7 +31,7 @@ jobs: api: '🔧 API' compatibility: '🤝 Compatibility' - - uses: thollander/actions-comment-pull-request@v2.4.3 + - uses: thollander/actions-comment-pull-request@v2.5.0 name: Leave a comment about the applied label if: ${{ steps.labeller.outputs.applied != 0 }} with: @@ -40,7 +40,7 @@ jobs: Your Pull Request was automatically labelled as: "${{ steps.labeller.outputs.applied }}" Thank you for contributing to this project! ❤️ - - uses: thollander/actions-comment-pull-request@v2.4.3 + - uses: thollander/actions-comment-pull-request@v2.5.0 name: Leave a comment about our branch naming convention if: ${{ steps.labeller.outputs.applied == 0 }} with: From 6502d30d2da8eb5944aeefcb35dfcf27fc47e089 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:12:27 +0100 Subject: [PATCH 14/22] [CI skip] Update dependency org.junit.jupiter:junit-jupiter to v5.10.2 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 34ca030d4a..e24cecaa78 100644 --- a/pom.xml +++ b/pom.xml @@ -376,7 +376,7 @@ org.junit.jupiter junit-jupiter - 5.10.1 + 5.10.2 test From 542ec4e8279af1bba52a402da24a0474270a60c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:12:55 +0100 Subject: [PATCH 15/22] [CI skip] Update dependency com.gmail.nossr50.mcMMO:mcMMO to v2.1.229 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e24cecaa78..49314fc6c6 100644 --- a/pom.xml +++ b/pom.xml @@ -453,7 +453,7 @@ com.gmail.nossr50.mcMMO mcMMO - 2.1.226 + 2.1.229 provided From 206a9d6f53c4354d4eededad0dcc380d3d8a820f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:30:40 +0100 Subject: [PATCH 16/22] [CI skip] Update dependency com.sk89q.worldedit:worldedit-bukkit to v7.2.19 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 49314fc6c6..4d0dbf7571 100644 --- a/pom.xml +++ b/pom.xml @@ -439,7 +439,7 @@ com.sk89q.worldedit worldedit-bukkit - 7.2.18 + 7.2.19 provided From 3d30c51c2026e83347640cf7bb13f9aec03755b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:37:45 +0100 Subject: [PATCH 17/22] [CI skip] Update dependency org.apache.maven.plugins:maven-shade-plugin to v3.5.2 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4d0dbf7571..9dbf2e4f5d 100644 --- a/pom.xml +++ b/pom.xml @@ -191,7 +191,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.1 + 3.5.2 From 53b365db5025bebf114b4870ebe116b799166347 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:37:54 +0100 Subject: [PATCH 18/22] [CI skip] Update dependency com.sk89q.worldedit:worldedit-core to v7.2.19 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9dbf2e4f5d..ed2a62d7eb 100644 --- a/pom.xml +++ b/pom.xml @@ -425,7 +425,7 @@ com.sk89q.worldedit worldedit-core - 7.2.18 + 7.2.19 provided From f7ba08d9f7bf947023e1ecb0c2522d6fbb289e92 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:02:40 +0100 Subject: [PATCH 19/22] [CI skip] Update GitHub Artifact Actions to v4 (major) --- .github/workflows/e2e-testing.yml | 2 +- .github/workflows/pull-request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-testing.yml b/.github/workflows/e2e-testing.yml index 0f7cc57413..351249cbcb 100644 --- a/.github/workflows/e2e-testing.yml +++ b/.github/workflows/e2e-testing.yml @@ -62,7 +62,7 @@ jobs: "https://api.papermc.io/v2/projects/paper/versions/$VERSION/builds/$BUILD/downloads/$JAR_FILE" - name: Download Slimefun - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ${{ inputs.artifact-name }} path: plugins/ diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ad2a33e080..904ebb8121 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -50,7 +50,7 @@ jobs: run: mvn package - name: Upload the artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: slimefun-${{ github.event.number }}-${{ env.SHORT_COMMIT_HASH }} path: 'target/Slimefun v${{ env.JAR_VERSION }}.jar' From 98786cbbae8ec3e894ceb2bb923d491bb452ad59 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:03:01 +0100 Subject: [PATCH 20/22] [CI skip] Update hmarr/auto-approve-action action to v4 --- .github/workflows/auto-approve.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml index 4d44968a97..8f3836fad9 100644 --- a/.github/workflows/auto-approve.yml +++ b/.github/workflows/auto-approve.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Approve via actions - uses: hmarr/auto-approve-action@v3.2.1 + uses: hmarr/auto-approve-action@v4.0.0 if: github.actor == 'TheBusyBot' || github.actor == 'renovate[bot]' with: github-token: "${{ secrets.GITHUB_TOKEN }}" From 20486fb959df0d3c7844bda7d35c930bc05b6da7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:03:24 +0100 Subject: [PATCH 21/22] [CI skip] Update dependency com.gmail.nossr50.mcMMO:mcMMO to v2.1.230 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ed2a62d7eb..ec30e20254 100644 --- a/pom.xml +++ b/pom.xml @@ -453,7 +453,7 @@ com.gmail.nossr50.mcMMO mcMMO - 2.1.229 + 2.1.230 provided From 3f93bb043f359c94b6e46f163f1151ed6ebc0ef4 Mon Sep 17 00:00:00 2001 From: Miku <26039249+xMikux@users.noreply.github.com> Date: Sun, 25 Feb 2024 10:14:26 +0800 Subject: [PATCH 22/22] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1334b3d8cd..ff36d7c474 100644 --- a/README.md +++ b/README.md @@ -145,11 +145,11 @@ Slimefun4 使用[bStats](https://bstats.org/plugin/bukkit/Slimefun/4574)來收 --- -Slimefun also uses its own analytics system to collect anonymous information about the performance of this plugin.
-This is solely for statistical purposes, as we are interested in how it's performing for all servers.
-All available data is anonymous and aggregated, at no point can we see individual server information.
+Slimefun 同時使用自己的分析系統以匿名方式來收集有關此插件的效能資訊。
+這僅用於統計目的,因為我們對所有伺服器的效能運作有興趣。
+所有資料均為匿名,我們決不會查看到個別伺服器的資訊。
-You can also disable this behaviour under `/plugins/Slimefun/config.yml`.
+你可以在 `/plugins/Slimefun/config.yml` 下停用。