Metadata
This commit is contained in:
parent
77f1f18f27
commit
ece4416c2e
@ -1,5 +1,34 @@
|
||||
package mineplex.core.antihack;
|
||||
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import net.minecraft.server.v1_8_R3.ChatClickable;
|
||||
import net.minecraft.server.v1_8_R3.ChatComponentText;
|
||||
import net.minecraft.server.v1_8_R3.ChatHoverable;
|
||||
import net.minecraft.server.v1_8_R3.ChatModifier;
|
||||
import net.minecraft.server.v1_8_R3.EnumChatFormat;
|
||||
import net.minecraft.server.v1_8_R3.IChatBaseComponent;
|
||||
import net.minecraft.server.v1_8_R3.MinecraftServer;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerToggleFlightEvent;
|
||||
import org.bukkit.plugin.ServicePriority;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@ -44,34 +73,9 @@ import mineplex.core.punish.Category;
|
||||
import mineplex.core.punish.Punish;
|
||||
import mineplex.core.punish.PunishClient;
|
||||
import mineplex.core.punish.Punishment;
|
||||
import mineplex.core.punish.PunishmentResponse;
|
||||
import mineplex.serverdata.commands.ServerCommandManager;
|
||||
|
||||
import net.minecraft.server.v1_8_R3.ChatClickable;
|
||||
import net.minecraft.server.v1_8_R3.ChatComponentText;
|
||||
import net.minecraft.server.v1_8_R3.ChatHoverable;
|
||||
import net.minecraft.server.v1_8_R3.ChatModifier;
|
||||
import net.minecraft.server.v1_8_R3.EnumChatFormat;
|
||||
import net.minecraft.server.v1_8_R3.IChatBaseComponent;
|
||||
import net.minecraft.server.v1_8_R3.MinecraftServer;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerToggleFlightEvent;
|
||||
import org.bukkit.plugin.ServicePriority;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ReflectivelyCreateMiniPlugin
|
||||
public class AntiHack extends MiniPlugin
|
||||
{
|
||||
@ -90,6 +94,8 @@ public class AntiHack extends MiniPlugin
|
||||
|
||||
public static final CheckThresholds UNKNOWN_TYPE = new CheckThresholds("Unknown", 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
|
||||
public static final int ID_LENGTH = 5;
|
||||
|
||||
public static final String NAME = "Chiss";
|
||||
public static final String USER_HAS_BEEN_BANNED = F.main("GWEN", "%s has been banned. I am always watching.");
|
||||
public static final String USER_HAS_BEEN_BANNED_BANWAVE = USER_HAS_BEEN_BANNED;
|
||||
@ -188,22 +194,37 @@ public class AntiHack extends MiniPlugin
|
||||
}
|
||||
}
|
||||
|
||||
public void doBan(Player player, String message)
|
||||
public void doBan(Player player)
|
||||
{
|
||||
runSync(() ->
|
||||
{
|
||||
CoreClient coreClient = _clientManager.Get(player);
|
||||
|
||||
Consumer<Consumer<PunishmentResponse>> doPunish = after ->
|
||||
{
|
||||
String id = generateId(ID_LENGTH);
|
||||
String finalMessage = "[GWEN] " + id + "";
|
||||
require(AntihackLogger.class).save(player, id, () ->
|
||||
{
|
||||
require(Punish.class).AddPunishment(coreClient.getName(), Category.Hacking, finalMessage, AntiHack.NAME, 3, true, -1, true, after);
|
||||
});
|
||||
};
|
||||
|
||||
if (coreClient.GetRank().has(Rank.TWITCH))
|
||||
{
|
||||
require(Punish.class).AddPunishment(coreClient.getName(), Category.Hacking, message, AntiHack.NAME, 3, true, -1, true);
|
||||
doPunish.accept(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
runBanAnimation(player, () ->
|
||||
{
|
||||
require(Punish.class).AddPunishment(coreClient.getName(), Category.Hacking, message, AntiHack.NAME, 3, true, -1, true);
|
||||
doPunish.accept(result ->
|
||||
{
|
||||
if (result == PunishmentResponse.Punished)
|
||||
{
|
||||
announceBan(player);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -635,4 +656,11 @@ public class AntiHack extends MiniPlugin
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static String generateId(int length)
|
||||
{
|
||||
byte[] holder = new byte[length];
|
||||
ThreadLocalRandom.current().nextBytes(holder);
|
||||
return DatatypeConverter.printHexBinary(holder);
|
||||
}
|
||||
}
|
||||
|
@ -18,14 +18,12 @@ public abstract class AntiHackAction implements Listener
|
||||
private static final Map<Class<?>, AntiHackAction> ACTIONS = new HashMap<>();
|
||||
private static final AntiHackAction NOOP_ACTION = new NoopAction();
|
||||
|
||||
private static final Date NEXT_BAN_WAVE = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5));
|
||||
|
||||
static
|
||||
{
|
||||
// ACTIONS.put(KillauraTypeA.class, new ImmediateBanAction(200));
|
||||
// ACTIONS.put(KillauraTypeD.class, new BanwaveAction(2000));
|
||||
// ACTIONS.put(Glide.class, new ImmediateBanAction(10000));
|
||||
// ACTIONS.put(Speed.class, new ImmediateBanAction(10000));
|
||||
ACTIONS.put(KillauraTypeA.class, new ImmediateBanAction(200));
|
||||
ACTIONS.put(KillauraTypeD.class, new BanwaveAction(2000));
|
||||
ACTIONS.put(Glide.class, new ImmediateBanAction(10000));
|
||||
ACTIONS.put(Speed.class, new ImmediateBanAction(10000));
|
||||
}
|
||||
|
||||
private int _vl;
|
||||
|
@ -27,7 +27,6 @@ class BanwaveAction extends AntiHackAction
|
||||
event.getPlayer(),
|
||||
banTime,
|
||||
event.getCheckClass(),
|
||||
"[GWEN] Hacking [BanWave]",
|
||||
event.getViolations(),
|
||||
UtilServer.getServerName()
|
||||
);
|
||||
|
@ -17,12 +17,7 @@ class ImmediateBanAction extends AntiHackAction
|
||||
{
|
||||
if (event.getViolations() >= this.getMinVl())
|
||||
{
|
||||
String server = UtilServer.getServerName();
|
||||
if (server.contains("-"))
|
||||
{
|
||||
server = server.substring(0, server.indexOf('-'));
|
||||
}
|
||||
Managers.get(AntiHack.class).doBan(event.getPlayer(), "[GWEN] Hacking [" + server + "]");
|
||||
Managers.get(AntiHack.class).doBan(event.getPlayer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
package mineplex.core.antihack.banwave;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import mineplex.core.MiniPlugin;
|
||||
import mineplex.core.ReflectivelyCreateMiniPlugin;
|
||||
import mineplex.core.account.CoreClient;
|
||||
import mineplex.core.account.CoreClientManager;
|
||||
import mineplex.core.antihack.AntiHack;
|
||||
import mineplex.core.antihack.logging.AntihackLogger;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
@ -39,23 +43,23 @@ public class BanWaveManager extends MiniPlugin
|
||||
});
|
||||
}
|
||||
|
||||
public void insertBanWaveInfo(Player player, long timeToBan, Class<?> checkClass, String message, int vl, String server)
|
||||
public void insertBanWaveInfo(Player player, long timeToBan, Class<?> checkClass, int vl, String server)
|
||||
{
|
||||
insertBanWaveInfo(player, timeToBan, checkClass, message, vl, server, null);
|
||||
insertBanWaveInfo(player, timeToBan, checkClass, vl, server, null);
|
||||
}
|
||||
|
||||
public void insertBanWaveInfo(Player player, long timeToBan, Class<?> checkClass, String message, int vl, String server, Runnable after)
|
||||
public void insertBanWaveInfo(Player player, long timeToBan, Class<?> checkClass, int vl, String server, Runnable after)
|
||||
{
|
||||
runAsync(() ->
|
||||
{
|
||||
String id = AntiHack.generateId(AntiHack.ID_LENGTH);
|
||||
String newMessage = "[GWEN] [BanWave] " + id + "";
|
||||
|
||||
CoreClient client = require(CoreClientManager.class).Get(player);
|
||||
|
||||
this._repository.insertBanWaveInfo(client.getAccountId(), timeToBan, checkClass.getName(), message, vl, server);
|
||||
this._repository.insertBanWaveInfo(client.getAccountId(), timeToBan, checkClass.getName(), newMessage, vl, server);
|
||||
|
||||
if (after != null)
|
||||
{
|
||||
after.run();
|
||||
}
|
||||
require(AntihackLogger.class).save(player, id, after);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package mineplex.core.antihack.logging;
|
||||
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
|
||||
import com.mineplex.anticheat.checks.Check;
|
||||
import com.mineplex.anticheat.checks.CheckManager;
|
||||
import gnu.trove.map.TIntObjectMap;
|
||||
@ -7,25 +9,34 @@ import gnu.trove.map.TIntObjectMap;
|
||||
import mineplex.core.antihack.ViolationLevels;
|
||||
import mineplex.core.database.MinecraftRepository;
|
||||
import mineplex.serverdata.database.DBPool;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AnticheatDatabase extends MinecraftRepository
|
||||
{
|
||||
//CREATE TABLE IF NOT EXISTS user_anticheat_vls (accountId INT, checkId INT, maxViolations INT, totalAlerts INT, PRIMARY KEY(accountId, checkId));
|
||||
/*
|
||||
CREATE TABLE IF NOT EXISTS anticheat_vl_logs (accountId INT, checkId INT, maxViolations INT, totalAlerts INT, PRIMARY KEY(accountId, checkId));
|
||||
CREATE TABLE IF NOT EXISTS anticheat_ban_metadata (id INT NOT NULL AUTO_INCREMENT, accountId INT, banId CHAR(10) NOT NULL, data MEDIUMTEXT NOT NULL, PRIMARY KEY(id));
|
||||
*/
|
||||
|
||||
public static final String INSERT_INTO_BAN_LOG = "INSERT INTO user_anticheat_bans (accountId, banId, data) VALUES (?, ?, ?)";
|
||||
public static final String INSERT_INTO_BAN_LOG = "INSERT INTO anticheat_vl_logs (accountId, banId, data) VALUES (?, ?, ?)";
|
||||
|
||||
public static final String UPDATE_VIOLATIONS = "INSERT INTO user_anticheat_vls (accountId, checkId, "
|
||||
public static final String INSERT_INTO_METADATA = "INSERT INTO anticheat_ban_metadata (accountId, banId, data) VALUES (?, ?, ?)";
|
||||
|
||||
public static final String UPDATE_VIOLATIONS = "INSERT INTO anticheat_vl_logs (accountId, checkId, "
|
||||
+ "maxViolations, totalAlerts) VALUES (?, ?, ?, ?) ON DUPLICATE KEY"
|
||||
+ " UPDATE maxViolations = VALUES(maxViolations), totalAlerts = VALUES(totalAlerts);";
|
||||
|
||||
private static final String GET_VLS = "SELECT checkId, maxViolations, totalAlerts FROM user_anticheat_vls";
|
||||
private static final String GET_VLS = "SELECT checkId, maxViolations, totalAlerts FROM anticheat_vl_logs";
|
||||
|
||||
private static final String GET_VLS_BY_ACCOUNT_ID = GET_VLS + " WHERE accountId = ?";
|
||||
|
||||
@ -132,6 +143,26 @@ public class AnticheatDatabase extends MinecraftRepository
|
||||
return Optional.ofNullable(levels);
|
||||
}
|
||||
|
||||
public void saveMetadata(int accountId, String id, String base64, Runnable after)
|
||||
{
|
||||
try (Connection connection = getConnection())
|
||||
{
|
||||
PreparedStatement statement = connection.prepareStatement(INSERT_INTO_METADATA);
|
||||
statement.setInt(1, accountId);
|
||||
statement.setString(2, id);
|
||||
statement.setString(3, base64);
|
||||
|
||||
statement.executeUpdate();
|
||||
|
||||
if (after != null)
|
||||
after.run();
|
||||
}
|
||||
catch (SQLException ex)
|
||||
{
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize()
|
||||
{
|
||||
|
@ -5,8 +5,10 @@ import javax.xml.bind.DatatypeConverter;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -14,11 +16,13 @@ import java.util.UUID;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerLoginEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.tukaani.xz.LZMA2Options;
|
||||
import org.tukaani.xz.UnsupportedOptionsException;
|
||||
import org.tukaani.xz.XZ;
|
||||
import org.tukaani.xz.XZOutputStream;
|
||||
|
||||
@ -30,9 +34,15 @@ import com.mineplex.anticheat.checks.Check;
|
||||
import mineplex.core.MiniPlugin;
|
||||
import mineplex.core.ReflectivelyCreateMiniPlugin;
|
||||
import mineplex.core.account.CoreClientManager;
|
||||
import mineplex.core.antihack.AntiHack;
|
||||
import mineplex.core.antihack.ViolationLevels;
|
||||
import mineplex.core.antihack.logging.builtin.ServerInfoMetadata;
|
||||
import mineplex.core.antihack.logging.builtin.ViolationInfoMetadata;
|
||||
import mineplex.core.command.CommandBase;
|
||||
import mineplex.core.common.Rank;
|
||||
import mineplex.core.common.util.F;
|
||||
import mineplex.core.common.util.UtilPlayer;
|
||||
import mineplex.core.common.util.UtilServer;
|
||||
|
||||
import gnu.trove.map.TIntObjectMap;
|
||||
import gnu.trove.map.hash.TIntObjectHashMap;
|
||||
@ -83,6 +93,36 @@ public class AntihackLogger extends MiniPlugin
|
||||
pushQueuedViolationChanges();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void addCommands()
|
||||
{
|
||||
if (UtilServer.isTestServer())
|
||||
{
|
||||
addCommand(new CommandBase<AntihackLogger>(this, Rank.DEVELOPER, "savemetadata")
|
||||
{
|
||||
@Override
|
||||
public void Execute(Player caller, String[] args)
|
||||
{
|
||||
if (caller.getUniqueId().toString().equals("b86b54da-93dd-46f9-be33-27bd92aa36d7"))
|
||||
{
|
||||
if (args.length == 1)
|
||||
{
|
||||
Player player = Bukkit.getPlayer(args[0]);
|
||||
if (player != null)
|
||||
{
|
||||
String id = AntiHack.generateId(AntiHack.ID_LENGTH);
|
||||
save(player, id, () ->
|
||||
{
|
||||
UtilPlayer.message(caller, F.main(getName(), "Saved metadata for " + player.getName() + " with id " + id));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void pushQueuedViolationChanges()
|
||||
{
|
||||
TIntObjectMap<ViolationLevels> ret;
|
||||
@ -200,47 +240,20 @@ public class AntihackLogger extends MiniPlugin
|
||||
runAsync(() -> _db.saveViolationLevels(queue));
|
||||
}
|
||||
|
||||
_metadata.values().forEach(metadata -> metadata.remove(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
public void save(Player player, String id, Runnable after)
|
||||
{
|
||||
runAsync(() ->
|
||||
{
|
||||
JsonObject info = new JsonObject();
|
||||
|
||||
for (AnticheatMetadata anticheatMetadata : _metadata.values())
|
||||
{
|
||||
info.add(anticheatMetadata.getId(), anticheatMetadata.build(event.getPlayer().getUniqueId()));
|
||||
anticheatMetadata.remove(event.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
byte[] b = GSON.toJson(info).getBytes(StandardCharsets.UTF_8);
|
||||
{
|
||||
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||
File out = new File(event.getPlayer().getName() + ".xz");
|
||||
out.createNewFile();
|
||||
FileOutputStream fout = new FileOutputStream(out);
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
XZOutputStream o2 = new XZOutputStream(bout, new LZMA2Options(LZMA2Options.PRESET_MIN), XZ.CHECK_NONE);
|
||||
o2.write(b);
|
||||
o2.close();
|
||||
long end = System.currentTimeMillis();
|
||||
System.out.println("Took " + (end - start) + "ms");
|
||||
|
||||
fout.write(bout.toByteArray());
|
||||
fout.close();
|
||||
|
||||
|
||||
File out1 = new File(event.getPlayer().getName() + ".base64");
|
||||
out1.createNewFile();
|
||||
FileOutputStream fout1 = new FileOutputStream(out1);
|
||||
fout1.write(DatatypeConverter.printBase64Binary(bout.toByteArray()).getBytes(StandardCharsets.UTF_8));
|
||||
fout1.close();
|
||||
}
|
||||
|
||||
File out1 = new File(event.getPlayer().getName() + ".txt");
|
||||
out1.createNewFile();
|
||||
|
||||
FileOutputStream o1 = new FileOutputStream(out1);
|
||||
o1.write(b);
|
||||
o1.close();
|
||||
info.add(anticheatMetadata.getId(), anticheatMetadata.build(player.getUniqueId()));
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
@ -248,6 +261,30 @@ public class AntihackLogger extends MiniPlugin
|
||||
}
|
||||
}
|
||||
|
||||
String str = GSON.toJson(info);
|
||||
byte[] b = str.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
XZOutputStream o2 = new XZOutputStream(bout, new LZMA2Options(LZMA2Options.PRESET_MIN), XZ.CHECK_NONE);
|
||||
o2.write(b);
|
||||
o2.close();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
// Should never happen
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
byte[] compressed = bout.toByteArray();
|
||||
String base64 = Base64.getEncoder().encodeToString(compressed);
|
||||
|
||||
_db.saveMetadata(_clientManager.getAccountId(player), id, base64, after);
|
||||
});
|
||||
}
|
||||
|
||||
public void registerMetadata(AnticheatMetadata metadata)
|
||||
{
|
||||
if (!this._metadata.containsKey(metadata.getId()))
|
||||
|
@ -1,7 +1,7 @@
|
||||
package mineplex.core.punish;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import mineplex.core.account.CoreClient;
|
||||
@ -170,6 +170,11 @@ public class Punish extends MiniPlugin
|
||||
}
|
||||
|
||||
public void AddPunishment(String playerName, final Category category, final String reason, String callerName, final int severity, boolean ban, long duration, final boolean silent)
|
||||
{
|
||||
AddPunishment(playerName, category, reason, callerName, severity, ban, duration, silent, null);
|
||||
}
|
||||
|
||||
public void AddPunishment(String playerName, final Category category, final String reason, String callerName, final int severity, boolean ban, long duration, final boolean silent, Consumer<PunishmentResponse> callback)
|
||||
{
|
||||
Player player = Bukkit.getPlayerExact(playerName);
|
||||
if (player != null)
|
||||
@ -335,6 +340,10 @@ public class Punish extends MiniPlugin
|
||||
});
|
||||
}
|
||||
}
|
||||
if (callback != null)
|
||||
{
|
||||
callback.accept(banResult);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -179,10 +179,13 @@ public class GameInfoMetadata extends AnticheatMetadata
|
||||
|
||||
Map<String, Integer> stats = game.GetStats().get(player);
|
||||
|
||||
if (stats != null)
|
||||
{
|
||||
JsonObject statsObject = new JsonObject();
|
||||
stats.forEach(statsObject::addProperty);
|
||||
|
||||
gameObj.add(KEY_STATS, statsObject);
|
||||
}
|
||||
|
||||
gameObj.addProperty(KEY_WINNER, game.Winner);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user