diff --git a/Plan/common/src/main/java/com/djrapitops/plan/modules/FiltersModule.java b/Plan/common/src/main/java/com/djrapitops/plan/modules/FiltersModule.java
index 34bbf110c..b8cdba19d 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/modules/FiltersModule.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/modules/FiltersModule.java
@@ -53,4 +53,8 @@ public interface FiltersModule {
@IntoSet
Filter filter7(GeolocationsFilter filter);
+ @Binds
+ @IntoSet
+ Filter filter8(PluginBooleanGroupFilter filter);
+
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/PluginBooleanGroupFilter.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/PluginBooleanGroupFilter.java
new file mode 100644
index 000000000..930d6cbba
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/PluginBooleanGroupFilter.java
@@ -0,0 +1,248 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan 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 Plan. If not, see .
+ */
+package com.djrapitops.plan.storage.database.queries.filter.filters;
+
+import com.djrapitops.plan.identification.Server;
+import com.djrapitops.plan.identification.ServerUUID;
+import com.djrapitops.plan.storage.database.DBSystem;
+import com.djrapitops.plan.storage.database.Database;
+import com.djrapitops.plan.storage.database.queries.Query;
+import com.djrapitops.plan.storage.database.queries.QueryAllStatement;
+import com.djrapitops.plan.storage.database.queries.QueryStatement;
+import com.djrapitops.plan.storage.database.queries.filter.SpecifiedFilterInformation;
+import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
+import com.djrapitops.plan.storage.database.sql.tables.ExtensionPlayerValueTable;
+import com.djrapitops.plan.storage.database.sql.tables.ExtensionPluginTable;
+import com.djrapitops.plan.storage.database.sql.tables.ExtensionProviderTable;
+import com.djrapitops.plan.storage.database.sql.tables.ServerTable;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.*;
+
+import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
+
+@Singleton
+public class PluginBooleanGroupFilter extends MultiOptionFilter {
+
+ private final DBSystem dbSystem;
+
+ @Inject
+ public PluginBooleanGroupFilter(DBSystem dbSystem) {
+ this.dbSystem = dbSystem;
+ }
+
+ private static Query> pluginBooleanOptionsQuery() {
+ String selectOptions = SELECT +
+ "server." + ServerTable.SERVER_ID + " as server_id," +
+ "server." + ServerTable.NAME + " as server_name," +
+ "plugin." + ExtensionPluginTable.PLUGIN_NAME + " as plugin_name," +
+ "provider." + ExtensionProviderTable.TEXT + " as provider_text" +
+ FROM + ServerTable.TABLE_NAME + " server" +
+ INNER_JOIN + ExtensionPluginTable.TABLE_NAME + " plugin on plugin." + ExtensionPluginTable.SERVER_UUID + "=server." + ServerTable.SERVER_UUID +
+ INNER_JOIN + ExtensionProviderTable.TABLE_NAME + " provider on provider." + ExtensionProviderTable.PLUGIN_ID + "=plugin." + ExtensionPluginTable.ID +
+ INNER_JOIN + ExtensionPlayerValueTable.TABLE_NAME + " value on value." + ExtensionPlayerValueTable.PROVIDER_ID + "=provider." + ExtensionProviderTable.ID +
+ WHERE + "value." + ExtensionPlayerValueTable.BOOLEAN_VALUE + " IS NOT NULL" +
+ ORDER_BY + "server_name ASC, plugin_name ASC, provider_text ASC";
+ return new QueryAllStatement>(selectOptions) {
+ @Override
+ public List processResults(ResultSet set) throws SQLException {
+ List options = new ArrayList<>();
+ while (set.next()) {
+ int serverId = set.getInt("server_id");
+ String serverName = set.getString("server_name");
+ String pluginName = set.getString("plugin_name");
+ String providerText = set.getString("provider_text");
+ options.add(new PluginBooleanOption(
+ Server.getIdentifiableName(serverName, serverId),
+ pluginName,
+ providerText
+ ));
+ }
+ Collections.sort(options);
+ return options;
+ }
+ };
+ }
+
+ private static Query> playersInGroups(
+ Map selected,
+ Map namesToUUIDs
+ ) {
+ return db -> {
+ Set playerUUIDs = new HashSet<>();
+ for (Map.Entry option : selected.entrySet()) {
+ PluginBooleanOption pluginBooleanOption = option.getKey();
+ SelectedBoolean selectedBoolean = option.getValue();
+ playerUUIDs.addAll(
+ db.query(playersInGroup(
+ namesToUUIDs.get(pluginBooleanOption.getServerName()),
+ pluginBooleanOption.getPluginName(),
+ pluginBooleanOption.getProviderText(),
+ selectedBoolean
+ ))
+ );
+ }
+ return playerUUIDs;
+ };
+ }
+
+ private static Query> playersInGroup(
+ ServerUUID serverUUID, String pluginName, String providerText, SelectedBoolean selectedBoolean
+ ) {
+ String selectUUIDsWithBooleanValues = SELECT + DISTINCT + "value." + ExtensionPlayerValueTable.USER_UUID + " as uuid" +
+ FROM + ExtensionPluginTable.TABLE_NAME + " plugin" +
+ INNER_JOIN + ExtensionProviderTable.TABLE_NAME + " provider on provider." + ExtensionProviderTable.PLUGIN_ID + "=plugin." + ExtensionPluginTable.ID +
+ INNER_JOIN + ExtensionPlayerValueTable.TABLE_NAME + " value on value." + ExtensionPlayerValueTable.PROVIDER_ID + "=provider." + ExtensionProviderTable.ID +
+ WHERE + "plugin." + ExtensionPluginTable.SERVER_UUID + "=?" +
+ AND + "plugin." + ExtensionPluginTable.PLUGIN_NAME + "=?" +
+ AND + "provider." + ExtensionProviderTable.TEXT + "=?" +
+ AND + "value." + ExtensionPlayerValueTable.BOOLEAN_VALUE + (selectedBoolean == SelectedBoolean.BOTH ? "IS NOT NULL" : "=?");
+
+ return new QueryStatement>(selectUUIDsWithBooleanValues) {
+ @Override
+ public void prepare(PreparedStatement statement) throws SQLException {
+ statement.setString(1, serverUUID.toString());
+ statement.setString(2, pluginName);
+ statement.setString(3, providerText);
+ if (selectedBoolean != SelectedBoolean.BOTH) {
+ statement.setBoolean(4, selectedBoolean == SelectedBoolean.TRUE);
+ }
+ }
+
+ @Override
+ public Set processResults(ResultSet set) throws SQLException {
+ Set uuids = new HashSet<>();
+ while (set.next()) {
+ uuids.add(UUID.fromString(set.getString("uuid")));
+ }
+ return uuids;
+ }
+ };
+ }
+
+ @Override
+ public String getKind() {
+ return "pluginsBooleanGroups";
+ }
+
+ private List getOptionList() {
+ Database database = dbSystem.getDatabase();
+ List pluginBooleanOptions = database.query(pluginBooleanOptionsQuery());
+
+ List options = new ArrayList<>();
+ for (PluginBooleanOption pluginBooleanOption : pluginBooleanOptions) {
+ String names = pluginBooleanOption.format();
+ options.add(names + ": true");
+ options.add(names + ": false");
+ }
+
+ return options;
+ }
+
+ @Override
+ public Map getOptions() {
+ return Collections.singletonMap("options", getOptionList());
+ }
+
+ @Override
+ public Set getMatchingUUIDs(SpecifiedFilterInformation query) {
+ Map selectedBooleanOptions = new HashMap<>();
+ for (String selected : getSelected(query)) {
+ String[] optionAndBoolean = StringUtils.split(selected, ":", 2);
+ PluginBooleanOption pluginBooleanOption = PluginBooleanOption.parse(optionAndBoolean[0].trim());
+ String selectedBoolean = optionAndBoolean[1].trim().toUpperCase();
+ selectedBooleanOptions.computeIfPresent(pluginBooleanOption, (key, existing) -> SelectedBoolean.BOTH);
+ selectedBooleanOptions.computeIfAbsent(pluginBooleanOption, key -> SelectedBoolean.valueOf(selectedBoolean));
+ }
+
+ Database db = dbSystem.getDatabase();
+ Map namesToUUIDs = db.query(ServerQueries.fetchServerNamesToUUIDs());
+ return db.query(playersInGroups(selectedBooleanOptions, namesToUUIDs));
+ }
+
+ public enum SelectedBoolean {
+ TRUE,
+ FALSE,
+ BOTH
+ }
+
+ private static class PluginBooleanOption implements Comparable {
+ private final String serverName;
+ private final String pluginName;
+ private final String providerText;
+
+ public PluginBooleanOption(String serverName, String pluginName, String providerText) {
+ this.serverName = serverName;
+ this.pluginName = pluginName;
+ this.providerText = providerText;
+ }
+
+ public static PluginBooleanOption parse(String fromFormatted) {
+ String[] split1 = StringUtils.split(fromFormatted, ",", 2);
+ String[] split2 = StringUtils.split(split1[1], "-", 2);
+ String serverName = split1[0].trim();
+ String pluginName = split2[0].trim();
+ String providerName = split2[1].trim();
+ return new PluginBooleanOption(serverName, pluginName, providerName);
+ }
+
+ public String getServerName() {
+ return serverName;
+ }
+
+ public String getPluginName() {
+ return pluginName;
+ }
+
+ public String getProviderText() {
+ return providerText;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PluginBooleanOption that = (PluginBooleanOption) o;
+ return Objects.equals(getServerName(), that.getServerName()) && Objects.equals(getPluginName(), that.getPluginName()) && Objects.equals(getProviderText(), that.getProviderText());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getServerName(), getPluginName(), getProviderText());
+ }
+
+ @Override
+ public int compareTo(PluginBooleanOption o) {
+ int serverNameAlphabetical = String.CASE_INSENSITIVE_ORDER.compare(serverName, o.serverName);
+ if (serverNameAlphabetical != 0) return serverNameAlphabetical;
+
+ int pluginNameAlphabetical = String.CASE_INSENSITIVE_ORDER.compare(pluginName, o.pluginName);
+ if (pluginNameAlphabetical != 0) return pluginNameAlphabetical;
+
+ return String.CASE_INSENSITIVE_ORDER.compare(providerText, o.providerText);
+ }
+
+ public String format() {
+ return serverName + ", " + pluginName + " - " + providerText;
+ }
+ }
+}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/ServerQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/ServerQueries.java
index e2a143714..33bb85782 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/ServerQueries.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/ServerQueries.java
@@ -23,6 +23,7 @@ import com.djrapitops.plan.storage.database.queries.QueryAllStatement;
import com.djrapitops.plan.storage.database.queries.QueryStatement;
import com.djrapitops.plan.storage.database.sql.building.Select;
import com.djrapitops.plan.storage.database.sql.tables.ServerTable;
+import com.djrapitops.plan.utilities.java.Maps;
import org.apache.commons.lang3.math.NumberUtils;
import java.sql.PreparedStatement;
@@ -148,7 +149,7 @@ public class ServerQueries {
public static Query