diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java
new file mode 100644
index 00000000..fa82dc50
--- /dev/null
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java
@@ -0,0 +1,60 @@
+package net.buildtheearth.buildteamtools.modules.common.metrics;
+
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Counts feature invocations since the last metrics submission.
+ *
+ * {@link #record(UsageFeature)} counts a feature with no further breakdown.
+ * {@link #record(UsageDetail)} counts that part and its parent feature, so the summary charts
+ * stay aligned with the drilldown pie.
+ */
+public final class FeatureUsage {
+
+ private static final EnumMap COUNTS = new EnumMap<>(UsageFeature.class);
+ private static final EnumMap DETAILS = new EnumMap<>(UsageDetail.class);
+
+ static {
+ for (UsageFeature feature : UsageFeature.values())
+ COUNTS.put(feature, new AtomicInteger());
+ for (UsageDetail detail : UsageDetail.values())
+ DETAILS.put(detail, new AtomicInteger());
+ }
+
+ private FeatureUsage() {
+ }
+
+ public static void record(UsageFeature feature) {
+ COUNTS.get(feature).incrementAndGet();
+ }
+
+ public static void record(UsageDetail detail) {
+ record(detail.feature());
+ DETAILS.get(detail).incrementAndGet();
+ }
+
+ /**
+ * Returns every feature, including those with zero uses, then resets the feature counters.
+ */
+ static Map drain() {
+ Map snapshot = new LinkedHashMap<>();
+ for (UsageFeature feature : UsageFeature.values())
+ snapshot.put(feature.chartId(), COUNTS.get(feature).getAndSet(0));
+ return snapshot;
+ }
+
+ /**
+ * Returns every part of the features that have a breakdown, including zeros, then resets those counters.
+ */
+ static Map> drainDetails() {
+ Map> snapshot = new LinkedHashMap<>();
+ for (UsageDetail detail : UsageDetail.values()) {
+ snapshot.computeIfAbsent(detail.feature().chartId(), ignored -> new LinkedHashMap<>())
+ .put(detail.chartId(), DETAILS.get(detail).getAndSet(0));
+ }
+ return snapshot;
+ }
+}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java
index 73ecee20..f8ec70e0 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java
@@ -1,13 +1,113 @@
package net.buildtheearth.buildteamtools.modules.common.metrics;
+import net.buildtheearth.buildteamtools.modules.Module;
+import net.buildtheearth.buildteamtools.modules.ModuleHandler;
+import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule;
+import net.buildtheearth.buildteamtools.modules.navigation.components.bluemap.BluemapComponent;
import org.bstats.bukkit.Metrics;
+import org.bstats.charts.AdvancedPie;
+import org.bstats.charts.DrilldownPie;
+import org.bstats.charts.SimpleBarChart;
import org.bukkit.plugin.java.JavaPlugin;
-public class MetricsManager {
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * bStats metrics for BuildTeamTools.
+ *
+ * {@code feature_uses} is a bar chart that lists every tracked feature. A zero means it was not used
+ * in that submission. It shares one snapshot with {@code feature_details} so a submission does not
+ * reset the counters twice.
+ *
+ * BlueMap has no player action in this plugin. It is reported as 1 while the integration is enabled.
+ *
+ * {@code feature_details} is a drilldown pie. Clicking navigator, warps, address, or kml opens the
+ * part that was used. It shares the snapshot with the summary charts.
+ */
+public final class MetricsManager {
private static final int BSTATS_PLUGIN_ID = 29076;
+ /** Long enough for every chart in one submission to share a snapshot, shorter than the 30 minute submit interval. */
+ private static final long SNAPSHOT_REUSE_NANOS = TimeUnit.MINUTES.toNanos(2);
+
+ private static Metrics metrics;
+ private static PublishedSnapshot published;
+
+ private MetricsManager() {
+ }
public static void init(JavaPlugin plugin) {
- new Metrics(plugin, BSTATS_PLUGIN_ID);
+ if (metrics != null)
+ metrics.shutdown();
+
+ Metrics created = new Metrics(plugin, BSTATS_PLUGIN_ID);
+ created.addCustomChart(new SimpleBarChart("feature_uses", MetricsManager::featureUseCounts));
+ created.addCustomChart(new DrilldownPie("feature_details", MetricsManager::featureDetails));
+ created.addCustomChart(new AdvancedPie("enabled_modules", MetricsManager::enabledModules));
+ metrics = created;
+ }
+
+ static Map featureUseCounts() {
+ return featureUseCounts(System.nanoTime());
+ }
+
+ static Map featureUseCounts(long nowNanos) {
+ return new LinkedHashMap<>(publish(nowNanos).counts());
+ }
+
+ static Map> featureDetails() {
+ return featureDetails(System.nanoTime());
+ }
+
+ static Map> featureDetails(long nowNanos) {
+ Map> copy = new LinkedHashMap<>();
+ publish(nowNanos).details().forEach((feature, parts) -> copy.put(feature, new LinkedHashMap<>(parts)));
+ return copy;
+ }
+
+ private static PublishedSnapshot publish(long nowNanos) {
+ synchronized (MetricsManager.class) {
+ if (published != null && nowNanos >= published.atNanos() && nowNanos - published.atNanos() < SNAPSHOT_REUSE_NANOS)
+ return published;
+
+ Map counts = FeatureUsage.drain();
+ counts.put("bluemap", blueMapInUse());
+ published = new PublishedSnapshot(counts, FeatureUsage.drainDetails(), nowNanos);
+ return published;
+ }
+ }
+
+ static Map enabledModules() {
+ Map enabled = new LinkedHashMap<>();
+ for (Module module : ModuleHandler.getInstance().getModules())
+ if (module.isEnabled())
+ enabled.put(module.getModuleName(), 1);
+ return enabled;
+ }
+
+ private static int blueMapInUse() {
+ NavigationModule navigation = NavigationModule.getInstance();
+ if (navigation == null)
+ return 0;
+
+ BluemapComponent bluemap = navigation.getBluemapComponent();
+ return bluemap != null && bluemap.isEnabled() ? 1 : 0;
+ }
+
+ static void resetForTests() {
+ synchronized (MetricsManager.class) {
+ published = null;
+ }
+ FeatureUsage.drain();
+ FeatureUsage.drainDetails();
+ }
+
+ private record PublishedSnapshot(
+ Map counts,
+ Map> details,
+ long atNanos
+ ) {
}
}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageDetail.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageDetail.java
new file mode 100644
index 00000000..06b7baa3
--- /dev/null
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageDetail.java
@@ -0,0 +1,48 @@
+package net.buildtheearth.buildteamtools.modules.common.metrics;
+
+/**
+ * A part of a {@link UsageFeature} shown when that feature is opened on the bStats drilldown pie.
+ */
+public enum UsageDetail {
+ NAVIGATOR_MENU(UsageFeature.NAVIGATOR, "menu"),
+ NAVIGATOR_ITEM(UsageFeature.NAVIGATOR, "item"),
+ NAVIGATOR_EXPLORE(UsageFeature.NAVIGATOR, "explore"),
+ NAVIGATOR_COUNTRIES(UsageFeature.NAVIGATOR, "countries"),
+ NAVIGATOR_STATES(UsageFeature.NAVIGATOR, "states"),
+ NAVIGATOR_CITIES(UsageFeature.NAVIGATOR, "cities"),
+ NAVIGATOR_BUILD(UsageFeature.NAVIGATOR, "build"),
+ NAVIGATOR_PLOTSYSTEM(UsageFeature.NAVIGATOR, "plotsystem"),
+ NAVIGATOR_TUTORIALS(UsageFeature.NAVIGATOR, "tutorials"),
+ NAVIGATOR_TOGGLE(UsageFeature.NAVIGATOR, "toggle"),
+ NAVIGATOR_BUILDTEAM(UsageFeature.NAVIGATOR, "buildteam"),
+
+ WARPS_MENU(UsageFeature.WARPS, "menu"),
+ WARPS_TELEPORT(UsageFeature.WARPS, "teleport"),
+ WARPS_CREATE(UsageFeature.WARPS, "create"),
+ WARPS_MIGRATE(UsageFeature.WARPS, "migrate"),
+
+ ADDRESS_GET(UsageFeature.ADDRESS, "get"),
+ ADDRESS_TELEPORT(UsageFeature.ADDRESS, "teleport"),
+
+ KML_GEOPOINTS(UsageFeature.KML, "geopoints"),
+ KML_GEOPATH(UsageFeature.KML, "geopath"),
+ KML_GEORING(UsageFeature.KML, "georing"),
+ KML_GEOSURFACE(UsageFeature.KML, "geosurface"),
+ KML_UNDO(UsageFeature.KML, "undo");
+
+ private final UsageFeature feature;
+ private final String chartId;
+
+ UsageDetail(UsageFeature feature, String chartId) {
+ this.feature = feature;
+ this.chartId = chartId;
+ }
+
+ public UsageFeature feature() {
+ return feature;
+ }
+
+ public String chartId() {
+ return chartId;
+ }
+}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageFeature.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageFeature.java
new file mode 100644
index 00000000..27c454b4
--- /dev/null
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageFeature.java
@@ -0,0 +1,33 @@
+package net.buildtheearth.buildteamtools.modules.common.metrics;
+
+/**
+ * Features whose invocations are reported to bStats.
+ *
+ * Plot system and the sign text generator have no live player entry point, so they are not listed here.
+ * Whether those modules stay enabled is covered by the enabled-modules chart.
+ */
+public enum UsageFeature {
+ HOUSE("house"),
+ ROAD("road"),
+ RAIL("rail"),
+ TREE("tree"),
+ FIELD("field"),
+ KML("kml"),
+ NAVIGATOR("navigator"),
+ WARPS("warps"),
+ TPLL("tpll"),
+ ADDRESS("address"),
+ MAP("map"),
+ STATS("stats"),
+ BLOCK_PALETTE("block_palette");
+
+ private final String chartId;
+
+ UsageFeature(String chartId) {
+ this.chartId = chartId;
+ }
+
+ public String chartId() {
+ return chartId;
+ }
+}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/field/Field.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/field/Field.java
index 69df9d05..b652c007 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/field/Field.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/field/Field.java
@@ -25,6 +25,7 @@ public void generate(Player p) {
if (!checkForPlayer(p))
return;
+ recordUse();
new FieldScripts(p, this);
}
}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/house/House.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/house/House.java
index c804522e..bcc0415a 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/house/House.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/house/House.java
@@ -33,6 +33,7 @@ public void generate(Player p) {
if (!checkForPlayer(p))
return;
+ recordUse();
new HouseScripts(p, this);
}
}
\ No newline at end of file
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java
index 26c16eb6..10d6c2d6 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java
@@ -6,6 +6,9 @@
import net.buildtheearth.buildteamtools.BuildTeamTools;
import net.buildtheearth.buildteamtools.modules.Module;
import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature;
import net.buildtheearth.buildteamtools.modules.navigation.NavUtils;
import net.buildtheearth.buildteamtools.utils.BlockLocation;
import net.buildtheearth.buildteamtools.utils.LineRasterization;
@@ -268,6 +271,7 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) {
return false;
}
+ recordKml(blockCreationCommand);
String kml_content = String.join(" ", args);
Material matchedBlockMaterial = Material.matchMaterial(blocktypeString);
@@ -498,6 +502,7 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) {
* @return true if the CommandBlock was created successfully
*/
public boolean createPasteUI(Player player, String alias, String @NonNull [] args) {
+ recordKml(alias);
//The command either creates a command-block at the player location
//arguments are an
// optional blocktype
@@ -580,6 +585,7 @@ public boolean undoCommand(Player player) {
return false;
}
+ FeatureUsage.record(UsageDetail.KML_UNDO);
ChangeTransaction transaction = playerHistory.pop();
transaction.undo();
player.sendMessage(String.format("undo successful. Restored %d blocks.", transaction.size()));
@@ -673,6 +679,20 @@ public boolean undoCommand(Player player) {
return mcLines;
}
+ private static void recordKml(@NonNull String command) {
+ UsageDetail detail = switch (command) {
+ case GEOPOINTS_COMMAND -> UsageDetail.KML_GEOPOINTS;
+ case GEOPATH_COMMAND -> UsageDetail.KML_GEOPATH;
+ case GEORING_COMMAND -> UsageDetail.KML_GEORING;
+ case GEOSURFACE_COMMAND -> UsageDetail.KML_GEOSURFACE;
+ default -> null;
+ };
+ if (detail == null)
+ FeatureUsage.record(UsageFeature.KML);
+ else
+ FeatureUsage.record(detail);
+ }
+
/**
* Maps command aliases to their corresponding block creation modes.
*
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java
index 02e11105..22f948dd 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java
@@ -76,6 +76,7 @@ public void generate(Player player) {
return;
}
+ recordUse();
new RailScripts(player, this, () -> preparingPlayers.remove(player.getUniqueId()));
}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/road/Road.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/road/Road.java
index dc133d3f..894c35c1 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/road/Road.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/road/Road.java
@@ -22,6 +22,7 @@ public void generate(Player p) {
if (!GeneratorModule.getInstance().getRoad().checkForPlayer(p))
return;
+ recordUse();
new RoadScripts(p, this);
}
}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/tree/Tree.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/tree/Tree.java
index 6c5e747f..72a954bf 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/tree/Tree.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/tree/Tree.java
@@ -52,6 +52,7 @@ public void generate(Player p) {
if (!checkForPlayer(p))
return;
+ recordUse();
TreeScripts.treescript_v_1_0(p, this);
}
}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java
index ff0d3639..00d459bf 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java
@@ -5,6 +5,8 @@
import com.alpsbte.alpslib.utils.WikiDocumented;
import lombok.Getter;
import net.buildtheearth.buildteamtools.modules.ModuleComponent;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature;
import net.buildtheearth.buildteamtools.modules.generator.components.field.FieldSettings;
import net.buildtheearth.buildteamtools.modules.generator.components.house.HouseSettings;
import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailSettings;
@@ -39,6 +41,19 @@ protected GeneratorComponent(@NonNull GeneratorType type) {
public abstract void generate(Player p);
+ /**
+ * Records a generation that got past the feature's own checks.
+ */
+ protected void recordUse() {
+ FeatureUsage.record(switch (generatorType) {
+ case HOUSE -> UsageFeature.HOUSE;
+ case ROAD -> UsageFeature.ROAD;
+ case RAIL -> UsageFeature.RAIL;
+ case TREE -> UsageFeature.TREE;
+ case FIELD -> UsageFeature.FIELD;
+ });
+ }
+
public void analyzeCommand(Player p, String[] args) {
if (isHelpCommand(args)) {
sendHelp(p);
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/map/commands/MapCreateCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/map/commands/MapCreateCommand.java
index 388cc364..a821a3d7 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/map/commands/MapCreateCommand.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/map/commands/MapCreateCommand.java
@@ -21,6 +21,8 @@
import net.buildtheearth.buildteamtools.BuildTeamTools;
import net.buildtheearth.buildteamtools.modules.Module;
import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature;
import net.buildtheearth.buildteamtools.modules.map.MapModule;
import net.buildtheearth.buildteamtools.modules.network.model.Permissions;
import net.buildtheearth.buildteamtools.utils.io.MapConfig;
@@ -102,6 +104,7 @@ private void create(Player player, String claimName) {
request.name(claimName);
}
+ FeatureUsage.record(UsageFeature.MAP);
player.sendMessage(ChatHelper.getStandardComponent(false, "Creating map claim from your selection..."));
Bukkit.getScheduler().runTaskAsynchronously(BuildTeamTools.getInstance(),
() -> submitClaim(player, request));
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java
index 783a9be8..d1d9dc26 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java
@@ -2,6 +2,8 @@
import net.buildtheearth.buildteamtools.BuildTeamTools;
import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature;
import net.buildtheearth.buildteamtools.modules.Module;
import net.buildtheearth.buildteamtools.modules.network.model.Permissions;
import net.kyori.adventure.text.Component;
@@ -48,8 +50,10 @@ public void register(Module owner) {
"Open the block palette and reset its filters.", builder ->
builder.permission(Permissions.BLOCK_PALETTE_USE), context -> {
CommandSender sender = context.sender().getSender();
- if (sender instanceof Player player)
+ if (sender instanceof Player player) {
+ FeatureUsage.record(UsageFeature.BLOCK_PALETTE);
blockPalletManager.get().setPlayerFiltersAndOpen(player);
+ }
else
sender.sendMessage("This command can only be used by players.");
});
@@ -76,6 +80,7 @@ public void register(Module owner) {
Set newFilters = new HashSet<>(List.of(filterInput.split("\\s+")));
manager.updatePlayerFilters(player, newFilters);
}
+ FeatureUsage.record(UsageFeature.BLOCK_PALETTE);
new ChoosePaletteMenu(manager, player, plugin).open();
});
}
@@ -90,6 +95,7 @@ private void execute(@NonNull CommandSender sender, String @NonNull [] args) {
BlockPaletteManager manager = blockPalletManager.get();
// /bp ⇒ open block menu with remembered filters (do NOT reset to "color")
if (args.length == 0) {
+ FeatureUsage.record(UsageFeature.BLOCK_PALETTE);
manager.openBlockMenu(player);
return;
}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java
index c2861c3d..aedee053 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java
@@ -5,8 +5,10 @@
import net.buildtheearth.buildteamtools.BuildTeamTools;
import net.buildtheearth.buildteamtools.modules.Module;
import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager;
-import net.buildtheearth.buildteamtools.modules.network.api.NominatimAPI;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail;
import net.buildtheearth.buildteamtools.modules.network.api.PhotonAPI;
+import net.buildtheearth.buildteamtools.modules.network.api.NominatimAPI;
import net.buildtheearth.buildteamtools.modules.network.model.Permissions;
import net.buildtheearth.model.GeographicalCoordinate;
import net.kyori.adventure.text.Component;
@@ -86,6 +88,7 @@ private void handleGetCommand(@NonNull Player player, boolean international) {
return;
}
+ FeatureUsage.record(UsageDetail.ADDRESS_GET);
player.sendMessage("Getting closest address...");
final GeographicalCoordinate geoCoord;
@@ -173,6 +176,7 @@ private void handleTeleportCommand(@NonNull Player player, String address) {
return;
}
+ FeatureUsage.record(UsageDetail.ADDRESS_TELEPORT);
String lang = player.locale().getLanguage();
PhotonAPI.getCoordinatesFromAddressAsync(address, lang)
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java
index 6a5f36ed..48a8b610 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java
@@ -4,6 +4,8 @@
import net.buildtheearth.buildteamtools.BuildTeamTools;
import net.buildtheearth.buildteamtools.modules.Module;
import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail;
import net.buildtheearth.buildteamtools.modules.navigation.NavUtils;
import net.buildtheearth.buildteamtools.modules.network.NetworkModule;
import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam;
@@ -51,6 +53,7 @@ protected void btCommand(@NonNull CommandSender sender, String buildTeamName, St
}
public void execute(Player player, BuildTeam team) {
+ FeatureUsage.record(UsageDetail.NAVIGATOR_BUILDTEAM);
NavUtils.switchToTeam(team, player);
}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java
index 9d9517ad..2f468911 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java
@@ -5,6 +5,8 @@
import net.buildtheearth.buildteamtools.BuildTeamTools;
import net.buildtheearth.buildteamtools.modules.Module;
import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail;
import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule;
import net.buildtheearth.buildteamtools.modules.navigation.menu.CitySelectorMenu;
import net.buildtheearth.buildteamtools.modules.navigation.menu.CountrySelectorMenu;
@@ -40,7 +42,8 @@ public void register(Module owner) {
var commandManager = BuildTeamTools.getInstance().getCommandManager();
commandManager.registerNoArguments(owner, "navigator", Permissions.NAVIGATOR_USE, this::openNavigator);
registerSubcommand(owner, "explore", "Open the continent selection menu.",
- context -> openPlayer(context.sender().getSender(), player -> new ExploreMenu(player, true)));
+ context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_EXPLORE,
+ player -> new ExploreMenu(player, true)));
registerSubcommand(owner, "countries", "Open the country selection menu for a continent.",
builder -> builder
.permission(Permissions.NAVIGATOR_USE)
@@ -56,10 +59,11 @@ public void register(Module owner) {
String.join(", ", continentNames())));
return;
}
- openPlayer(sender, player -> new CountrySelectorMenu(player, continent, true));
+ openPlayer(sender, UsageDetail.NAVIGATOR_COUNTRIES,
+ player -> new CountrySelectorMenu(player, continent, true));
});
registerSubcommand(owner, "states", "Open the USA state selection menu.",
- context -> openPlayer(context.sender().getSender(),
+ context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_STATES,
player -> new StateSelectorMenu(player, Continent.NORTH_AMERICA, true)));
registerSubcommand(owner, "cities", "Open the city selection menu for a supported state.",
builder -> builder
@@ -80,25 +84,28 @@ public void register(Module owner) {
String.join(", ", cityStateNames())));
return;
}
- openPlayer(sender, player -> new CitySelectorMenu(
+ openPlayer(sender, UsageDetail.NAVIGATOR_CITIES, player -> new CitySelectorMenu(
state, StateSelectorMenu.getCityTeamsForState(state), player, true));
});
// The module constructor registers commands before its own config field is set
NavigationConfig.MainMenuItems items = BuildTeamTools.getInstance().getNavigationConfig().mainMenuItems();
if (items.buildItem().enabled()) {
registerSubcommand(owner, "build", "Run the configured Build menu action.",
- context -> openPlayer(context.sender().getSender(), MainMenu::openBuild));
+ context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_BUILD,
+ MainMenu::openBuild));
}
if (items.plotsystemItem().enabled()) {
registerSubcommand(owner, "plotsystem", "Run the configured Plot System menu action.",
- context -> openPlayer(context.sender().getSender(), MainMenu::openPlotSystem));
+ context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_PLOTSYSTEM,
+ MainMenu::openPlotSystem));
}
if (items.tutorialItem().enabled()) {
registerSubcommand(owner, "tutorials", "Open the tutorial menu or run its configured action.",
- context -> openPlayer(context.sender().getSender(), MainMenu::openTutorials));
+ context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_TUTORIALS,
+ MainMenu::openTutorials));
}
registerSubcommand(owner, "toggle", "Toggle the navigator hotbar item.",
- context -> openPlayer(context.sender().getSender(),
+ context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_TOGGLE,
NavigationModule.getInstance().getNavigatorComponent()::toggle));
}
@@ -126,14 +133,15 @@ private void registerSubcommand(
}
private void openNavigator(@NonNull CommandSender sender) {
- openPlayer(sender, MainMenu::new);
+ openPlayer(sender, UsageDetail.NAVIGATOR_MENU, MainMenu::new);
}
- private void openPlayer(@NonNull CommandSender sender, Consumer action) {
+ private void openPlayer(@NonNull CommandSender sender, UsageDetail detail, Consumer action) {
if (!(sender instanceof Player player)) {
sendPlayerOnlyMessage(sender);
return;
}
+ FeatureUsage.record(detail);
action.accept(player);
}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java
index f895305f..0484b09b 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java
@@ -1,5 +1,7 @@
package net.buildtheearth.buildteamtools.modules.navigation.components.navigator.listeners;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail;
import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule;
import net.buildtheearth.buildteamtools.modules.navigation.menu.MainMenu;
import org.bukkit.entity.Player;
@@ -22,6 +24,7 @@ public void interactEvent(@NonNull PlayerInteractEvent event) {
if (NavigationModule.getInstance().getNavigatorComponent().isItem(event.getItem())) {
event.setCancelled(true);
+ FeatureUsage.record(UsageDetail.NAVIGATOR_ITEM);
new MainMenu(event.getPlayer());
}
}
@@ -45,6 +48,7 @@ public void onClick(@NonNull InventoryClickEvent event) {
}
// Opens the navigator.
+ FeatureUsage.record(UsageDetail.NAVIGATOR_ITEM);
new MainMenu(player);
}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/tpll/TpllComponent.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/tpll/TpllComponent.java
index e41acc40..529de31b 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/tpll/TpllComponent.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/tpll/TpllComponent.java
@@ -6,6 +6,8 @@
import com.google.common.io.ByteStreams;
import net.buildtheearth.buildteamtools.BuildTeamTools;
import net.buildtheearth.buildteamtools.modules.ModuleComponent;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature;
import net.buildtheearth.buildteamtools.modules.navigation.NavUtils;
import net.buildtheearth.buildteamtools.modules.network.NetworkModule;
import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam;
@@ -79,6 +81,7 @@ public void processQueueForPlayer(@NonNull Player player) {
return;
}
+ FeatureUsage.record(UsageFeature.TPLL);
player.teleport(tpllTarget);
ChatHelper.logDebug("Teleported the player to the tpll target.");
tpllQueue.remove(player.getUniqueId());
@@ -93,6 +96,7 @@ public void processQueueForPlayer(@NonNull Player player) {
* @param targetServerName The server to send the player to.
*/
public void tpllPlayer(@NotNull Player player, @NotNull GeographicalCoordinate coordinate, String targetServerName) {
+ FeatureUsage.record(UsageFeature.TPLL);
ChatHelper.logDebug("Starting universal tpll teleportation for %s to %s.", player.getDisplayName(), targetServerName);
// Send a plugin message to the target server which adds the tpll to the queue
ByteArrayDataOutput out = ByteStreams.newDataOutput();
@@ -108,6 +112,7 @@ public void tpllPlayer(@NotNull Player player, @NotNull GeographicalCoordinate c
}
public void tpllPlayerTransfer(@NotNull Player player, @NotNull GeographicalCoordinate coordinate, String ip) {
+ FeatureUsage.record(UsageFeature.TPLL);
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeDouble(coordinate.latitude());
out.writeDouble(coordinate.longitude());
@@ -120,6 +125,7 @@ public void processCookie(@NotNull Player player, byte[] cookie) {
double targetLatitude = in.readDouble();
double targetLongitude = in.readDouble();
ChatHelper.logDebug("Processing cookie for tpll event: lat: %s lon: %s", targetLatitude, targetLongitude);
+ FeatureUsage.record(UsageFeature.TPLL);
Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), // Needs to be delayed if not exception will be trown and nothing happens
() -> player.performCommand("tpll " + targetLatitude + " " + targetLongitude));
}
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java
index 98dedcc9..17d2647f 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java
@@ -8,6 +8,8 @@
import net.buildtheearth.Projection;
import net.buildtheearth.buildteamtools.BuildTeamTools;
import net.buildtheearth.buildteamtools.modules.ModuleComponent;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail;
import net.buildtheearth.buildteamtools.modules.navigation.NavUtils;
import net.buildtheearth.buildteamtools.modules.navigation.components.warps.menu.WarpEditMenu;
import net.buildtheearth.buildteamtools.modules.navigation.components.warps.menu.WarpGroupEditMenu;
@@ -118,6 +120,7 @@ public void warpPlayer(Player player, @NotNull Warp warp) {
loc.setY(warp.getY());
+ FeatureUsage.record(UsageDetail.WARPS_TELEPORT);
player.teleportAsync(loc);
ChatHelper.sendSuccessfulMessage(player, "Successfully warped you to %s.", warp.getName());
return;
@@ -128,6 +131,7 @@ public void warpPlayer(Player player, @NotNull Warp warp) {
var type = NavUtils.determineSwitchPossibilityOrMsgPlayerIfNone(player, warp.getWarpGroup().getBuildTeam());
if (type == NavUtils.NavSwitchType.NETWORK) {
+ FeatureUsage.record(UsageDetail.WARPS_TELEPORT);
// Send a plugin message to that server which adds the warp to the queue
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("UniversalWarps");
@@ -138,6 +142,7 @@ public void warpPlayer(Player player, @NotNull Warp warp) {
// Switch the player to the target server
NetworkModule.getInstance().switchServer(player, warp.getWarpGroup().getBuildTeam().getServerName());
} else if (type == NavUtils.NavSwitchType.TRANSFER) {
+ FeatureUsage.record(UsageDetail.WARPS_TELEPORT);
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF(String.valueOf(warp.getId()));
player.storeCookie(WARP_COOKIE_KEY, out.toByteArray());
@@ -166,6 +171,7 @@ public static void createWarp(Player creator) {
* @param creator The player that is creating the warp
*/
public static void createWarp(@NonNull Player creator, WarpGroup group) {
+ FeatureUsage.record(UsageDetail.WARPS_CREATE);
// Get the geographic coordinates of the player's location.
Location location = creator.getLocation();
try {
@@ -270,6 +276,7 @@ public static void openWarpMenu(@NotNull Player player) {
public static void openWarpMenu(@NotNull Player player, @NotNull BuildTeam buildTeam, @Nullable AbstractMenu menu) {
if (buildTeam.getWarpGroups() == null) return;
+ FeatureUsage.record(UsageDetail.WARPS_MENU);
int warpGroupCount = buildTeam.getWarpGroups().size();
switch (warpGroupCount) {
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java
index 709dc0dd..732b36c0 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java
@@ -11,6 +11,8 @@
import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam;
import net.buildtheearth.buildteamtools.modules.network.model.Permissions;
import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail;
import net.buildtheearth.buildteamtools.utils.Utils;
import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandSender;
@@ -106,6 +108,7 @@ private void handleMigrateCommand(@NonNull Player player, WarpMigrationSource so
return;
}
+ FeatureUsage.record(UsageDetail.WARPS_MIGRATE);
WarpMigrator migrator = new WarpMigrator(source);
player.sendMessage(ChatHelper.getStandardComponent(true, "Migrating the warps..."));
migrator.migrate(player).whenComplete((result, throwable) ->
diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/stats/StatsModule.java b/src/main/java/net/buildtheearth/buildteamtools/modules/stats/StatsModule.java
index 6b9a79d1..5112ee5d 100644
--- a/src/main/java/net/buildtheearth/buildteamtools/modules/stats/StatsModule.java
+++ b/src/main/java/net/buildtheearth/buildteamtools/modules/stats/StatsModule.java
@@ -5,6 +5,8 @@
import lombok.Getter;
import net.buildtheearth.buildteamtools.BuildTeamTools;
import net.buildtheearth.buildteamtools.modules.Module;
+import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage;
+import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature;
import net.buildtheearth.buildteamtools.modules.network.NetworkModule;
import net.buildtheearth.buildteamtools.modules.stats.listeners.StatsListener;
import net.buildtheearth.buildteamtools.modules.stats.model.StatsPlayer;
@@ -118,7 +120,7 @@ public boolean updateAndSave() {
out.writeUTF(StatsModule.getInstance().getCurrentCache().toJSONString());
p.sendPluginMessage(BuildTeamTools.getInstance(), "btt:buildteam", out.toByteArray());
-
+ FeatureUsage.record(UsageFeature.STATS);
return true;
}
diff --git a/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java b/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java
new file mode 100644
index 00000000..489b119c
--- /dev/null
+++ b/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java
@@ -0,0 +1,89 @@
+package net.buildtheearth.buildteamtools.modules.common.metrics;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class FeatureUsageTest {
+
+ @BeforeEach
+ void resetMetrics() {
+ MetricsManager.resetForTests();
+ }
+
+ @Test
+ void drainReturnsEveryFeatureAndResetsTheCounters() {
+ FeatureUsage.record(UsageFeature.HOUSE);
+ FeatureUsage.record(UsageFeature.HOUSE);
+ FeatureUsage.record(UsageFeature.KML);
+
+ Map first = FeatureUsage.drain();
+ assertEquals(UsageFeature.values().length, first.size());
+ assertEquals(2, first.get("house"));
+ assertEquals(1, first.get("kml"));
+ assertEquals(0, first.get("field"));
+ assertEquals(0, first.get("block_palette"));
+
+ Map second = FeatureUsage.drain();
+ assertEquals(0, second.get("house"));
+ assertEquals(0, second.get("kml"));
+ }
+
+ @Test
+ void bothChartsShareOneSnapshotUntilTheNextSubmission() {
+ FeatureUsage.record(UsageFeature.ROAD);
+
+ Map first = MetricsManager.featureUseCounts(0);
+ assertEquals(1, first.get("road"));
+ assertEquals(0, first.get("house"));
+ assertEquals(0, first.get("bluemap"));
+ assertEquals(UsageFeature.values().length + 1, first.size());
+
+ FeatureUsage.record(UsageFeature.TREE);
+ Map reused = MetricsManager.featureUseCounts(TimeUnit.SECONDS.toNanos(30));
+ assertEquals(1, reused.get("road"));
+ assertEquals(0, reused.get("tree"));
+
+ Map nextSubmission = MetricsManager.featureUseCounts(TimeUnit.MINUTES.toNanos(2));
+ assertEquals(0, nextSubmission.get("road"));
+ assertEquals(1, nextSubmission.get("tree"));
+ assertEquals(0, nextSubmission.get("bluemap"));
+ }
+
+ @Test
+ void drilldownCountsThePartAndItsParent() {
+ FeatureUsage.record(UsageDetail.NAVIGATOR_EXPLORE);
+ FeatureUsage.record(UsageDetail.NAVIGATOR_EXPLORE);
+ FeatureUsage.record(UsageDetail.ADDRESS_GET);
+ FeatureUsage.record(UsageDetail.KML_UNDO);
+
+ Map counts = MetricsManager.featureUseCounts(0);
+ assertEquals(2, counts.get("navigator"));
+ assertEquals(1, counts.get("address"));
+ assertEquals(1, counts.get("kml"));
+ assertEquals(0, counts.get("warps"));
+
+ Map> details = MetricsManager.featureDetails(0);
+ assertEquals(2, details.get("navigator").get("explore"));
+ assertEquals(0, details.get("navigator").get("cities"));
+ assertEquals(0, details.get("navigator").get("buildteam"));
+ assertEquals(1, details.get("address").get("get"));
+ assertEquals(0, details.get("address").get("teleport"));
+ assertEquals(1, details.get("kml").get("undo"));
+ assertEquals(0, details.get("kml").get("geopoints"));
+ assertEquals(0, details.get("warps").get("migrate"));
+ assertEquals(4, details.size());
+
+ FeatureUsage.record(UsageDetail.WARPS_MENU);
+ Map> reused = MetricsManager.featureDetails(TimeUnit.SECONDS.toNanos(30));
+ assertEquals(0, reused.get("warps").get("menu"));
+
+ Map> nextSubmission = MetricsManager.featureDetails(TimeUnit.MINUTES.toNanos(2));
+ assertEquals(1, nextSubmission.get("warps").get("menu"));
+ assertEquals(0, nextSubmission.get("navigator").get("explore"));
+ }
+}