Update stats and leaderboard code to be cleaner and better organized

This commit is contained in:
AlexTheCoder 2018-09-10 22:07:30 -05:00
parent 835b50c683
commit 0d708a99f4
6 changed files with 144 additions and 463 deletions

View File

@ -108,11 +108,6 @@ public class LeaderboardManager extends MiniPlugin
});
}
public void handleStatIncrease(Map<Integer, Map<Integer, Long>> stats)
{
_repo.insertStats(stats);
}
public void registerIfNotExists(String identifier, LeaderboardDisplay display)
{
if (_leaderboards.containsKey(identifier))

View File

@ -1,7 +1,6 @@
package mineplex.core.leaderboard;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
@ -24,9 +23,6 @@ public class LeaderboardRepository extends RepositoryBase
private static final String CREATE_DAILY = "CREATE TABLE accountStatsDaily (accountId INT NOT NULL, statId INT NOT NULL, date DATE NOT NULL, value BIGINT NOT NULL, PRIMARY KEY (accountId, statId), INDEX valueIndex (value DESC), INDEX dateIndex (date), FOREIGN KEY (accountId) REFERENCES accounts(id), FOREIGN KEY (statId) REFERENCES stats(id));";
private static final String CREATE_SEASON = "CREATE TABLE statSeasons (id SMALLINT NOT NULL, seasonName VARCHAR(50) NOT NULL, startDate TIMESTAMP NOT NULL DEFAULT '1969-12-31 18:00:01', endDate TIMESTAMP NOT NULL DEFAULT '1969-12-31 18:00:01', PRIMARY KEY (id), UNIQUE INDEX seasonIndex (seasonName), INDEX startIndex (startDate), INDEX endIndex (endDate));";
private static final String INSERT_STAT = "INSERT INTO accountStatsAllTime (accountId, statId, value) VALUES (?, ?, ?);";
private static final String UPDATE_STAT = "UPDATE accountStatsAllTime SET value=value + ? WHERE accountId=? AND statId=?;";
private static final String FETCH_STAT_ALL = "SELECT a.name, a.uuid, sl.value FROM (SELECT accountId, value FROM accountStatsAllTime WHERE statId=(SELECT id FROM stats WHERE name='%STAT%') ORDER BY value DESC LIMIT %START%,%LIMIT%) AS sl INNER JOIN accounts AS a ON a.id=sl.accountId;";
private static final String FETCH_STAT_YEARLY = "SELECT a.name, a.uuid, sl.value FROM (SELECT accountId, value FROM accountStatsYearly WHERE (date BETWEEN MAKEDATE(YEAR(CURDATE()),1) AND CURDATE()) AND statId=(SELECT id FROM stats WHERE name='%STAT%') ORDER BY value DESC LIMIT %START%,%LIMIT%) AS sl INNER JOIN accounts AS a ON a.id=sl.accountId;";
@ -41,90 +37,6 @@ public class LeaderboardRepository extends RepositoryBase
super(DBPool.getAccount());
}
public void insertStats(Map<Integer, Map<Integer, Long>> stats)
{
UtilServer.runAsync(() ->
{
try (
Connection c = getConnection();
PreparedStatement updateStat = c.prepareStatement(UPDATE_STAT);
PreparedStatement insertStat = c.prepareStatement(INSERT_STAT);
)
{
for (Integer accountId : stats.keySet())
{
for (Integer statId : stats.get(accountId).keySet())
{
updateStat.setLong(1, stats.get(accountId).get(statId));
updateStat.setInt(2, accountId);
updateStat.setInt(3, statId);
updateStat.addBatch();
}
}
int[] rowsAffected = updateStat.executeBatch();
int i = 0;
for (Integer accountId : stats.keySet())
{
for (Integer statId : stats.get(accountId).keySet())
{
if (rowsAffected[i] < 1)
{
insertStat.setInt(1, accountId);
insertStat.setInt(2, statId);
insertStat.setLong(3, stats.get(accountId).get(statId));
insertStat.addBatch();
}
i++;
}
}
insertStat.executeBatch();
}
catch (SQLException e)
{
e.printStackTrace();
}
});
}
public void insertStats(int accountId, Map<Integer, Long> stats)
{
UtilServer.runAsync(() ->
{
try (
Connection c = getConnection();
PreparedStatement updateStat = c.prepareStatement(UPDATE_STAT);
PreparedStatement insertStat = c.prepareStatement(INSERT_STAT);
)
{
for (Integer statId : stats.keySet())
{
updateStat.setLong(1, stats.get(statId));
updateStat.setInt(2, accountId);
updateStat.setInt(3, statId);
updateStat.addBatch();
}
int[] rowsAffected = updateStat.executeBatch();
int i = 0;
for (Integer statId : stats.keySet())
{
if (rowsAffected[i] < 1)
{
insertStat.setInt(1, accountId);
insertStat.setInt(2, statId);
insertStat.setLong(3, stats.get(statId));
insertStat.addBatch();
}
i++;
}
insertStat.executeBatch();
}
catch (SQLException e)
{
e.printStackTrace();
}
});
}
public void loadLeaderboard(Leaderboard board, Consumer<Map<String, Long>> leaderboard)
{
UtilServer.runAsync(() ->

View File

@ -18,9 +18,6 @@ public class PlayerStats
@GuardedBy("_lock")
private Map<String, Long> _stats = new HashMap<>();
@GuardedBy("_lock")
private Map<String, Long> _statsOld = new HashMap<>();
private final boolean _temporary;
public PlayerStats(boolean temporary)
@ -63,14 +60,6 @@ public class PlayerStats
return value;
}
}
void setStatOld(String statName, long value)
{
synchronized (_lock)
{
_statsOld.put(statName, value);
}
}
/**
* Gets the value for the specified stat
@ -79,24 +68,6 @@ public class PlayerStats
* @return The value of the stat if it exists, or 0 if it does not
*/
public long getStat(String statName)
{
synchronized (_lock)
{
long cur = _stats.getOrDefault(statName, 0L);
long old = _statsOld.getOrDefault(statName, 0L);
return cur + (old > cur ? old - cur : 0);
}
}
public long getStatOld(String statName)
{
synchronized (_lock)
{
return _statsOld.getOrDefault(statName, 0L);
}
}
public long getJustCurrentStat(String statName)
{
synchronized (_lock)
{

View File

@ -39,7 +39,7 @@ import mineplex.core.utils.UtilScheduler;
/**
* This manager handles player statistics
*/
public class StatsManager extends MiniClientPlugin<PlayerStats>//MiniDbClientPlugin<PlayerStats>
public class StatsManager extends MiniClientPlugin<PlayerStats>
{
public enum Perm implements Permission
{
@ -52,63 +52,27 @@ public class StatsManager extends MiniClientPlugin<PlayerStats>//MiniDbClientPlu
private final CoreClientManager _coreClientManager;
private final StatsRepository _repository;
private final LeaderboardManager _leaderboard;
private final Map<String, Integer> _stats = new HashMap<>();
private final Map<CoreClient, Map<String, Long>> _statUploadQueue = new HashMap<>();
private final Map<CoreClient, Map<String, Long>> _statUploadQueueOverRidable = new HashMap<>();
private final Set<UUID> _loading = Collections.synchronizedSet(new HashSet<>());
public StatsManager(JavaPlugin plugin, CoreClientManager clientManager)
{
//super("Stats Manager", plugin, clientManager);
super("Stats Manager", plugin);
_repository = new StatsRepository();
_coreClientManager = clientManager;
_leaderboard = new LeaderboardManager(this);
/*clientManager.addStoredProcedureLoginProcessor(new ILoginProcessor()
{
public String getName()
{
return "Stat Old Selector";
}
@Override
public void processLoginResultSet(String playerName, UUID uuid, int accountId, ResultSet resultSet) throws SQLException
{
PlayerStats stats = Get(uuid);
while (resultSet.next())
{
stats.setStatOld(resultSet.getString(1), resultSet.getLong(2));
}
}
@Override
public String getQuery(int accountId, String uuid, String name)
{
return "SELECT stats.name, value FROM accountStat INNER JOIN stats ON stats.id = accountStat.statId WHERE accountStat.accountId = '" + accountId + "';";
}
});*/
new LeaderboardManager(this);
UtilScheduler.runAsyncEvery(UpdateType.SEC, () ->
{
save(_statUploadQueue, map ->
{
//_repository.saveStats(map);
_repository.saveOnlyExisting(map);
_leaderboard.handleStatIncrease(map);
_repository.insertStats(map);
}, "increment");
save(_statUploadQueueOverRidable, map ->
{
//_repository.saveStats(map, true);
_repository.saveOnlyExisting(map);
_leaderboard.handleStatIncrease(map);
}, "override");
});
for (Stat stat : _repository.retrieveStats())
@ -180,38 +144,13 @@ public class StatsManager extends MiniClientPlugin<PlayerStats>//MiniDbClientPlu
return;
}
long oldCompositeValue = snapshot.getStat(statName);
long oldValue = snapshot.getStat(statName);
long newValue = snapshot.addStat(statName, value);
UtilServer.getServer().getPluginManager().callEvent(new StatChangeEvent(player, statName, oldCompositeValue, newValue));
UtilServer.getServer().getPluginManager().callEvent(new StatChangeEvent(player, statName, oldValue, newValue));
registerNewStat(statName, () -> addToQueue(statName, client, value));
}
/**
* Sets the value of a stat for the given player
*
* @param value The value, must be greater or equal to zero
*/
@Deprecated
public void setStat(Player player, String statName, long value)
{
if (value < 0)
return;
if (0 == 0)
{
return;
}
CoreClient client = _coreClientManager.Get(player);
long oldValue = Get(player).getStat(statName);
Get(player).setStat(statName, value);
UtilServer.getServer().getPluginManager().callEvent(new StatChangeEvent(player, statName, oldValue, value));
registerNewStat(statName, () -> addToOverRidableQueue(statName, client, value));
}
/**
* Increments a stat for the given account ID of an <b>offline player</b> by the specified amount
*/
@ -219,58 +158,13 @@ public class StatsManager extends MiniClientPlugin<PlayerStats>//MiniDbClientPlu
{
registerNewStat(statName, () ->
{
Map<Integer, Map<Integer, Long>> uploadQueue = new HashMap<>();
uploadQueue.computeIfAbsent(accountId, key -> new HashMap<>()).put(_stats.get(statName), value);
_repository.saveStats(uploadQueue, false);
Map<Integer, Long> stats = new HashMap<>();
stats.put(_stats.get(statName), value);
_repository.insertStats(accountId, stats);
});
}
/**
* Sets the value of a stat for the given account ID of an <b>offline player</b>
*
* @param value The value, must be greater or equal to zero
*/
@Deprecated
public void setStat(final int accountId, final String statName, final long value)
{
if (value < 0)
return;
if (0 == 0)
{
return;
}
registerNewStat(statName, () ->
{
Map<Integer, Map<Integer, Long>> uploadQueue = new HashMap<>();
uploadQueue.computeIfAbsent(accountId, key -> new HashMap<>()).put(_stats.get(statName), value);
_repository.saveStats(uploadQueue, true);
});
}
@Deprecated
private void addToOverRidableQueue(String statName, CoreClient client, long value)
{
if (0 == 0)
{
return;
}
if (client.getAccountId() == -1)
{
System.out.println(String.format("Error: Tried to add %s/%s to overridable queue with -1 account id", client.getName(), client.getUniqueId()));
return;
}
synchronized (STATS_LOCK)
{
_statUploadQueueOverRidable
.computeIfAbsent(client, key -> new HashMap<>())
.put(statName, value);
}
}
private void addToQueue(String statName, CoreClient client, long value)
{
if (client.getAccountId() == -1)
@ -369,17 +263,13 @@ public class StatsManager extends MiniClientPlugin<PlayerStats>//MiniDbClientPlu
final UUID uuid = event.getPlayer().getUniqueId();
final int accountId = _coreClientManager.Get(event.getPlayer()).getAccountId();
UtilPlayer.message(event.getPlayer(), F.main(getName(), "Loading your stats..."));
runAsync(() ->
runSyncLater(() ->
{
_repository.loadStatsFromOld(accountId, data ->
_repository.loadStats(accountId, data ->
{
PlayerStats stats = new PlayerStats(false);
data.forEach((stat, values) ->
{
stats.setStatOld(stat, values.getLeft());
stats.addStat(stat, values.getRight());
});
data.forEach(stats::setStat);
if (_loading.remove(uuid))
{
@ -431,33 +321,4 @@ public class StatsManager extends MiniClientPlugin<PlayerStats>//MiniDbClientPlu
{
return _coreClientManager;
}
/* @Override
public void processLoginResultSet(String playerName, UUID uuid, int accountId, ResultSet resultSet) throws SQLException
{
PlayerStats playerStats = new PlayerStats();
while (resultSet.next())
{
try
{
playerStats.addStat(resultSet.getString(1), resultSet.getLong(2));
}
catch (Exception ex)
{
ex.printStackTrace();
playerStats.addStat(resultSet.getString(1), -1);
}
}
Set(uuid, playerStats);
}
*/
/* @Override
public String getQuery(int accountId, String uuid, String name)
{
return "SELECT stats.name, value FROM accountStatsAllTime INNER JOIN stats ON stats.id = accountStatsAllTime.statId WHERE accountStatsAllTime.accountId = '" + accountId + "';";
}
*/
}

View File

@ -2,36 +2,29 @@ package mineplex.core.stats;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.Consumer;
import org.jooq.DSLContext;
import org.jooq.Insert;
import org.jooq.Record2;
import org.jooq.Result;
import org.jooq.SQLDialect;
import org.jooq.Update;
import org.jooq.impl.DSL;
import org.jooq.types.ULong;
import com.mysql.jdbc.exceptions.jdbc4.MySQLDataException;
import mineplex.core.common.Pair;
import mineplex.core.common.util.UtilServer;
import mineplex.database.Tables;
import mineplex.serverdata.database.DBPool;
import mineplex.serverdata.database.RepositoryBase;
import mineplex.serverdata.database.column.ColumnInt;
import mineplex.serverdata.database.column.ColumnVarChar;
public class StatsRepository extends RepositoryBase
{
private static final String SELECT_ACCOUNT_STATS = "SELECT stats.name, accountStatsAllTime.value FROM accountStatsAllTime INNER JOIN stats ON stats.id = accountStatsAllTime.statId WHERE accountStatsAllTime.accountId=?;";
private static final String SELECT_USER_STATS = "SELECT stats.name, accountStatsAllTime.value FROM accountStatsAllTime INNER JOIN stats ON stats.id = accountStatsAllTime.statId WHERE accountStatsAllTime.accountId=(SELECT id FROM accounts WHERE name=? ORDER BY lastLogin DESC LIMIT 1);";
private static final String INSERT_ACCOUNT_STAT = "INSERT INTO accountStatsAllTime (accountId, statId, value) VALUES (?, ?, ?);";
private static final String UPDATE_ACCOUNT_STAT = "UPDATE accountStatsAllTime SET value=value + ? WHERE accountId=? AND statId=?;";
private static final String RETRIEVE_STATS = "SELECT id, name FROM stats;";
private static final String INSERT_STAT = "INSERT INTO stats (name) VALUES (?);";
@ -39,66 +32,6 @@ public class StatsRepository extends RepositoryBase
{
super(DBPool.getAccount());
}
public void loadStatsFromOld(int accountId, Consumer<Map<String, Pair<Long, Long>>> callback)
{
try (Connection c = getConnection();
PreparedStatement oldStatement = c.prepareStatement("SELECT stats.name, value FROM accountStat INNER JOIN stats ON stats.id = accountStat.statId WHERE accountStat.accountId=?;");
PreparedStatement newStatement = c.prepareStatement("SELECT stats.name, value FROM accountStatsAllTime INNER JOIN stats ON stats.id = accountStatsAllTime.statId WHERE accountStatsAllTime.accountId=?;");
)
{
oldStatement.setInt(1, accountId);
newStatement.setInt(1, accountId);
try (ResultSet oldSet = oldStatement.executeQuery();
ResultSet newSet = newStatement.executeQuery();
)
{
final Map<String, Pair<Long, Long>> statMap = new HashMap<>();
while (oldSet.next())
{
String statName = oldSet.getString(1);
long oldValue;
try
{
oldValue = oldSet.getLong(2);
}
catch (MySQLDataException ex)
{
oldValue = 0;
}
statMap.put(statName, Pair.create(oldValue, 0L));
}
while (newSet.next())
{
String statName = newSet.getString(1);
long newValue;
try
{
newValue = newSet.getLong(2);
}
catch (MySQLDataException ex)
{
newValue = 0;
}
if (statMap.containsKey(statName))
{
statMap.get(statName).setRight(newValue);
}
else
{
statMap.put(statName, Pair.create(0L, newValue));
}
}
UtilServer.runSync(() -> callback.accept(statMap));
}
}
catch (SQLException e)
{
e.printStackTrace();
UtilServer.runSync(() -> callback.accept(new HashMap<>()));
}
}
/**
* Retrieves all the remote registered stats
@ -129,7 +62,7 @@ public class StatsRepository extends RepositoryBase
{
try (Connection c = getConnection())
{
executeInsert(c, "INSERT INTO stats (name) VALUES (?);", rs -> onComplete.run(), () -> {}, new ColumnVarChar("name", 100, name));
executeInsert(c, INSERT_STAT, rs -> onComplete.run(), () -> {}, new ColumnVarChar("name", 100, name));
}
catch (SQLException e)
{
@ -137,111 +70,33 @@ public class StatsRepository extends RepositoryBase
}
}
public void saveOnlyExisting(Map<Integer, Map<Integer, Long>> uploadQueue)
public void loadStats(int accountId, Consumer<Map<String, Long>> callback)
{
try (Connection c = getConnection();
PreparedStatement ps = c.prepareStatement("UPDATE accountStat SET value=value+? WHERE accountId=? AND statId=?;");
)
UtilServer.runAsync(() ->
{
for (Entry<Integer, Map<Integer, Long>> accountEntry : uploadQueue.entrySet())
Map<String, Long> loaded = new HashMap<>();
executeQuery(SELECT_ACCOUNT_STATS, resultSet ->
{
int accountId = accountEntry.getKey();
for (Entry<Integer, Long> statEntry : accountEntry.getValue().entrySet())
while (resultSet.next())
{
int statId = statEntry.getKey();
long delta = statEntry.getValue();
ps.setLong(1, delta);
ps.setInt(2, accountId);
ps.setInt(3, statId);
ps.addBatch();
}
}
ps.executeBatch();
}
catch (SQLException e)
{
e.printStackTrace();
}
}
/**
* Saves the given stats
*
* @param uploadQueue A map of account ID to a map of stat IDS to values
*/
public void saveStats(Map<Integer, Map<Integer, Long>> uploadQueue)
{
saveStats(uploadQueue, false);
}
/**
* Saves the given stats
*
* @param uploadQueue A map of account ID to a map of stat IDS to values
* @param overrideStat Whether to replace the remote value, or to add to it
*/
public void saveStats(Map<Integer, Map<Integer, Long>> uploadQueue, boolean overrideStat)
{
try
{
DSLContext context = DSL.using(getConnectionPool(), SQLDialect.MYSQL);
List<Update> updates = new ArrayList<>();
List<Insert> inserts = new ArrayList<>();
for (int accountId : uploadQueue.keySet())
{
for (Integer statId : uploadQueue.get(accountId).keySet())
{
if (overrideStat)
String statName = resultSet.getString(1);
long value;
try
{
Update update = context
.update(Tables.accountStat)
.set(Tables.accountStat.value, ULong.valueOf(uploadQueue.get(accountId).get(statId)))
.where(Tables.accountStat.accountId.eq(accountId))
.and(Tables.accountStat.statId.eq(statId));
updates.add(update);
value = resultSet.getLong(2);
}
else
catch (MySQLDataException ex)
{
Update update = context
.update(Tables.accountStat)
.set(Tables.accountStat.value, Tables.accountStat.value.plus(uploadQueue.get(accountId).get(statId)))
.where(Tables.accountStat.accountId.eq(accountId))
.and(Tables.accountStat.statId.eq(statId));
updates.add(update);
value = 0;
}
Insert insert = context
.insertInto(Tables.accountStat)
.set(Tables.accountStat.accountId, accountId)
.set(Tables.accountStat.statId, statId)
.set(Tables.accountStat.value, ULong.valueOf(uploadQueue.get(accountId).get(statId)));
inserts.add(insert);
loaded.put(statName, value);
}
}
int[] updateResult = context.batch(updates).execute();
for (int i = 0; i < updateResult.length; i++)
{
if (updateResult[i] > 0)
inserts.set(i, null);
}
inserts.removeIf(Objects::isNull);
context.batch(inserts).execute();
}
catch (Exception e)
{
System.out.println("Failed to save stats: " + uploadQueue);
e.printStackTrace();
}
}, new ColumnInt("accountId", accountId));
UtilServer.runSync(() -> callback.accept(loaded));
});
}
/**
* Gets offline stats for the specified player name. This performs SQL on the current thread
*/
@ -249,32 +104,113 @@ public class StatsRepository extends RepositoryBase
{
PlayerStats playerStats = null;
DSLContext context;
synchronized (this)
Map<String, Long> loaded = new HashMap<>();
executeQuery(SELECT_USER_STATS, resultSet ->
{
context = DSL.using(getConnectionPool(), SQLDialect.MYSQL);
}
while (resultSet.next())
{
String statName = resultSet.getString(1);
long value;
try
{
value = resultSet.getLong(2);
}
catch (MySQLDataException ex)
{
value = 0;
}
loaded.put(statName, value);
}
}, new ColumnVarChar("name", playerName.length(), playerName));
Result<Record2<String, ULong>> result = context.select(Tables.stats.name, Tables.accountStat.value).from(Tables.accountStat)
.join(Tables.stats)
.on(Tables.stats.id.eq(Tables.accountStat.statId))
.where(Tables.accountStat.accountId.eq(DSL.select(Tables.accounts.id)
.from(Tables.accounts)
.where(Tables.accounts.name.eq(playerName)).limit(1))
)
.fetch();
if (result.isNotEmpty())
if (!loaded.isEmpty())
{
playerStats = new PlayerStats(false);
for (Record2<String, ULong> record : result)
{
playerStats.addStat(record.value1(), record.value2().longValue());
}
loaded.forEach(playerStats::addStat);
}
return playerStats;
}
}
public void insertStats(Map<Integer, Map<Integer, Long>> stats)
{
UtilServer.runAsync(() ->
{
try (Connection c = getConnection();
PreparedStatement updateStat = c.prepareStatement(UPDATE_ACCOUNT_STAT);
PreparedStatement insertStat = c.prepareStatement(INSERT_ACCOUNT_STAT);
)
{
for (Integer accountId : stats.keySet())
{
for (Integer statId : stats.get(accountId).keySet())
{
updateStat.setLong(1, stats.get(accountId).get(statId));
updateStat.setInt(2, accountId);
updateStat.setInt(3, statId);
updateStat.addBatch();
}
}
int[] rowsAffected = updateStat.executeBatch();
int i = 0;
for (Integer accountId : stats.keySet())
{
for (Integer statId : stats.get(accountId).keySet())
{
if (rowsAffected[i] < 1)
{
insertStat.setInt(1, accountId);
insertStat.setInt(2, statId);
insertStat.setLong(3, stats.get(accountId).get(statId));
insertStat.addBatch();
}
i++;
}
}
insertStat.executeBatch();
}
catch (SQLException e)
{
e.printStackTrace();
}
});
}
public void insertStats(int accountId, Map<Integer, Long> stats)
{
UtilServer.runAsync(() ->
{
try (Connection c = getConnection();
PreparedStatement updateStat = c.prepareStatement(UPDATE_ACCOUNT_STAT);
PreparedStatement insertStat = c.prepareStatement(INSERT_ACCOUNT_STAT);
)
{
for (Integer statId : stats.keySet())
{
updateStat.setLong(1, stats.get(statId));
updateStat.setInt(2, accountId);
updateStat.setInt(3, statId);
updateStat.addBatch();
}
int[] rowsAffected = updateStat.executeBatch();
int i = 0;
for (Integer statId : stats.keySet())
{
if (rowsAffected[i] < 1)
{
insertStat.setInt(1, accountId);
insertStat.setInt(2, statId);
insertStat.setLong(3, stats.get(statId));
insertStat.addBatch();
}
i++;
}
insertStat.executeBatch();
}
catch (SQLException e)
{
e.printStackTrace();
}
});
}
}

View File

@ -37,6 +37,12 @@ public class GiveStatCommand extends CommandBase<StatsManager>
UtilPlayer.message(caller, F.main("Stats", F.elem(args[1]) + " is not a number"));
return;
}
if (amount < 1)
{
UtilPlayer.message(caller, F.main("Stats", "That amount is invalid"));
return;
}
String statName = StringUtils.join(args, " ", 2, args.length);