Add Baltop API (#3702)

Co-authored-by: Mariell <proximyst@proximyst.com>
Co-authored-by: MD <1917406+mdcfe@users.noreply.github.com>

This moves storage of balances from the baltop command into the UserMap. This was needed by Glare to able to get a hold of all users balances without causing jvm hell on the usermap.

To access this API as an end user;
```java
import net.essentialsx.api.v2.services.BalanceTop;
//...
BalanceTop api = Bukkit.getServer().getServicesManager().load(BalanceTop.class);
```

Closes #3100, closes #3540
This commit is contained in:
Josh Roy 2021-02-15 07:43:10 -08:00 committed by GitHub
parent 191cea7fb3
commit 36422ab22b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 264 additions and 96 deletions

View File

@ -0,0 +1,87 @@
package com.earth2me.essentials;
import net.ess3.api.IEssentials;
import net.essentialsx.api.v2.services.BalanceTop;
import org.bukkit.plugin.ServicePriority;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class BalanceTopImpl implements BalanceTop {
private final IEssentials ess;
private LinkedHashMap<UUID, BalanceTop.Entry> topCache = new LinkedHashMap<>();
private BigDecimal balanceTopTotal = BigDecimal.ZERO;
private long cacheAge = 0;
private CompletableFuture<Void> cacheLock;
public BalanceTopImpl(IEssentials ess) {
this.ess = ess;
ess.getServer().getServicesManager().register(BalanceTop.class, this, ess, ServicePriority.Normal);
}
private void calculateBalanceTopMap() {
final List<Entry> entries = new LinkedList<>();
BigDecimal newTotal = BigDecimal.ZERO;
for (UUID u : ess.getUserMap().getAllUniqueUsers()) {
final User user = ess.getUserMap().getUser(u);
if (user != null) {
if (!ess.getSettings().isNpcsInBalanceRanking() && user.isNPC()) {
// Don't list NPCs in output
continue;
}
if (!user.isBaltopExempt()) {
final BigDecimal userMoney = user.getMoney();
user.updateMoneyCache(userMoney);
newTotal = newTotal.add(userMoney);
final String name = user.isHidden() ? user.getName() : user.getDisplayName();
entries.add(new BalanceTop.Entry(user.getBase().getUniqueId(), name, userMoney));
}
}
}
final LinkedHashMap<UUID, Entry> sortedMap = new LinkedHashMap<>();
entries.sort((entry1, entry2) -> entry2.getBalance().compareTo(entry1.getBalance()));
for (Entry entry : entries) {
sortedMap.put(entry.getUuid(), entry);
}
topCache = sortedMap;
balanceTopTotal = newTotal;
cacheAge = System.currentTimeMillis();
cacheLock.complete(null);
cacheLock = null;
}
@Override
public CompletableFuture<Void> calculateBalanceTopMapAsync() {
if (cacheLock != null) {
return cacheLock;
}
cacheLock = new CompletableFuture<>();
ess.runTaskAsynchronously(this::calculateBalanceTopMap);
return cacheLock;
}
@Override
public Map<UUID, Entry> getBalanceTopCache() {
return Collections.unmodifiableMap(topCache);
}
@Override
public long getCacheAge() {
return cacheAge;
}
@Override
public BigDecimal getBalanceTopTotal() {
return balanceTopTotal;
}
public boolean isCacheLocked() {
return cacheLock != null;
}
}

View File

@ -68,6 +68,7 @@ import net.ess3.provider.providers.PaperKnownCommandsProvider;
import net.ess3.provider.providers.PaperMaterialTagProvider;
import net.ess3.provider.providers.PaperRecipeBookListener;
import net.ess3.provider.providers.PaperServerStateProvider;
import net.essentialsx.api.v2.services.BalanceTop;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.World;
@ -127,6 +128,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
private transient PermissionsHandler permissionsHandler;
private transient AlternativeCommandsHandler alternativeCommandsHandler;
private transient UserMap userMap;
private transient BalanceTopImpl balanceTop;
private transient ExecuteTimer execTimer;
private transient I18n i18n;
private transient MetricsWrapper metrics;
@ -181,6 +183,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
LOGGER.log(Level.INFO, dataFolder.toString());
settings = new Settings(this);
userMap = new UserMap(this);
balanceTop = new BalanceTopImpl(this);
permissionsHandler = new PermissionsHandler(this, false);
Economy.setEss(this);
confList = new ArrayList<>();
@ -246,6 +249,9 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
confList.add(userMap);
execTimer.mark("Init(Usermap)");
balanceTop = new BalanceTopImpl(this);
execTimer.mark("Init(BalanceTop)");
kits = new Kits(this);
confList.add(kits);
upgrade.convertKits();
@ -968,6 +974,11 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
return userMap;
}
@Override
public BalanceTop getBalanceTop() {
return balanceTop;
}
@Override
public I18n getI18n() {
return i18n;

View File

@ -10,6 +10,7 @@ import net.ess3.provider.KnownCommandsProvider;
import net.ess3.provider.ServerStateProvider;
import net.ess3.provider.SpawnerBlockProvider;
import net.ess3.provider.SpawnerItemProvider;
import net.essentialsx.api.v2.services.BalanceTop;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@ -95,6 +96,8 @@ public interface IEssentials extends Plugin {
UserMap getUserMap();
BalanceTop getBalanceTop();
EssentialsTimer getTimer();
/**

View File

@ -1041,6 +1041,15 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
this.lastHomeConfirmationTimestamp = System.currentTimeMillis();
}
public boolean isBaltopExempt() {
if (getBase().isOnline()) {
final boolean exempt = isAuthorized("essentials.balancetop.exclude");
setBaltopExemptCache(exempt);
return exempt;
}
return isBaltopExcludeCache();
}
@Override
public Block getTargetBlock(int maxDistance) {
final Block block;

View File

@ -70,6 +70,7 @@ public abstract class UserData extends PlayerExtension implements IConf {
private Boolean confirmPay;
private Boolean confirmClear;
private boolean lastMessageReplyRecipient;
private boolean baltopExemptCache;
protected UserData(final Player base, final IEssentials ess) {
super(base);
@ -141,6 +142,7 @@ public abstract class UserData extends PlayerExtension implements IConf {
confirmPay = _getConfirmPay();
confirmClear = _getConfirmClear();
lastMessageReplyRecipient = _getLastMessageReplyRecipient();
baltopExemptCache = _getBaltopExcludeCache();
}
private BigDecimal _getMoney() {
@ -1027,6 +1029,20 @@ public abstract class UserData extends PlayerExtension implements IConf {
save();
}
public boolean _getBaltopExcludeCache() {
return config.getBoolean("baltop-exempt", false);
}
public boolean isBaltopExcludeCache() {
return baltopExemptCache;
}
public void setBaltopExemptCache(boolean baltopExempt) {
this.baltopExemptCache = baltopExempt;
config.setProperty("baltop-exempt", baltopExempt);
config.save();
}
public UUID getConfigUUID() {
return config.uuid;
}

View File

@ -1,40 +1,35 @@
package com.earth2me.essentials.commands;
import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.User;
import com.earth2me.essentials.textreader.SimpleTextInput;
import com.earth2me.essentials.textreader.TextPager;
import com.earth2me.essentials.utils.NumberUtil;
import com.google.common.collect.Lists;
import net.essentialsx.api.v2.services.BalanceTop;
import org.bukkit.Server;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.CompletableFuture;
import static com.earth2me.essentials.I18n.tl;
public class Commandbalancetop extends EssentialsCommand {
public static final int MINUSERS = 50;
private static final int CACHETIME = 2 * 60 * 1000;
private static final SimpleTextInput cache = new SimpleTextInput();
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static long cacheage = 0;
private static SimpleTextInput cache = new SimpleTextInput();
public Commandbalancetop() {
super("balancetop");
}
private static void outputCache(final CommandSource sender, final int page) {
private void outputCache(final CommandSource sender, final int page) {
final Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(cacheage);
cal.setTimeInMillis(ess.getBalanceTop().getCacheAge());
final DateFormat format = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
sender.sendMessage(tl("balanceTop", format.format(cal.getTime())));
new TextPager(cache).showPage(Integer.toString(page), null, "balancetop", sender);
@ -54,25 +49,17 @@ public class Commandbalancetop extends EssentialsCommand {
}
}
if (!force && lock.readLock().tryLock()) {
try {
if (cacheage > System.currentTimeMillis() - CACHETIME) {
outputCache(sender, page);
return;
}
if (ess.getUserMap().getUniqueUsers() > MINUSERS) {
sender.sendMessage(tl("orderBalances", ess.getUserMap().getUniqueUsers()));
}
} finally {
lock.readLock().unlock();
}
} else {
if (ess.getUserMap().getUniqueUsers() > MINUSERS) {
sender.sendMessage(tl("orderBalances", ess.getUserMap().getUniqueUsers()));
}
if (!force && ess.getBalanceTop().getCacheAge() > System.currentTimeMillis() - CACHETIME) {
outputCache(sender, page);
return;
}
ess.runTaskAsynchronously(new Viewer(sender, commandLabel, page, force));
// If there are less than 50 users in our usermap, there is no need to display a warning as these calculations should be done quickly
if (ess.getUserMap().getUniqueUsers() > MINUSERS) {
sender.sendMessage(tl("orderBalances", ess.getUserMap().getUniqueUsers()));
}
ess.runTaskAsynchronously(new Viewer(sender, page, force));
}
@Override
@ -88,89 +75,42 @@ public class Commandbalancetop extends EssentialsCommand {
}
}
private class Calculator implements Runnable {
private final transient Viewer viewer;
private final boolean force;
Calculator(final Viewer viewer, final boolean force) {
this.viewer = viewer;
this.force = force;
}
@Override
public void run() {
lock.writeLock().lock();
try {
if (force || cacheage <= System.currentTimeMillis() - CACHETIME) {
cache.getLines().clear();
final Map<String, BigDecimal> balances = new HashMap<>();
BigDecimal totalMoney = BigDecimal.ZERO;
if (ess.getSettings().isEcoDisabled()) {
if (ess.getSettings().isDebug()) {
ess.getLogger().info("Internal economy functions disabled, aborting baltop.");
}
} else {
for (final UUID u : ess.getUserMap().getAllUniqueUsers()) {
final User user = ess.getUserMap().getUser(u);
if (user != null) {
if (!ess.getSettings().isNpcsInBalanceRanking() && user.isNPC()) {
// Don't list NPCs in output
continue;
}
if (!user.isAuthorized("essentials.balancetop.exclude")) {
final BigDecimal userMoney = user.getMoney();
user.updateMoneyCache(userMoney);
totalMoney = totalMoney.add(userMoney);
final String name = user.isHidden() ? user.getName() : user.getDisplayName();
balances.put(name, userMoney);
}
}
}
}
final List<Map.Entry<String, BigDecimal>> sortedEntries = new ArrayList<>(balances.entrySet());
sortedEntries.sort((entry1, entry2) -> entry2.getValue().compareTo(entry1.getValue()));
cache.getLines().add(tl("serverTotal", NumberUtil.displayCurrency(totalMoney, ess)));
int pos = 1;
for (final Map.Entry<String, BigDecimal> entry : sortedEntries) {
cache.getLines().add(tl("balanceTopLine", pos, entry.getKey(), NumberUtil.displayCurrency(entry.getValue(), ess)));
pos++;
}
cacheage = System.currentTimeMillis();
}
} finally {
lock.writeLock().unlock();
}
ess.runTaskAsynchronously(viewer);
}
}
private class Viewer implements Runnable {
private final transient CommandSource sender;
private final transient int page;
private final transient boolean force;
private final transient String commandLabel;
Viewer(final CommandSource sender, final String commandLabel, final int page, final boolean force) {
Viewer(final CommandSource sender, final int page, final boolean force) {
this.sender = sender;
this.page = page;
this.force = force;
this.commandLabel = commandLabel;
}
@Override
public void run() {
lock.readLock().lock();
try {
if (!force && cacheage > System.currentTimeMillis() - CACHETIME) {
outputCache(sender, page);
return;
if (ess.getSettings().isEcoDisabled()) {
if (ess.getSettings().isDebug()) {
ess.getLogger().info("Internal economy functions disabled, aborting baltop.");
}
} finally {
lock.readLock().unlock();
return;
}
ess.runTaskAsynchronously(new Calculator(new Viewer(sender, commandLabel, page, false), force));
final boolean fresh = force || ess.getBalanceTop().isCacheLocked() || ess.getBalanceTop().getCacheAge() <= System.currentTimeMillis() - CACHETIME;
final CompletableFuture<Void> future = fresh ? ess.getBalanceTop().calculateBalanceTopMapAsync() : CompletableFuture.completedFuture(null);
future.thenRun(() -> {
if (fresh) {
final SimpleTextInput newCache = new SimpleTextInput();
newCache.getLines().add(tl("serverTotal", NumberUtil.displayCurrency(ess.getBalanceTop().getBalanceTopTotal(), ess)));
int pos = 1;
for (final Map.Entry<UUID, BalanceTop.Entry> entry : ess.getBalanceTop().getBalanceTopCache().entrySet()) {
newCache.getLines().add(tl("balanceTopLine", pos, entry.getValue().getDisplayName(), NumberUtil.displayCurrency(entry.getValue().getBalance(), ess)));
pos++;
}
cache = newCache;
}
outputCache(sender, page);
});
}
}
}

View File

@ -0,0 +1,102 @@
package net.essentialsx.api.v2.services;
import java.math.BigDecimal;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* A class which provides numerous methods to interact with Essentials' balance top calculations.
* <p>
* Note: Implementations of this class should be thread-safe and thus do not need to be called from the server thread.
*/
public interface BalanceTop {
/**
* Re-calculates the balance top cache asynchronously.
* <p>
* This method will return a {@link CompletableFuture CompletableFuture&lt;Void&gt;} which
* will be completed upon the recalculation of the balance top map.
* After which you should run {@link BalanceTop#getBalanceTopCache()}
* to get the newly updated cache
*
* @return A future which completes after the balance top cache has been calculated.
*/
CompletableFuture<Void> calculateBalanceTopMapAsync();
/**
* Gets the balance top cache or an empty list if one has not been calculated yet. The balance top cache is a {@link Map}
* which maps the UUID of the player to a {@link BalanceTop.Entry} object which stores the user's display name and balance.
* The returned map is sorted by greatest to least wealth.
* <p>
* There is no guarantee the returned cache is up to date. The balancetop command is directly responsible for updating
* this cache and does so every two minutes (if executed). See {@link BalanceTop#calculateBalanceTopMapAsync()} to
* manually update this cache yourself.
*
* @return The balance top cache.
* @see BalanceTop#calculateBalanceTopMapAsync()
*/
Map<UUID, Entry> getBalanceTopCache();
/**
* Gets the epoch time (in mills.) that the baltop cache was last updated at. A value of zero indicates the cache
* has not been calculated yet at all.
*
* @return The epoch time (in mills.) since last cache update or zero.
*/
long getCacheAge();
/**
* Gets the total amount of money in the economy at the point of the last balance top cache calculation or returns zero
* if no baltop calculation has been made as of yet.
*
* @return The total amount of money in the economy or zero.
* @see BalanceTop#getCacheAge() to find last baltop cache calculation
*/
BigDecimal getBalanceTopTotal();
/**
* Checks to see if {@link BalanceTop#calculateBalanceTopMapAsync()} is still in the process of calculating the map.
*
* @return true if the balance top cache is still in the process of being calculated, otherwise false.
*/
boolean isCacheLocked();
/**
* This class represents a user's name/balance in the balancetop cache.
*/
class Entry {
private final UUID uuid;
private final String displayName;
private final BigDecimal balance;
public Entry(UUID uuid, String displayName, BigDecimal balance) {
this.uuid = uuid;
this.displayName = displayName;
this.balance = balance;
}
/**
* Gets the UUID of the user.
* @return The uuid of this user.
*/
public UUID getUuid() {
return uuid;
}
/**
* Gets the display name of the user at the time of cache population.
* @return The display name of this user.
*/
public String getDisplayName() {
return displayName;
}
/**
* Gets the balance of the user at the time of cache population.
* @return The balance of this user.
*/
public BigDecimal getBalance() {
return balance;
}
}
}