diff --git a/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/misc/BindingExtensions.kt b/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/misc/BindingExtensions.kt
new file mode 100644
index 000000000..5868c908d
--- /dev/null
+++ b/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/misc/BindingExtensions.kt
@@ -0,0 +1,148 @@
+/*
+ * Anvil - AnvilPowered
+ * Copyright (C) 2020-2021
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package org.anvilpowered.anvil.api.misc
+
+import com.google.common.reflect.TypeToken
+import com.google.inject.Key
+import com.google.inject.Provider
+import com.google.inject.TypeLiteral
+import org.anvilpowered.anvil.api.Anvil
+import org.anvilpowered.anvil.api.datastore.DBComponent
+
+@Suppress("unchecked", "UnstableApiUsage")
+interface BindingExtensions {
+
+ /**
+ * Full binding method for a component
+ *
+ * A typical example of usage of this method:
+ *
+ * be.bind(
+ *
+ * new TypeToken>(getClass()) {
+ * },
+ *
+ * new TypeToken>(getClass()) {
+ * },
+ *
+ * new TypeToken, Datastore>>(getClass()) {
+ * },
+ *
+ * new TypeToken>(getClass()) { // final implementation
+ * },
+ *
+ * Names.named("mongodb")
+ *
+ * );
+ */
+ fun , From2 : DBComponent<*, *>, From3 : From1, Target : From1> bind(
+ from1: TypeToken,
+ from2: TypeToken,
+ from3: TypeToken,
+ target: TypeToken,
+ componentAnnotation: Annotation,
+ )
+
+ /**
+ * Binding method for a component
+ *
+ *
+ * A typical example of usage of this method:
+ *
+ * be.bind(
+ *
+ * new TypeToken>(getClass()) {
+ * },
+ *
+ * new TypeToken>(getClass()) {
+ * },
+ *
+ * new TypeToken(getClass()) { // final implementation
+ * },
+ * Names.named("mongodb")
+ *
+ * );
+ */
+ fun , From2 : From1, Target : From1> bind(
+ from1: TypeToken,
+ from2: TypeToken,
+ target: TypeToken,
+ componentAnnotation: Annotation,
+ )
+
+ fun bind(
+ from: TypeToken,
+ target: TypeToken,
+ annotation: Annotation,
+ )
+
+ fun bind(
+ from: TypeToken,
+ target: TypeToken,
+ annotation: Class,
+ )
+
+ fun bind(
+ from: TypeToken,
+ target: TypeToken,
+ )
+
+ /**
+ * Binds the mongodb [DataStoreContext]
+ *
+ * Using this method is the same as invoking:
+ *
+ * bind(new TypeLiteral>() {
+ * }).to(MongoContext.class);
+ */
+ fun withMongoDB()
+
+ /**
+ * Binds the xodus [DataStoreContext]
+ *
+ * Using this method is the same as invoking:
+ *
+ * binder.bind(new TypeLiteral>() {
+ * }).to(XodusContext.class);
+ *
+ */
+ fun withXodus()
+
+ companion object {
+ fun getTypeLiteral(typeToken: TypeToken): TypeLiteral {
+ return TypeLiteral.get(typeToken.type) as TypeLiteral
+ }
+
+ fun getKey(typeToken: TypeToken): Key? {
+ return Key.get(getTypeLiteral(typeToken))
+ }
+
+ fun asInternalProvider(clazz: Class): Provider {
+ return Provider { Anvil.getEnvironment().injector.getInstance(clazz) }
+ }
+
+ fun asInternalProvider(typeLiteral: TypeLiteral): Provider {
+ return Anvil.getEnvironment().injector.getProvider(Key.get(typeLiteral))
+ }
+
+ fun asInternalProvider(typeToken: TypeToken): Provider {
+ return Anvil.getEnvironment().injector.getProvider(getKey(typeToken))
+ }
+ }
+}
diff --git a/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/plugin/PluginInfo.kt b/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/plugin/PluginInfo.kt
index 0bf49e49d..72a8c853b 100644
--- a/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/plugin/PluginInfo.kt
+++ b/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/plugin/PluginInfo.kt
@@ -38,4 +38,6 @@ interface PluginInfo : Named {
val organizationName: String
val buildDate: String
+
+ val metricIds: Map
}
diff --git a/anvil-bungee/build.gradle b/anvil-bungee/build.gradle
index a771bac64..25e2c46a9 100644
--- a/anvil-bungee/build.gradle
+++ b/anvil-bungee/build.gradle
@@ -14,6 +14,7 @@ dependencies {
}
implementation bungee
+ implementation bstats
implementation configurate_hocon
implementation javasisst
implementation(kotlin_reflect + ":" + kotlin_version)
@@ -50,6 +51,7 @@ shadowJar {
include dependency(apache_commons)
include dependency(aopalliance)
include dependency(bson)
+ include dependency(bstats)
include dependency(configurate_core)
include dependency(configurate_hocon)
include dependency(guice)
diff --git a/anvil-bungee/src/main/kotlin/org/anvilpowered/anvil/bungee/metric/BungeeMetricService.kt b/anvil-bungee/src/main/kotlin/org/anvilpowered/anvil/bungee/metric/BungeeMetricService.kt
new file mode 100644
index 000000000..aec035916
--- /dev/null
+++ b/anvil-bungee/src/main/kotlin/org/anvilpowered/anvil/bungee/metric/BungeeMetricService.kt
@@ -0,0 +1,265 @@
+/*
+ * Anvil - AnvilPowered
+ * Copyright (C) 2020-2021
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package org.anvilpowered.anvil.bungee.metric
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonObject
+import com.google.inject.Inject
+import java.io.BufferedReader
+import java.io.BufferedWriter
+import java.io.ByteArrayOutputStream
+import java.io.DataOutputStream
+import java.io.File
+import java.io.FileReader
+import java.io.FileWriter
+import java.io.IOException
+import java.io.InputStreamReader
+import java.lang.reflect.InvocationTargetException
+import java.net.URL
+import java.nio.charset.StandardCharsets
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+import java.util.zip.GZIPOutputStream
+import javax.net.ssl.HttpsURLConnection
+import net.md_5.bungee.api.ProxyServer
+import net.md_5.bungee.api.plugin.Plugin
+import net.md_5.bungee.config.Configuration
+import net.md_5.bungee.config.ConfigurationProvider
+import net.md_5.bungee.config.YamlConfiguration
+import org.anvilpowered.anvil.api.Environment
+import org.anvilpowered.anvil.common.metric.MetricService
+import org.slf4j.Logger
+
+class BungeeMetricService @Inject constructor(
+ private val logger: Logger
+) : MetricService {
+
+ private lateinit var environment: Environment
+ private var serviceId = 0
+ private var enabled = false
+ private var serverUUID: String? = null
+ private var logFailedRequests = false
+ private var logSentData = false
+ private var logResponseStatusText = false
+ private val knownMetricsInstances: MutableList = ArrayList()
+
+ override fun initialize(env: Environment) {
+ val bungeeId: Int? = env.pluginInfo.metricIds["bungeecord"]
+ requireNotNull(bungeeId) { "Could not find a valid Metrics Id for BungeeCord. Please check your PluginInfo" }
+ initialize(env, bungeeId)
+ }
+
+ private fun initialize(environment: Environment, serviceId: Int) {
+ this.environment = environment
+ this.serviceId = serviceId
+ try {
+ loadConfig()
+ } catch (e: IOException) {
+ logger.error("Failed to load bStats config!", e)
+ return
+ }
+
+ if (!enabled) {
+ return
+ }
+
+ val usedMetricsClass = getFirstBStatsClass() ?: return
+ if (usedMetricsClass == javaClass) {
+ linkMetrics(this)
+ startSubmitting()
+ } else {
+ val logMsg = "Failed to link to first metrics class ${usedMetricsClass.name}"
+ try {
+ usedMetricsClass.getMethod("linkMetrics", Any::class.java).invoke(null, this)
+ } catch (e: NoSuchMethodException) {
+ if (logFailedRequests) {
+ logger.error(logMsg, e)
+ }
+ } catch (e: IllegalAccessException) {
+ if (logFailedRequests) {
+ logger.error(logMsg, e)
+ }
+ } catch (e: InvocationTargetException) {
+ if (logFailedRequests) {
+ logger.error(logMsg, e)
+ }
+ }
+ }
+ }
+
+ private fun linkMetrics(metrics: Any) = knownMetricsInstances.add(metrics)
+
+ private fun startSubmitting() {
+ val initialDelay = (1000 * 60 * (3 + Math.random() * 3)).toLong()
+ val secondDelay = (1000 * 60 * (Math.random() * 30)).toLong()
+ val plugin = environment.plugin as Plugin
+ ProxyServer.getInstance().scheduler.schedule(plugin, { submitData() }, initialDelay, TimeUnit.MILLISECONDS)
+ ProxyServer.getInstance().scheduler.schedule(
+ plugin, { submitData() }, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS
+ )
+ }
+
+ private fun getServerData(): JsonObject {
+ val proxyInstance = ProxyServer.getInstance()
+ val data = JsonObject()
+ data.addProperty("serverUUID", serverUUID)
+ data.addProperty("playerAmount", proxyInstance.onlineCount)
+ data.addProperty("managedServers", proxyInstance.servers.size)
+ data.addProperty("onlineMode", if (proxyInstance.config.isOnlineMode) 1 else 0)
+ data.addProperty("bungeecordVersion", proxyInstance.version)
+ data.addProperty("javaVersion", System.getProperty("java.version"))
+ data.addProperty("osName", System.getProperty("os.name"))
+ data.addProperty("osArch", System.getProperty("os.arch"))
+ data.addProperty("osVersion", System.getProperty("os.version"))
+ data.addProperty("coreCount", Runtime.getRuntime().availableProcessors())
+ return data
+ }
+
+ private fun submitData() {
+ val data: JsonObject = getServerData()
+ val pluginData = JsonArray()
+ for (metrics in knownMetricsInstances) {
+ try {
+ val plugin = metrics.javaClass.getMethod("getPluginData").invoke(metrics)
+ if (plugin is JsonObject) {
+ pluginData.add(plugin)
+ }
+ } catch (ignored: Exception) {
+ }
+ }
+ data.add("plugins", pluginData)
+ try {
+ sendData(data)
+ } catch (e: Exception) {
+ if (logFailedRequests) {
+ logger.error("Could not submit plugin stats!", e)
+ }
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun loadConfig() {
+ val bStatsFolder = File("plugins/bStats")
+ check(bStatsFolder.mkdirs()) { "Could not create the config directory for bStats!" }
+ val configFile = File(bStatsFolder, "config.yml")
+ check(configFile.mkdirs()) { "Could not create the config file for bStats!" }
+
+ writeFile(
+ configFile,
+ "#bStats collects some data for plugin authors like how many servers are using their plugins.",
+ "#To honor their work, you should not disable it.",
+ "#This has nearly no effect on the server performance!",
+ "#Check out https://bStats.org/ to learn more :)",
+ "enabled: true",
+ "serverUuid: \"" + UUID.randomUUID() + "\"",
+ "logFailedRequests: false",
+ "logSentData: false",
+ "logResponseStatusText: false"
+ )
+
+ val configuration: Configuration = ConfigurationProvider.getProvider(YamlConfiguration::class.java).load(configFile)
+
+ enabled = configuration.getBoolean("enabled", true)
+ serverUUID = configuration.getString("serverUuid")
+ logFailedRequests = configuration.getBoolean("logFailedRequests", false)
+ logSentData = configuration.getBoolean("logSentData", false)
+ logResponseStatusText = configuration.getBoolean("logResponseStatusText", false)
+ }
+
+ private fun getFirstBStatsClass(): Class<*>? {
+ val bStatsFolder = File("plugins/bStats")
+ bStatsFolder.mkdirs()
+ check(bStatsFolder.mkdirs()) { "Could not create the bStats config folder!" }
+ val tempFile = File(bStatsFolder, "temp.txt")
+ return try {
+ val className = readFile(tempFile)
+ if (className != null) {
+ try {
+ return Class.forName(className)
+ } catch (ignored: ClassNotFoundException) {
+ }
+ }
+ writeFile(tempFile, javaClass.name)
+ javaClass
+ } catch (e: IOException) {
+ if (logFailedRequests) {
+ logger.error("Failed to get first bStats class!", e)
+ }
+ null
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun readFile(file: File): String? {
+ return if (!file.exists()) {
+ null
+ } else {
+ BufferedReader(FileReader(file)).use { it.readLine() }
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun writeFile(file: File, vararg lines: String) {
+ BufferedWriter(FileWriter(file)).use {
+ for (line in lines) {
+ it.write(line)
+ it.newLine()
+ }
+ }
+ }
+
+ private fun sendData(data: JsonObject?) {
+ requireNotNull(data) { "Data cannot be null" }
+ if (logSentData) {
+ logger.info("Sending data to bStats: $data")
+ }
+ val connection: HttpsURLConnection = URL("https://bStats.org/submitData/bungeecord").openConnection() as HttpsURLConnection
+ val compressedData = compress(data.toString())
+
+ connection.requestMethod = "POST"
+ connection.addRequestProperty("Accept", "application/json")
+ connection.addRequestProperty("Connection", "close")
+ connection.addRequestProperty("Content-Encoding", "gzip")
+ connection.addRequestProperty("Content-Length", compressedData!!.size.toString())
+ connection.setRequestProperty("Content-Type", "application/json")
+ connection.setRequestProperty("User-Agent", "MC-Server/1")
+
+ connection.doOutput = true
+ DataOutputStream(connection.outputStream).use { outputStream -> outputStream.write(compressedData) }
+ val builder = StringBuilder()
+ BufferedReader(InputStreamReader(connection.inputStream)).use { bufferedReader ->
+ var line: String?
+ while (bufferedReader.readLine().also { line = it } != null) {
+ builder.append(line)
+ }
+ }
+ if (logResponseStatusText) {
+ logger.info("Sent data to bStats and received response: $builder")
+ }
+ }
+
+ private fun compress(str: String?): ByteArray? {
+ if (str == null) {
+ return null
+ }
+ val outputStream = ByteArrayOutputStream()
+ GZIPOutputStream(outputStream).use { gzip -> gzip.write(str.toByteArray(StandardCharsets.UTF_8)) }
+ return outputStream.toByteArray()
+ }
+}
diff --git a/anvil-common/build.gradle b/anvil-common/build.gradle
index 2d2900cf5..93ec9ff74 100644
--- a/anvil-common/build.gradle
+++ b/anvil-common/build.gradle
@@ -8,6 +8,7 @@ plugins {
dependencies {
api(project(':anvil-api'))
+ api(bstats)
api("net.kyori:adventure-text-serializer-legacy:4.5.0")
api("net.kyori:adventure-text-serializer-plain:4.5.0")
api(configurate_hocon)
diff --git a/anvil-common/src/main/java/org/anvilpowered/anvil/api/EnvironmentBuilderImpl.java b/anvil-common/src/main/java/org/anvilpowered/anvil/api/EnvironmentBuilderImpl.java
index c0c42ccf9..3f7e044fb 100644
--- a/anvil-common/src/main/java/org/anvilpowered/anvil/api/EnvironmentBuilderImpl.java
+++ b/anvil-common/src/main/java/org/anvilpowered/anvil/api/EnvironmentBuilderImpl.java
@@ -21,6 +21,7 @@
import com.google.common.base.Preconditions;
import com.google.common.reflect.TypeToken;
import com.google.inject.AbstractModule;
+import com.google.inject.Binding;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
@@ -32,6 +33,7 @@
import org.anvilpowered.anvil.api.registry.Registry;
import org.anvilpowered.anvil.api.registry.RegistryScope;
import org.anvilpowered.anvil.common.PlatformImpl;
+import org.anvilpowered.anvil.common.metric.MetricService;
import org.anvilpowered.anvil.common.module.PlatformModule;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -132,6 +134,11 @@ protected void configure() {
}
ServiceManagerImpl.environmentManager
.registerEnvironment(environment, environment.getPlugin());
+ Binding metricBinding =
+ Anvil.environment.getInjector().getExistingBinding(Key.get(MetricService.class));
+ if (metricBinding != null) {
+ metricBinding.getProvider().get().initialize(environment);
+ }
for (Map.Entry, Consumer>> entry
: environment.getEarlyServices().entrySet()) {
((Consumer) entry.getValue())
diff --git a/anvil-common/src/main/java/org/anvilpowered/anvil/common/plugin/AnvilPluginInfo.java b/anvil-common/src/main/java/org/anvilpowered/anvil/common/plugin/AnvilPluginInfo.java
index 37ffaf4fe..814f31e17 100644
--- a/anvil-common/src/main/java/org/anvilpowered/anvil/common/plugin/AnvilPluginInfo.java
+++ b/anvil-common/src/main/java/org/anvilpowered/anvil/common/plugin/AnvilPluginInfo.java
@@ -18,11 +18,16 @@
package org.anvilpowered.anvil.common.plugin;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.anvilpowered.anvil.api.plugin.PluginInfo;
+import org.anvilpowered.anvil.api.util.TextService;
-public class AnvilPluginInfo implements PluginInfo {
+import java.util.Map;
+
+public class AnvilPluginInfo implements PluginInfo {
public static final String id = "anvil";
public static final String name = "Anvil";
public static final String version = "$modVersion";
@@ -36,6 +41,7 @@ public class AnvilPluginInfo implements PluginInfo {
.append(Component.text(name, NamedTextColor.AQUA))
.append(Component.text("] ", NamedTextColor.BLUE))
.build();
+ public static final Map metricId = ImmutableMap.of();
@Override
public String getId() {
@@ -81,4 +87,9 @@ public String getBuildDate() {
public Component getPrefix() {
return pluginPrefix;
}
+
+ @Override
+ public Map getMetricIds() {
+ return metricId;
+ }
}
diff --git a/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/metric/MetricService.kt b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/metric/MetricService.kt
new file mode 100644
index 000000000..2c80e8b6d
--- /dev/null
+++ b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/metric/MetricService.kt
@@ -0,0 +1,30 @@
+/*
+ * Anvil - AnvilPowered
+ * Copyright (C) 2020-2021
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package org.anvilpowered.anvil.common.metric
+
+import org.anvilpowered.anvil.api.Environment
+
+interface MetricService {
+
+ /**
+ * Initializes metrics through bStats for the specified [Environment]
+ */
+ fun initialize(env: Environment)
+
+}
diff --git a/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/misc/CommonBindingExtensions.kt b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/misc/CommonBindingExtensions.kt
new file mode 100644
index 000000000..39c1de1f5
--- /dev/null
+++ b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/misc/CommonBindingExtensions.kt
@@ -0,0 +1,97 @@
+/*
+ * Anvil - AnvilPowered
+ * Copyright (C) 2020-2021
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package org.anvilpowered.anvil.common.misc
+
+import com.google.common.reflect.TypeToken
+import com.google.inject.Binder
+import com.google.inject.TypeLiteral
+import dev.morphia.Datastore
+import jetbrains.exodus.entitystore.EntityId
+import jetbrains.exodus.entitystore.PersistentEntityStore
+import org.anvilpowered.anvil.api.datastore.DBComponent
+import org.anvilpowered.anvil.api.datastore.DataStoreContext
+import org.anvilpowered.anvil.api.datastore.MongoContext
+import org.anvilpowered.anvil.api.datastore.XodusContext
+import org.anvilpowered.anvil.api.misc.BindingExtensions
+import org.bson.types.ObjectId
+
+@Suppress("unchecked", "UnstableApiUsage")
+class CommonBindingExtensions(val binder: Binder) : BindingExtensions {
+
+ override fun , From2 : DBComponent<*, *>, From3 : From1, Target : From1> bind(
+ from1: TypeToken,
+ from2: TypeToken,
+ from3: TypeToken,
+ target: TypeToken,
+ componentAnnotation: Annotation,
+ ) {
+ binder.bind(TypeLiteral.get(from2.type) as TypeLiteral)
+ .annotatedWith(componentAnnotation)
+ .to(BindingExtensions.getTypeLiteral(target))
+ binder.bind(TypeLiteral.get(from3.type) as TypeLiteral)
+ .to(BindingExtensions.getTypeLiteral(target))
+ }
+
+ override fun , From2 : From1, Target : From1> bind(
+ from1: TypeToken,
+ from2: TypeToken,
+ target: TypeToken,
+ componentAnnotation: Annotation,
+ ) {
+ bind(from1, from1, from2, target, componentAnnotation)
+ }
+
+ override fun bind(
+ from: TypeToken,
+ target: TypeToken,
+ annotation: Annotation,
+ ) {
+ binder.bind(BindingExtensions.getTypeLiteral(from))
+ .annotatedWith(annotation)
+ .to(BindingExtensions.getTypeLiteral(target))
+ }
+
+ override fun bind(
+ from: TypeToken,
+ target: TypeToken,
+ annotation: Class,
+ ) {
+ binder.bind(BindingExtensions.getTypeLiteral(from))
+ .annotatedWith(annotation)
+ .to(BindingExtensions.getTypeLiteral(target))
+ }
+
+ override fun bind(
+ from: TypeToken,
+ target: TypeToken,
+ ) {
+ binder.bind(BindingExtensions.getTypeLiteral(from))
+ .to(BindingExtensions.getTypeLiteral(target))
+ }
+
+ override fun withMongoDB() {
+ binder.bind(object : TypeLiteral>() {})
+ .to(MongoContext::class.java)
+ }
+
+ override fun withXodus() {
+ binder.bind(object : TypeLiteral>() {})
+ .to(XodusContext::class.java)
+ }
+}
diff --git a/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/plugin/FallbackPluginInfo.kt b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/plugin/FallbackPluginInfo.kt
index 00348462e..144d8113e 100644
--- a/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/plugin/FallbackPluginInfo.kt
+++ b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/plugin/FallbackPluginInfo.kt
@@ -18,6 +18,7 @@
package org.anvilpowered.anvil.common.plugin
+import com.google.common.collect.ImmutableMap
import com.google.inject.Inject
import net.kyori.adventure.text.Component
import org.anvilpowered.anvil.api.Environment
@@ -32,6 +33,7 @@ class FallbackPluginInfo : PluginInfo {
val authors = arrayOf("author")
const val organizationName = "organizationName"
const val buildDate = "last night"
+ val metricIds: Map = ImmutableMap.of("fake", 0)
}
@Inject
@@ -53,4 +55,5 @@ class FallbackPluginInfo : PluginInfo {
override val organizationName: String = Companion.organizationName
override val buildDate: String = Companion.buildDate
override val prefix: Component = pluginPrefix
+ override val metricIds: Map = Companion.metricIds
}
diff --git a/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/metric/SpigotMetricService.kt b/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/metric/SpigotMetricService.kt
new file mode 100644
index 000000000..af0ce8120
--- /dev/null
+++ b/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/metric/SpigotMetricService.kt
@@ -0,0 +1,115 @@
+/*
+ * Anvil - AnvilPowered
+ * Copyright (C) 2020-2021
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package org.anvilpowered.anvil.spigot.metric
+
+import com.google.inject.Inject
+import java.io.File
+import java.io.IOException
+import java.util.UUID
+import org.anvilpowered.anvil.api.Environment
+import org.anvilpowered.anvil.api.util.UserService
+import org.anvilpowered.anvil.common.metric.MetricService
+import org.bstats.MetricsBase
+import org.bstats.json.JsonObjectBuilder
+import org.bukkit.Bukkit
+import org.bukkit.configuration.file.YamlConfiguration
+import org.bukkit.entity.Player
+import org.bukkit.plugin.Plugin
+import org.slf4j.Logger
+
+class SpigotMetricService @Inject constructor(
+ private val logger: Logger,
+ private val userService: UserService
+) : MetricService {
+
+ private lateinit var environment: Environment
+ private var metricsBase: MetricsBase? = null
+
+ override fun initialize(env: Environment) {
+ val serviceId: Int? = env.pluginInfo.metricIds["spigot"]
+ checkNotNull(serviceId) { "Could not find a valid Metrics Id for Spigot. Please check your PluginInfo" }
+ initialize(env, serviceId)
+ }
+
+ private fun initialize(environment: Environment, serviceId: Int) {
+ this.environment = environment
+ val bStatsFolder = File("plugins/bStats")
+ val configFile = File(bStatsFolder, "config.yml")
+ if (!bStatsFolder.exists()) {
+ require(configFile.mkdirs()) { "Could not create the bStats config!" }
+ }
+ val config: YamlConfiguration = YamlConfiguration.loadConfiguration(configFile)
+ if (!config.isSet("serverUuid")) {
+ config.addDefault("enabled", true)
+ config.addDefault("serverUuid", UUID.randomUUID().toString())
+ config.addDefault("logFailedRequests", false)
+ config.addDefault("logSentData", false)
+ config.addDefault("logResponseStatusText", false)
+
+ config.options().header(
+ """
+ bStats collects some data for plugin authors like how many servers are using their plugins.\n
+ To honor their work, you should not disable it.\n
+ This has nearly no effect on the server performance!\n
+ Check out https://bStats.org/ to learn more :)
+ """.trimIndent()
+ ).copyDefaults(true)
+ try {
+ config.save(configFile)
+ } catch (ignored: IOException) {
+ }
+ }
+
+ val enabled: Boolean = config.getBoolean("enabled", true)
+ val serverUUID: String = config.getString("serverUuid")!!
+ val logErrors: Boolean = config.getBoolean("logFailedRequests", false)
+ val logSentData: Boolean = config.getBoolean("logSentData", false)
+ val logResponseStatusText: Boolean = config.getBoolean("logResponseStatusText", false)
+ metricsBase = MetricsBase(
+ "bukkit",
+ serverUUID,
+ serviceId,
+ enabled,
+ { appendPlatformData(it) },
+ { appendServiceData(it) },
+ { Bukkit.getScheduler().runTask(environment.plugin as Plugin, it) },
+ { (environment.plugin as Plugin).isEnabled },
+ { message: String, error: Throwable -> logger.error(message, error) },
+ { logger.info(it) },
+ logErrors,
+ logSentData,
+ logResponseStatusText
+ )
+ }
+
+ private fun appendPlatformData(builder: JsonObjectBuilder) {
+ builder.appendField("playerAmount", getPlayerAmount())
+ builder.appendField("onlineMode", if (Bukkit.getOnlineMode()) 1 else 0)
+ builder.appendField("bukkitVersion", Bukkit.getVersion())
+ builder.appendField("bukkitName", Bukkit.getName())
+ builder.appendField("javaVersion", System.getProperty("java.version"))
+ builder.appendField("osName", System.getProperty("os.name"))
+ builder.appendField("osArch", System.getProperty("os.arch"))
+ builder.appendField("osVersion", System.getProperty("os.version"))
+ builder.appendField("coreCount", Runtime.getRuntime().availableProcessors())
+ }
+
+ private fun appendServiceData(builder: JsonObjectBuilder) = builder.appendField("pluginVersion", environment.pluginInfo.version)
+ private fun getPlayerAmount(): Int = userService.onlinePlayers.size
+}
diff --git a/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/module/ApiSpigotModule.kt b/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/module/ApiSpigotModule.kt
index 4e0eaa3a9..6d6e5c2fe 100644
--- a/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/module/ApiSpigotModule.kt
+++ b/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/module/ApiSpigotModule.kt
@@ -27,6 +27,7 @@ import org.anvilpowered.anvil.api.util.TextService
import org.anvilpowered.anvil.api.util.UserService
import org.anvilpowered.anvil.common.PlatformImpl
import org.anvilpowered.anvil.common.entity.EntityUtils
+import org.anvilpowered.anvil.common.metric.MetricService
import org.anvilpowered.anvil.common.module.JavaUtilLoggingAdapter
import org.anvilpowered.anvil.common.module.PlatformModule
import org.anvilpowered.anvil.common.util.CommonTextService
@@ -34,6 +35,7 @@ import org.anvilpowered.anvil.common.util.SendTextService
import org.anvilpowered.anvil.spigot.command.SpigotCommandExecuteService
import org.anvilpowered.anvil.spigot.command.SpigotSimpleCommandService
import org.anvilpowered.anvil.spigot.entity.SpigotEntityUtils
+import org.anvilpowered.anvil.spigot.metric.SpigotMetricService
import org.anvilpowered.anvil.spigot.server.SpigotLocationService
import org.anvilpowered.anvil.spigot.util.SpigotKickService
import org.anvilpowered.anvil.spigot.util.SpigotPermissionService
@@ -58,6 +60,7 @@ class ApiSpigotModule : PlatformModule(
bind(KickService::class.java).to(SpigotKickService::class.java)
bind(EntityUtils::class.java).to(SpigotEntityUtils::class.java)
bind(LocationService::class.java).to(SpigotLocationService::class.java)
+ bind(MetricService::class.java).to(SpigotMetricService::class.java)
bind(PermissionService::class.java).to(SpigotPermissionService::class.java)
bind(object : TypeLiteral>() {}).to(SpigotSendTextService::class.java)
bind(object : TypeLiteral>() {}).to(object : TypeLiteral>() {})
diff --git a/anvil-sponge/anvil-sponge-7/src/main/kotlin/org/anvilpowered/anvil/sponge7/metric/Sponge7MetricService.kt b/anvil-sponge/anvil-sponge-7/src/main/kotlin/org/anvilpowered/anvil/sponge7/metric/Sponge7MetricService.kt
new file mode 100644
index 000000000..d7b7d1705
--- /dev/null
+++ b/anvil-sponge/anvil-sponge-7/src/main/kotlin/org/anvilpowered/anvil/sponge7/metric/Sponge7MetricService.kt
@@ -0,0 +1,145 @@
+/*
+ * Anvil - AnvilPowered
+ * Copyright (C) 2020-2021
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package org.anvilpowered.anvil.sponge7.metric
+
+import com.google.inject.Inject
+import java.io.File
+import java.io.IOException
+import java.nio.file.Path
+import java.util.UUID
+import ninja.leaping.configurate.commented.CommentedConfigurationNode
+import ninja.leaping.configurate.hocon.HoconConfigurationLoader
+import org.anvilpowered.anvil.api.Environment
+import org.anvilpowered.anvil.common.metric.MetricService
+import org.bstats.MetricsBase
+import org.bstats.json.JsonObjectBuilder
+import org.slf4j.Logger
+import org.spongepowered.api.Platform
+import org.spongepowered.api.Sponge
+import org.spongepowered.api.plugin.PluginContainer
+import org.spongepowered.api.scheduler.Scheduler
+import org.spongepowered.api.scheduler.Task
+
+class Sponge7MetricService @Inject constructor(
+ private val logger: Logger
+) : MetricService {
+
+ private lateinit var plugin: PluginContainer
+ private lateinit var configDir: Path
+ private var serviceId = 0
+ private lateinit var metricsBase: MetricsBase
+ private var serverUUID: String? = null
+ private var logErrors = false
+ private var logSentData = false
+ private var logResponseStatusText = false
+
+ override fun initialize(env: Environment) {
+ val serviceId: Int? = env.pluginInfo.metricIds["sponge"]
+ checkNotNull(serviceId) { "Could not find a valid Metrics Id for Sponge. Please check your PluginInfo" }
+
+ this.serviceId = serviceId
+ this.configDir = Sponge.getConfigManager().getSharedConfig(env.plugin).configPath
+ try {
+ loadConfig()
+ } catch (e: IOException) {
+ logger.warn("Failed to load bStats config!", e)
+ return
+ }
+ this.plugin = env.injector.getInstance(PluginContainer::class.java)
+ metricsBase = MetricsBase(
+ "sponge",
+ serverUUID,
+ serviceId,
+ Sponge.getMetricsConfigManager().getCollectionState(plugin).asBoolean(),
+ { builder: JsonObjectBuilder -> appendPlatformData(builder) },
+ { builder: JsonObjectBuilder -> appendServiceData(builder) },
+ { task: Runnable ->
+ val scheduler: Scheduler = Sponge.getScheduler()
+ val taskBuilder: Task.Builder = scheduler.createTaskBuilder()
+ taskBuilder.execute(task).submit(plugin)
+ },
+ { true },
+ logger::warn,
+ logger::info,
+ logErrors,
+ logSentData,
+ logResponseStatusText
+ )
+ val builder = StringBuilder()
+ builder.append("Plugin ").append(plugin.name).append(" is using bStats Metrics ")
+ if (Sponge.getMetricsConfigManager().getCollectionState(plugin).asBoolean()) {
+ builder.append(" and is allowed to send data.")
+ } else {
+ builder.append(" but currently has data sending disabled.").append(System.lineSeparator())
+ builder.append("To change the enabled/disabled state of any bStats use in a plugin, visit the Sponge config!")
+ }
+ logger.info(builder.toString())
+ }
+
+ private fun loadConfig() {
+ val configPath: File = configDir.resolve("bStats").toFile()
+ if (!configPath.exists()) {
+ if (!configPath.mkdirs()) {
+ logger.error("Could not create the bStats directory!")
+ }
+ }
+
+ val configFile = File(configPath, "config.conf")
+ val configurationLoader: HoconConfigurationLoader =
+ HoconConfigurationLoader.builder().setFile(configFile).build()
+
+ if (!configFile.exists()) {
+ require(configFile.mkdirs()) { "Could not create the bStats config!" }
+ }
+
+ val node: CommentedConfigurationNode = configurationLoader.load()
+ node.getNode("serverUuid").value = UUID.randomUUID().toString()
+ node.getNode("logFailedRequests").value = false
+ node.getNode("logSentData").value = false
+ node.getNode("logResponseStatusText").value = false
+ node.getNode("serverUuid").setComment(
+ "bStats collects some data for plugin authors like how many servers are using their plugins.\n" +
+ "To control whether this is enabled or disabled, see the Sponge configuration file.\n" +
+ "Check out https://bStats.org/ to learn more :)"
+ )
+ node.getNode("configVersion").value = 2
+ configurationLoader.save(node)
+
+ serverUUID = node.getNode("serverUuid").string
+ logErrors = node.getNode("logFailedRequests").getBoolean(false)
+ logSentData = node.getNode("logSentData").getBoolean(false)
+ logResponseStatusText = node.getNode("logResponseStatusText").getBoolean(false)
+ }
+
+ private fun appendPlatformData(builder: JsonObjectBuilder) {
+ builder.appendField("playerAmount", Sponge.getServer().onlinePlayers.size)
+ builder.appendField("onlineMode", if (Sponge.getServer().onlineMode) 1 else 0)
+ builder.appendField("minecraftVersion", Sponge.getGame().platform.minecraftVersion.name)
+ builder.appendField("spongeImplementation", Sponge.getPlatform().getContainer(Platform.Component.IMPLEMENTATION).name)
+ builder.appendField("javaVersion", System.getProperty("java.version"))
+ builder.appendField("osName", System.getProperty("os.name"))
+ builder.appendField("osArch", System.getProperty("os.arch"))
+ builder.appendField("osVersion", System.getProperty("os.version"))
+ builder.appendField("coreCount", Runtime.getRuntime().availableProcessors())
+ }
+
+ private fun appendServiceData(builder: JsonObjectBuilder) {
+ builder.appendField("pluginVersion", plugin.version.orElse("unknown"))
+ }
+}
diff --git a/anvil-sponge/build.gradle b/anvil-sponge/build.gradle
index 5a136b397..fdeffceef 100644
--- a/anvil-sponge/build.gradle
+++ b/anvil-sponge/build.gradle
@@ -43,9 +43,11 @@ shadowJar {
exclude("META-INF/versions/**")
relocate("org.apache.commons", "relocated.apache")
+ relocate("org.bstats", "relocated.org.bstats")
relocate("ninja.leaping", "relocated")
include dependency(apache_commons)
include dependency(bson)
+ include dependency(bstats)
include dependency(configurate_core)
include dependency(configurate_hocon)
include dependency(javasisst)
diff --git a/anvil-velocity/build.gradle b/anvil-velocity/build.gradle
index 9439a75d7..726d48e8e 100644
--- a/anvil-velocity/build.gradle
+++ b/anvil-velocity/build.gradle
@@ -19,6 +19,7 @@ dependencies {
implementation project(':Anvil:anvil-common')
}
+ implementation bstats
implementation javasisst
implementation(kotlin_reflect + ":" + kotlin_version)
implementation(kotlin_stdlib + ":" + kotlin_version)
@@ -45,8 +46,10 @@ shadowJar {
include project(':Anvil:anvil-common')
}
+ relocate("org.bstats", "relocated.org.bstats")
include dependency(apache_commons)
include dependency(bson)
+ include dependency(bstats)
include dependency(javasisst)
include dependency(jedis)
include dependency(kotlin_reflect)
diff --git a/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/metric/VelocityMetricService.kt b/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/metric/VelocityMetricService.kt
new file mode 100644
index 000000000..ce3449317
--- /dev/null
+++ b/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/metric/VelocityMetricService.kt
@@ -0,0 +1,186 @@
+/*
+ * Anvil - AnvilPowered
+ * Copyright (C) 2020-2021
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package org.anvilpowered.anvil.velocity.metric
+
+import com.google.inject.Inject
+import com.velocitypowered.api.proxy.ProxyServer
+import java.io.BufferedReader
+import java.io.BufferedWriter
+import java.io.File
+import java.io.FileReader
+import java.io.FileWriter
+import java.io.IOException
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.Optional
+import java.util.UUID
+import java.util.regex.Pattern
+import java.util.stream.Collectors
+import org.anvilpowered.anvil.api.Environment
+import org.anvilpowered.anvil.api.Platform
+import org.anvilpowered.anvil.api.server.LocationService
+import org.anvilpowered.anvil.common.metric.MetricService
+import org.bstats.MetricsBase
+import org.bstats.json.JsonObjectBuilder
+import org.slf4j.Logger
+
+class VelocityMetricService @Inject constructor(
+ private val logger: Logger,
+ private val platform: Platform,
+ private val proxyServer: ProxyServer,
+ private val locationService: LocationService,
+) : MetricService {
+
+ private lateinit var metricsBase: MetricsBase
+ private var serverUUID: String? = null
+ private var enabled: Boolean = false
+ private var logErrors: Boolean = false
+ private var logSentData: Boolean = false
+ private var logResponseStatusText: Boolean = false
+ private lateinit var dataDirectory: Path
+ private lateinit var environment: Environment
+
+ override fun initialize(env: Environment) {
+ val serviceId: Int? = env.pluginInfo.metricIds["velocity"]
+ requireNotNull(serviceId) { "Could not find a valid Metrics Id for Velocity. Please check your PluginInfo" }
+ initialize(env, Paths.get("plugins/bStats"), serviceId)
+ }
+
+ private fun initialize(environment: Environment, dataPath: Path, serviceId: Int) {
+ this.dataDirectory = dataPath
+ this.environment = environment
+ try {
+ setupConfig(true)
+ } catch (e: IOException) {
+ logger.error("Failed to create bStats config", e)
+ }
+ metricsBase = MetricsBase(
+ platform.name,
+ serverUUID,
+ serviceId,
+ enabled,
+ this::appendPlatformData,
+ this::appendServiceData,
+ { task -> proxyServer.scheduler.buildTask(environment.plugin, task).schedule() },
+ { true },
+ logger::warn,
+ logger::info,
+ logErrors,
+ logSentData,
+ logResponseStatusText
+ )
+ }
+
+ private fun setupConfig(recreateWhenMalformed: Boolean) {
+ val configDir = dataDirectory.parent.resolve("bStats").toFile()
+ if (!configDir.exists()) {
+ require(configDir.mkdirs()) {"Could not create the bStats config!"}
+ }
+ val configFile = File(configDir, "config.txt")
+ if (!configFile.exists()) {
+ writeConfig(configFile)
+ }
+
+ val lines: List = readFile(configFile)
+
+ enabled = getConfigValue("enabled", lines).map { anObject: String -> "true" == anObject }.orElse(true)
+ serverUUID = getConfigValue("server-uuid", lines).orElse(null)
+ logErrors = getConfigValue("log-errors", lines).map { anObject: String -> "true" == anObject }.orElse(false)
+ logSentData = getConfigValue("log-sent-data", lines).map { anObject: String -> "true" == anObject }.orElse(false)
+ logResponseStatusText = getConfigValue("log-response-status-text", lines)
+ .map { anObject: String -> "true" == anObject }.orElse(false)
+
+ if (serverUUID == null) {
+ if (recreateWhenMalformed) {
+ logger.info("Found malformed bStats config file. Re-creating it...")
+ if (!configFile.delete()) {
+ logger.error("Could not delete the bStats config!")
+ return
+ }
+ setupConfig(false)
+ } else {
+ logger.error("Failed to re-create malformed bStats config file")
+ return
+ }
+ }
+ }
+
+ private fun writeConfig(file: File) {
+ val configContent: MutableList = ArrayList()
+ configContent.add("# bStats collects some basic information for plugin authors, like how many people use")
+ configContent.add("# their plugin and their total player count. It's recommend to keep bStats enabled, but")
+ configContent.add("# if you're not comfortable with this, you can turn this setting off. There is no")
+ configContent.add("# performance penalty associated with having metrics enabled, and data sent to bStats")
+ configContent.add("# can't identify your server.")
+ configContent.add("enabled=true")
+ configContent.add("server-uuid=" + UUID.randomUUID().toString())
+ configContent.add("log-errors=false")
+ configContent.add("log-sent-data=false")
+ configContent.add("log-response-status-text=false")
+ writeFile(file, configContent)
+ }
+
+ private fun getConfigValue(key: String, lines: List): Optional {
+ return lines.stream()
+ .filter { it.startsWith("$key=") }
+ .map { it.replaceFirst(Pattern.quote("$key=").toRegex(), "") }
+ .findFirst()
+ }
+
+ private fun readFile(file: File): List {
+ if (!file.exists()) {
+ return emptyList()
+ }
+ FileReader(file).use { BufferedReader(it).use { o -> return o.lines().collect(Collectors.toList()) } }
+ }
+
+ private fun writeFile(file: File, lines: List) {
+ if (!file.exists()) {
+ if (!file.createNewFile()) {
+ logger.error("Could not create the config file for bStats!")
+ return
+ }
+ }
+ FileWriter(file).use {
+ BufferedWriter(it).use { o ->
+ for (line in lines) {
+ o.write(line)
+ o.newLine()
+ }
+ }
+ }
+ }
+
+ private fun appendPlatformData(builder: JsonObjectBuilder) {
+ builder.appendField("playerAmount", proxyServer.playerCount)
+ builder.appendField("managedServers", locationService.getServers().size)
+ builder.appendField("onlineMode", if (proxyServer.configuration.isOnlineMode) 1 else 0)
+ builder.appendField("velocityVersionVersion", proxyServer.version.version)
+ builder.appendField("velocityVersionName", proxyServer.version.name)
+ builder.appendField("velocityVersionVendor", proxyServer.version.vendor)
+ builder.appendField("javaVersion", System.getProperty("java.version"))
+ builder.appendField("osName", System.getProperty("os.name"))
+ builder.appendField("osArch", System.getProperty("os.arch"))
+ builder.appendField("osVersion", System.getProperty("os.version"))
+ builder.appendField("coreCount", Runtime.getRuntime().availableProcessors())
+ }
+
+ private fun appendServiceData(builder: JsonObjectBuilder) =
+ builder.appendField("pluginVersion", environment.pluginInfo.version)
+}
diff --git a/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/module/ApiVelocityModule.kt b/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/module/ApiVelocityModule.kt
index a0e3e0963..86414a6cf 100644
--- a/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/module/ApiVelocityModule.kt
+++ b/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/module/ApiVelocityModule.kt
@@ -31,11 +31,13 @@ import org.anvilpowered.anvil.api.util.TextService
import org.anvilpowered.anvil.api.util.UserService
import org.anvilpowered.anvil.common.PlatformImpl
import org.anvilpowered.anvil.common.command.CommonCallbackCommand
+import org.anvilpowered.anvil.common.metric.MetricService
import org.anvilpowered.anvil.common.module.PlatformModule
import org.anvilpowered.anvil.common.util.CommonTextService
import org.anvilpowered.anvil.common.util.SendTextService
import org.anvilpowered.anvil.velocity.command.VelocityCommandExecuteService
import org.anvilpowered.anvil.velocity.command.VelocitySimpleCommandService
+import org.anvilpowered.anvil.velocity.metric.VelocityMetricService
import org.anvilpowered.anvil.velocity.server.VelocityLocationService
import org.anvilpowered.anvil.velocity.util.VelocityKickService
import org.anvilpowered.anvil.velocity.util.VelocityPermissionService
@@ -57,6 +59,7 @@ class ApiVelocityModule : PlatformModule(
bind>().to()
bind().to()
bind().to()
+ bind().to()
bind().to()
bind>().to>()
bind>().to>()
diff --git a/gradle.properties b/gradle.properties
index 70fd21b0a..f57556bae 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,6 +2,7 @@
aopalliance=aopalliance:aopalliance:1.0
apache_commons=org.apache.commons:commons-pool2:2.6.2
bson=org.mongodb:bson:3.12.0
+bstats=org.bstats:bstats-base:2.1.0
bungee=net.md-5:bungeecord-api:1.15-SNAPSHOT
configurate_core=org.spongepowered:configurate-core:3.7.2
configurate_hocon=org.spongepowered:configurate-hocon:3.7.2