summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/keuin/blame/Blame.java125
-rw-r--r--src/main/java/com/keuin/blame/EventHandler.java17
-rw-r--r--src/main/java/com/keuin/blame/SubmitWorker.java51
-rw-r--r--src/main/java/com/keuin/blame/command/BlameBlockCommand.java68
-rw-r--r--src/main/java/com/keuin/blame/data/LogEntry.java85
-rw-r--r--src/main/java/com/keuin/blame/data/LogEntryFactory.java21
-rw-r--r--src/main/java/com/keuin/blame/data/WorldPos.java21
-rw-r--r--src/main/java/com/keuin/blame/data/enums/ActionType.java21
-rw-r--r--src/main/java/com/keuin/blame/data/enums/ObjectType.java10
-rw-r--r--src/main/java/com/keuin/blame/data/enums/codec/LogEntryCodec.java104
-rw-r--r--src/main/java/com/keuin/blame/data/enums/codec/LogEntryNames.java14
-rw-r--r--src/main/java/com/keuin/blame/data/enums/codec/WorldPosCodec.java51
-rw-r--r--src/main/java/com/keuin/blame/lookup/AbstractLookupFilter.java13
-rw-r--r--src/main/java/com/keuin/blame/lookup/BlockPosLookupFilter.java22
-rw-r--r--src/main/java/com/keuin/blame/lookup/LookupCallback.java7
-rw-r--r--src/main/java/com/keuin/blame/lookup/LookupFilterWithCallback.java41
-rw-r--r--src/main/java/com/keuin/blame/lookup/LookupManager.java32
-rw-r--r--src/main/java/com/keuin/blame/lookup/LookupWorker.java82
-rw-r--r--src/main/java/com/keuin/blame/lookup/TestableFilter.java14
-rw-r--r--src/main/java/com/keuin/blame/util/DatabaseUtil.java38
-rw-r--r--src/main/java/com/keuin/blame/util/PrettyUtil.java10
-rw-r--r--src/main/java/com/keuin/blame/util/PrintUtil.java4
-rw-r--r--src/main/java/com/keuin/blame/util/UuidUtils.java3
23 files changed, 717 insertions, 137 deletions
diff --git a/src/main/java/com/keuin/blame/Blame.java b/src/main/java/com/keuin/blame/Blame.java
index 1e53a64..6ecfa97 100644
--- a/src/main/java/com/keuin/blame/Blame.java
+++ b/src/main/java/com/keuin/blame/Blame.java
@@ -2,11 +2,20 @@ package com.keuin.blame;
import com.google.gson.Gson;
import com.keuin.blame.adapter.*;
+import com.keuin.blame.command.BlameBlockCommand;
import com.keuin.blame.config.BlameConfig;
+import com.keuin.blame.lookup.LookupManager;
import com.keuin.blame.util.PrintUtil;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.brigadier.arguments.StringArgumentType;
import net.fabricmc.api.ModInitializer;
+import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.player.*;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.command.CommandManager;
+import net.minecraft.server.command.ServerCommandSource;
import java.io.File;
import java.io.IOException;
@@ -17,39 +26,85 @@ import java.util.logging.Logger;
public class Blame implements ModInitializer {
- private final Logger logger = Logger.getLogger(Blame.class.getName());
-
- public static BlameConfig config;
-
- @Override
- public void onInitialize() {
- // This code runs as soon as Minecraft is in a mod-load-ready state.
- // However, some things (like resources) may still be uninitialized.
- // Proceed with mild caution.
-
- String configFileName = "blame.json";
- try {
- // load config
- File configFile = new File(configFileName);
- if (!configFile.exists()) {
- logger.severe(String.format("Failed to read configuration file %s. Blame will be disabled.", configFileName));
- return;
- }
-
- Reader reader = Files.newBufferedReader(configFile.toPath(), StandardCharsets.UTF_8);
- config = (new Gson()).fromJson(reader, BlameConfig.class);
- } catch (IOException exception) {
- logger.severe(String.format("Failed to read configuration file %s: %s. " +
- "Blame will be disabled.", configFileName, exception));
- return;
- }
-
- AttackEntityCallback.EVENT.register(new AttackEntityAdapter(EventHandler.INSTANCE));
- PlayerBlockBreakEvents.AFTER.register(new BreakBlockAdapter(EventHandler.INSTANCE));
- UseBlockCallback.EVENT.register(new UseBlockAdapter(EventHandler.INSTANCE));
- UseEntityCallback.EVENT.register(new UseEntityAdapter(EventHandler.INSTANCE));
- UseItemCallback.EVENT.register(new UseItemAdapter(EventHandler.INSTANCE));
-
- ServerLifecycleEvents.SERVER_STARTED.register(PrintUtil.INSTANCE);
- }
+ private static final Logger logger = Logger.getLogger(Blame.class.getName());
+
+ public static BlameConfig config;
+
+ public static boolean loadConfig() {
+ String configFileName = "blame.json";
+ try {
+ // load config
+ File configFile = new File(configFileName);
+ if (!configFile.exists()) {
+ logger.severe(String.format("Failed to read configuration file %s. Blame will be disabled.", configFileName));
+ return false;
+ }
+
+ Reader reader = Files.newBufferedReader(configFile.toPath(), StandardCharsets.UTF_8);
+ config = (new Gson()).fromJson(reader, BlameConfig.class);
+ } catch (IOException exception) {
+ logger.severe(String.format("Failed to read configuration file %s: %s. " +
+ "Blame will be disabled.", configFileName, exception));
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onInitialize() {
+ // This code runs as soon as Minecraft is in a mod-load-ready state.
+ // However, some things (like resources) may still be uninitialized.
+ // Proceed with mild caution.
+
+ if (!loadConfig())
+ return;
+
+ // hook disable event
+ ServerLifecycleEvents.SERVER_STOPPING.register(new ServerLifecycleEvents.ServerStopping() {
+ @Override
+ public void onServerStopping(MinecraftServer minecraftServer) {
+ logger.info("Stopping LookupManager...");
+ LookupManager.INSTANCE.stop();
+
+ logger.info("Stopping SubmitWorker...");
+ SubmitWorker.INSTANCE.stop();
+ }
+ });
+
+ // hook game events
+ AttackEntityCallback.EVENT.register(new AttackEntityAdapter(EventHandler.INSTANCE));
+ PlayerBlockBreakEvents.AFTER.register(new BreakBlockAdapter(EventHandler.INSTANCE));
+ UseBlockCallback.EVENT.register(new UseBlockAdapter(EventHandler.INSTANCE));
+ UseEntityCallback.EVENT.register(new UseEntityAdapter(EventHandler.INSTANCE));
+ UseItemCallback.EVENT.register(new UseItemAdapter(EventHandler.INSTANCE));
+
+ // initialize PrintUtil
+ ServerLifecycleEvents.SERVER_STARTED.register(PrintUtil.INSTANCE);
+
+ // register
+ CommandRegistrationCallback.EVENT.register(new CommandRegistrationCallback() {
+ @Override
+ public void register(CommandDispatcher<ServerCommandSource> commandDispatcher, boolean b) {
+ commandDispatcher.register(
+ CommandManager.literal("blame")
+ .then(
+ CommandManager.literal("block")
+ .then(
+ CommandManager.argument("x", IntegerArgumentType.integer())
+ .then(
+ CommandManager.argument("y", IntegerArgumentType.integer())
+ .then(
+ CommandManager.argument("z", IntegerArgumentType.integer())
+ .then(
+ CommandManager.argument("world", StringArgumentType.greedyString())
+ .executes(BlameBlockCommand::run)
+ )
+ )
+ )
+ )
+ )
+ );
+ }
+ });
+ }
}
diff --git a/src/main/java/com/keuin/blame/EventHandler.java b/src/main/java/com/keuin/blame/EventHandler.java
index a939759..df10e5b 100644
--- a/src/main/java/com/keuin/blame/EventHandler.java
+++ b/src/main/java/com/keuin/blame/EventHandler.java
@@ -30,8 +30,7 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use
String worldString = MinecraftUtil.worldToString(world);
String blockId = Registry.BLOCK.getId(world.getBlockState(blockHitResult.getBlockPos()).getBlock()).toString();
LogEntry entry = LogEntryFactory.playerWithBlock(
- playerEntity.getUuid(),
- playerEntity.getPos(),
+ playerEntity,
worldString,
blockId,
blockHitResult.getBlockPos(),
@@ -49,8 +48,7 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use
String worldString = MinecraftUtil.worldToString(world);
String blockId = Registry.BLOCK.getId(blockState.getBlock()).toString();
LogEntry entry = LogEntryFactory.playerWithBlock(
- playerEntity.getUuid(),
- playerEntity.getPos(),
+ playerEntity,
worldString,
blockId,
blockPos,
@@ -67,8 +65,7 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use
String entityId = Registry.ENTITY_TYPE.getId(entity.getType()).toString();
String worldString = MinecraftUtil.worldToString(world);
LogEntry entry = LogEntryFactory.playerWithEntity(
- playerEntity.getUuid(),
- playerEntity.getPos(),
+ playerEntity,
worldString,
entityId,
entity.getPos(),
@@ -85,8 +82,7 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use
String entityId = Registry.ENTITY_TYPE.getId(entity.getType()).toString();
String worldString = MinecraftUtil.worldToString(world);
LogEntry entry = LogEntryFactory.playerWithEntity(
- playerEntity.getUuid(),
- playerEntity.getPos(),
+ playerEntity,
worldString,
entityId,
entity.getPos(),
@@ -96,6 +92,7 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use
SubmitWorker.INSTANCE.submit(entry);
PrintUtil.broadcast("use_entity; entity_id=" + entityId);
// TODO: 增加判断,无效的时候也会触发这个事件
+ // TODO: 增加cooldown,过滤掉两个相邻重复事件(时间间隔大概为20ms+)
PrintUtil.broadcast(String.format("player %s use entity %s", playerEntity.getName().getString(), entity));
}
@@ -103,14 +100,14 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use
public void onPlayerUseItem(PlayerEntity playerEntity, World world, Hand hand) {
String itemId = Registry.ITEM.getId(playerEntity.getStackInHand(hand).getItem()).toString();
LogEntry entry = LogEntryFactory.playerWithItem(
- playerEntity.getUuid(),
- playerEntity.getPos(),
+ playerEntity,
MinecraftUtil.worldToString(world),
itemId,
ActionType.ITEM_USE
);
SubmitWorker.INSTANCE.submit(entry);
PrintUtil.broadcast("use_item; item_id=" + itemId);
+ // TODO: 增加cooldown,过滤掉两个相邻重复事件(时间间隔大概为20ms+)
// PrintUtil.broadcast(String.format("player %s use item %s", playerEntity.getName().getString(), playerEntity.getStackInHand(hand)));
}
}
diff --git a/src/main/java/com/keuin/blame/SubmitWorker.java b/src/main/java/com/keuin/blame/SubmitWorker.java
index 1c47aaf..c7334f8 100644
--- a/src/main/java/com/keuin/blame/SubmitWorker.java
+++ b/src/main/java/com/keuin/blame/SubmitWorker.java
@@ -1,22 +1,12 @@
package com.keuin.blame;
-import com.keuin.blame.config.MongoConfig;
import com.keuin.blame.data.LogEntry;
-import com.keuin.blame.data.enums.ActionType;
-import com.keuin.blame.data.enums.ObjectType;
-import com.keuin.blame.data.enums.codec.ActionTypeCodec;
-import com.keuin.blame.data.enums.codec.ObjectTypeCodec;
-import com.keuin.blame.data.enums.transformer.ActionTypeTransformer;
-import com.keuin.blame.data.enums.transformer.ObjectTypeTransformer;
-import com.mongodb.ConnectionString;
-import com.mongodb.MongoClientSettings;
+import com.keuin.blame.util.DatabaseUtil;
+import com.mongodb.MongoClientException;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
-import org.bson.BSON;
-import org.bson.codecs.configuration.CodecRegistries;
-import org.bson.codecs.configuration.CodecRegistry;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
@@ -24,37 +14,16 @@ import java.util.logging.Logger;
public class SubmitWorker {
- public static final SubmitWorker INSTANCE = new SubmitWorker(Blame.config.getMongoConfig());
+ public static final SubmitWorker INSTANCE = new SubmitWorker();
private final Logger logger = Logger.getLogger(SubmitWorker.class.getName());
private final BlockingQueue<LogEntry> queue = new LinkedBlockingDeque<>(4096);
private final Thread thread = new Thread(SubmitWorker.this::run);
private boolean run = true;
- private final MongoConfig mongoConfig;
- private final MongoClientSettings settings;
-
- private SubmitWorker(MongoConfig mongoConfig) {
- if (mongoConfig == null)
- throw new IllegalArgumentException("mongo config cannot be null");
- this.mongoConfig = mongoConfig;
- logger.fine("Connecting to MongoDB server `" + mongoConfig.getAddress()
- + "` with database `" + mongoConfig.getDatabaseName()
- + "` and collection `" + mongoConfig.getLogCollectionName() + "`.");
-
- BSON.addEncodingHook(ActionType.class, new ActionTypeTransformer());
- BSON.addEncodingHook(ObjectType.class, new ObjectTypeTransformer());
-
- CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
- com.mongodb.MongoClient.getDefaultCodecRegistry(),
- CodecRegistries.fromCodecs(new ActionTypeCodec(), new ObjectTypeCodec())
- );
-
- settings = MongoClientSettings.builder()
- .applyConnectionString(new ConnectionString(mongoConfig.getAddress()))
- .codecRegistry(codecRegistry)
- .build();
+ private SubmitWorker() {
+ thread.setUncaughtExceptionHandler((t, e) -> logger.severe(String.format("Exception in thread %s: %s", t.getName(), e)));
thread.start();
}
@@ -70,18 +39,22 @@ public class SubmitWorker {
}
private void run() {
- try (final MongoClient mongoClient = MongoClients.create(settings)) {
+ try (final MongoClient mongoClient = MongoClients.create(DatabaseUtil.CLIENT_SETTINGS)) {
final MongoDatabase db = mongoClient.getDatabase(
- mongoConfig.getDatabaseName()
+ DatabaseUtil.MONGO_CONFIG.getDatabaseName()
);
final MongoCollection<LogEntry> collection = db.getCollection(
- mongoConfig.getLogCollectionName(), LogEntry.class
+ DatabaseUtil.MONGO_CONFIG.getLogCollectionName(), LogEntry.class
);
+ // TODO: 第一个事件触发导致延迟很大
while (this.run) {
LogEntry entry = queue.take();
collection.insertOne(entry);
+ logger.info("Entry inserted.");
}
} catch (InterruptedException ignored) {
+ } catch (MongoClientException exception) {
+ logger.severe("Failed to submit: " + exception + ". Worker is quitting...");
}
}
diff --git a/src/main/java/com/keuin/blame/command/BlameBlockCommand.java b/src/main/java/com/keuin/blame/command/BlameBlockCommand.java
new file mode 100644
index 0000000..63d99ec
--- /dev/null
+++ b/src/main/java/com/keuin/blame/command/BlameBlockCommand.java
@@ -0,0 +1,68 @@
+package com.keuin.blame.command;
+
+import com.keuin.blame.data.LogEntry;
+import com.keuin.blame.data.WorldPos;
+import com.keuin.blame.lookup.BlockPosLookupFilter;
+import com.keuin.blame.lookup.LookupCallback;
+import com.keuin.blame.lookup.LookupManager;
+import com.keuin.blame.util.PrintUtil;
+import com.mojang.brigadier.context.CommandContext;
+import net.minecraft.entity.Entity;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.server.network.ServerPlayerEntity;
+
+import java.util.Iterator;
+
+public class BlameBlockCommand {
+
+ private static final int SUCCESS = 1;
+ private static final int FAILED = -1;
+
+ public static int run(CommandContext<ServerCommandSource> context) {
+ Entity entity = context.getSource().getEntity();
+ if (!(entity instanceof ServerPlayerEntity)) {
+ // can only be executed by player
+ return FAILED;
+ }
+
+ ServerPlayerEntity playerEntity = (ServerPlayerEntity) entity;
+ int x = context.getArgument("x", Integer.class);
+ int y = context.getArgument("y", Integer.class);
+ int z = context.getArgument("z", Integer.class);
+ String world = context.getArgument("world", String.class);
+// String world = MinecraftUtil.worldToString(playerEntity.world);
+ WorldPos blockPos = new WorldPos(world, x, y, z);
+ LookupManager.INSTANCE.lookup(
+ new BlockPosLookupFilter(blockPos),
+ new Callback(context)
+ );
+ return SUCCESS;
+ }
+
+ private static class Callback implements LookupCallback {
+
+ private final CommandContext<ServerCommandSource> context;
+
+ private Callback(CommandContext<ServerCommandSource> context) {
+ this.context = context;
+ }
+
+ @Override
+ public void onLookupFinishes(Iterable<LogEntry> logEntries) {
+ StringBuilder printBuilder = new StringBuilder();
+ Iterator<LogEntry> iterator = logEntries.iterator();
+ int printCount;
+ for (printCount = 0; printCount < 5; ++printCount) {
+ if (!iterator.hasNext())
+ break;
+ LogEntry logEntry = iterator.next();
+ printBuilder.append(logEntry.toString());
+ printBuilder.append("\n")
+ .append("================")
+ .append("\n");
+ }
+ printBuilder.append(String.format("Displayed the most recent %d items.", printCount));
+ PrintUtil.msgInfo(context, printBuilder.toString());
+ }
+ }
+}
diff --git a/src/main/java/com/keuin/blame/data/LogEntry.java b/src/main/java/com/keuin/blame/data/LogEntry.java
index 30037c0..e178dc2 100644
--- a/src/main/java/com/keuin/blame/data/LogEntry.java
+++ b/src/main/java/com/keuin/blame/data/LogEntry.java
@@ -2,7 +2,9 @@ package com.keuin.blame.data;
import com.keuin.blame.data.enums.ActionType;
import com.keuin.blame.data.enums.ObjectType;
-import org.bson.codecs.pojo.annotations.BsonProperty;
+import com.keuin.blame.util.PrettyUtil;
+import com.keuin.blame.util.UuidUtils;
+import net.minecraft.MinecraftVersion;
import java.util.Objects;
import java.util.UUID;
@@ -34,24 +36,34 @@ public class LogEntry {
// }
//}
- @BsonProperty("version")
- private final int version = 1;
- @BsonProperty("timestamp_millis")
- private final long timeMillis;
- @BsonProperty("subject_uuid")
- private final String subjectUUID; // TODO: use Binary instead (BasicDBObject("_id", Binary(session.getIp().getAddress()))) (https://stackoverflow.com/questions/30566905/store-byte-in-mongodb-using-java/40843195)
- @BsonProperty("subject_pos")
- private final WorldPos subjectPos; // TODO: write codec and transformer for this
- @BsonProperty("action_type")
- private final ActionType actionType;
- @BsonProperty("object_type")
- private final ObjectType objectType;
- @BsonProperty("object_id")
- private final String objectId;
- @BsonProperty("object_pos")
- private final WorldPos objectPos;
-
- public LogEntry(long timeMillis, UUID subjectUUID, WorldPos subjectPos, ActionType actionType, ObjectType objectType, String objectId, WorldPos objectPos) {
+ // @BsonProperty("version")
+ private static int version = 1;
+ // @BsonProperty("game_version")
+ private static String gameVersion = MinecraftVersion.field_25319.getName();
+ // @BsonProperty("timestamp_millis")
+ private long timeMillis = 0;
+ // @BsonProperty("subject_id")
+ private String subjectId = "";
+ // @BsonProperty("subject_uuid")
+ private String subjectUUID = UuidUtils.UUID_NULL.toString(); // TODO: use Binary instead (BasicDBObject("_id", Binary(session.getIp().getAddress()))) (https://stackoverflow.com/questions/30566905/store-byte-in-mongodb-using-java/40843195)
+ // @BsonProperty("subject_pos")
+ private WorldPos subjectPos = WorldPos.NULL_POS; // TODO: write codec and transformer for this
+ // @BsonProperty("action_type")
+ private ActionType actionType = ActionType.NULL;
+ // @BsonProperty("object_type")
+ private ObjectType objectType = ObjectType.NULL;
+ // @BsonProperty("object_id")
+ private String objectId = "";
+ // @BsonProperty("object_pos")
+ private WorldPos objectPos = WorldPos.NULL_POS;
+
+ public static final LogEntry EMPTY_ENTRY = new LogEntry();
+
+ protected LogEntry() {
+ }
+
+ public LogEntry(long timeMillis, String subjectId, UUID subjectUUID, WorldPos subjectPos, ActionType actionType, ObjectType objectType, String objectId, WorldPos objectPos) {
+ this.subjectId = subjectId;
// this.subjectUUID = UuidUtils.asBytes(subjectUUID);
// this.subjectUUID
if (subjectUUID == null)
@@ -77,13 +89,21 @@ public class LogEntry {
}
public int getVersion() {
- return 1;
+ return version;
+ }
+
+ public String getGameVersion() {
+ return gameVersion;
}
public long getTimeMillis() {
return timeMillis;
}
+ public String getSubjectId() {
+ return subjectId;
+ }
+
public UUID getSubjectUUID() {
return UUID.fromString(subjectUUID);
}
@@ -113,8 +133,8 @@ public class LogEntry {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LogEntry entry = (LogEntry) o;
- return version == entry.version &&
- timeMillis == entry.timeMillis &&
+ return timeMillis == entry.timeMillis &&
+ Objects.equals(subjectId, entry.subjectId) &&
Objects.equals(subjectUUID, entry.subjectUUID) &&
Objects.equals(subjectPos, entry.subjectPos) &&
actionType == entry.actionType &&
@@ -125,6 +145,25 @@ public class LogEntry {
@Override
public int hashCode() {
- return Objects.hash(version, timeMillis, subjectUUID, subjectPos, actionType, objectType, objectId, objectPos);
+ return Objects.hash(timeMillis, subjectId, subjectUUID, subjectPos, actionType, objectType, objectId, objectPos);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Time: ").append(PrettyUtil.timestampToString(timeMillis)).append("\n");
+ builder.append("Subject: ").append(subjectId).append("{").append(subjectUUID).append("}@")
+ .append(subjectPos.toString())
+ .append("\n");
+ builder.append("Action: ").append(actionType.toString()).append("\n");
+ builder.append("Object: ").append(objectType.toString()).append("[").append(objectId).append("]@")
+ .append(objectPos.toString())
+ .append("\n");
+ builder.append("(entryVersion: ")
+ .append(version)
+ .append(", gameVersion:")
+ .append(gameVersion)
+ .append(")");
+ return builder.toString();
}
}
diff --git a/src/main/java/com/keuin/blame/data/LogEntryFactory.java b/src/main/java/com/keuin/blame/data/LogEntryFactory.java
index 1e2d5d1..bf71b27 100644
--- a/src/main/java/com/keuin/blame/data/LogEntryFactory.java
+++ b/src/main/java/com/keuin/blame/data/LogEntryFactory.java
@@ -2,16 +2,17 @@ package com.keuin.blame.data;
import com.keuin.blame.data.enums.ActionType;
import com.keuin.blame.data.enums.ObjectType;
+import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.math.Vec3i;
-import java.util.UUID;
-
public class LogEntryFactory {
- public static LogEntry playerWithBlock(UUID playerUUID, Vec3d playerPos, String playerWorld, String blockId, Vec3i blockPos, String blockWorld, ActionType actionType) {
+ public static LogEntry playerWithBlock(PlayerEntity player, String playerWorld, String blockId, Vec3i blockPos, String blockWorld, ActionType actionType) {
+ Vec3d playerPos = player.getPos();
return new LogEntry(
System.currentTimeMillis(),
- playerUUID,
+ player.getName().asString(),
+ player.getUuid(),
new WorldPos(playerWorld, playerPos.x, playerPos.y, playerPos.z),
actionType,
ObjectType.BLOCK,
@@ -20,10 +21,12 @@ public class LogEntryFactory {
);
}
- public static LogEntry playerWithEntity(UUID playerUUID, Vec3d playerPos, String playerWorld, String entityId, Vec3d entityPos, String entityWorld, ActionType actionType) {
+ public static LogEntry playerWithEntity(PlayerEntity player, String playerWorld, String entityId, Vec3d entityPos, String entityWorld, ActionType actionType) {
+ Vec3d playerPos = player.getPos();
return new LogEntry(
System.currentTimeMillis(),
- playerUUID,
+ player.getName().asString(),
+ player.getUuid(),
new WorldPos(playerWorld, playerPos.x, playerPos.y, playerPos.z),
actionType,
ObjectType.ENTITY,
@@ -32,10 +35,12 @@ public class LogEntryFactory {
);
}
- public static LogEntry playerWithItem(UUID playerUUID, Vec3d playerPos, String playerWorld, String itemId, ActionType actionType) {
+ public static LogEntry playerWithItem(PlayerEntity player, String playerWorld, String itemId, ActionType actionType) {
+ Vec3d playerPos = player.getPos();
return new LogEntry(
System.currentTimeMillis(),
- playerUUID,
+ player.getName().asString(),
+ player.getUuid(),
new WorldPos(playerWorld, playerPos.x, playerPos.y, playerPos.z),
actionType,
ObjectType.ENTITY,
diff --git a/src/main/java/com/keuin/blame/data/WorldPos.java b/src/main/java/com/keuin/blame/data/WorldPos.java
index aeda093..6783fea 100644
--- a/src/main/java/com/keuin/blame/data/WorldPos.java
+++ b/src/main/java/com/keuin/blame/data/WorldPos.java
@@ -6,10 +6,10 @@ public class WorldPos {
// immutable
- private final String world;
- private final double x;
- private final double y;
- private final double z;
+ private String world = "";
+ private double x = 0;
+ private double y = 0;
+ private double z = 0;
public static final WorldPos NULL_POS = new WorldPos("", 0, 0, 0);
@@ -20,6 +20,7 @@ public class WorldPos {
this.x = x;
this.y = y;
this.z = z;
+// System.out.printf("%s, %f, %f, %f%n", world, x, y, z);
}
public String getWorld() {
@@ -53,4 +54,16 @@ public class WorldPos {
public int hashCode() {
return Objects.hash(world, x, y, z);
}
+
+ @Override
+ public String toString() {
+ return String.format("(%s, %s, %s -> %s)", prettyDouble(x), prettyDouble(y), prettyDouble(z), world);
+ }
+
+ private String prettyDouble(double d) {
+ if ((d - (int) d) < 1e-3)
+ return String.valueOf((int) d);
+ else
+ return String.format("%.3f", d);
+ }
}
diff --git a/src/main/java/com/keuin/blame/data/enums/ActionType.java b/src/main/java/com/keuin/blame/data/enums/ActionType.java
index dcf2aa7..58c4a56 100644
--- a/src/main/java/com/keuin/blame/data/enums/ActionType.java
+++ b/src/main/java/com/keuin/blame/data/enums/ActionType.java
@@ -2,17 +2,20 @@ package com.keuin.blame.data.enums;
public enum ActionType implements IntegerEnum {
- BLOCK_BREAK(1),
- BLOCK_PLACE(2),
- BLOCK_USE(3),
- ENTITY_ATTACK(4),
- ENTITY_USE(5),
- ITEM_USE(6);
+ NULL(0, "NULL"),
+ BLOCK_BREAK(1, "BREAK_BLOCK"),
+ BLOCK_PLACE(2, "PLACE_BLOCK"),
+ BLOCK_USE(3, "USE_BLOCK"),
+ ENTITY_ATTACK(4, "ATTACK_ENTITY"),
+ ENTITY_USE(5, "USE_ENTITY"),
+ ITEM_USE(6, "USE_ITEM");
private final int value;
+ private final String typeString;
- ActionType(int value) {
+ ActionType(int value, String typeString) {
this.value = value;
+ this.typeString = typeString;
}
public static ActionType parseInt(int value) {
@@ -29,9 +32,7 @@ public enum ActionType implements IntegerEnum {
@Override
public String toString() {
- return "ActionType{" +
- "value=" + value +
- '}';
+ return typeString;
}
}
diff --git a/src/main/java/com/keuin/blame/data/enums/ObjectType.java b/src/main/java/com/keuin/blame/data/enums/ObjectType.java
index b3b1d67..4b9312b 100644
--- a/src/main/java/com/keuin/blame/data/enums/ObjectType.java
+++ b/src/main/java/com/keuin/blame/data/enums/ObjectType.java
@@ -2,12 +2,14 @@ package com.keuin.blame.data.enums;
public enum ObjectType implements IntegerEnum {
- BLOCK(1), ENTITY(2);
+ NULL(0, "NULL"), BLOCK(1, "BLOCK"), ENTITY(2, "ENTITY");
private final int value;
+ private final String typeString;
- ObjectType(int value) {
+ ObjectType(int value, String typeString) {
this.value = value;
+ this.typeString = typeString;
}
public static ObjectType parseInt(int value) {
@@ -24,9 +26,7 @@ public enum ObjectType implements IntegerEnum {
@Override
public String toString() {
- return "ObjectType{" +
- "value=" + value +
- '}';
+ return typeString;
}
}
diff --git a/src/main/java/com/keuin/blame/data/enums/codec/LogEntryCodec.java b/src/main/java/com/keuin/blame/data/enums/codec/LogEntryCodec.java
new file mode 100644
index 0000000..4bb61a3
--- /dev/null
+++ b/src/main/java/com/keuin/blame/data/enums/codec/LogEntryCodec.java
@@ -0,0 +1,104 @@
+package com.keuin.blame.data.enums.codec;
+
+import com.keuin.blame.data.LogEntry;
+import com.keuin.blame.data.WorldPos;
+import com.keuin.blame.data.enums.ActionType;
+import com.keuin.blame.data.enums.ObjectType;
+import org.bson.*;
+import org.bson.codecs.*;
+import org.bson.codecs.configuration.CodecRegistries;
+import org.bson.codecs.configuration.CodecRegistry;
+import org.bson.codecs.pojo.PojoCodecProvider;
+
+import java.util.Objects;
+import java.util.UUID;
+
+import static com.keuin.blame.data.enums.codec.LogEntryNames.*;
+import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
+
+public class LogEntryCodec implements CollectibleCodec<LogEntry> {
+
+ private final Codec<Document> documentCodec;
+
+ public LogEntryCodec() {
+ CodecRegistry CODEC_REGISTRY = CodecRegistries.fromRegistries(
+ com.mongodb.MongoClient.getDefaultCodecRegistry(),
+ CodecRegistries.fromCodecs(
+ new ActionTypeCodec(),
+ new ObjectTypeCodec(),
+ new WorldPosCodec()
+ ),
+ fromProviders(PojoCodecProvider.builder().automatic(true).build())
+ );
+ documentCodec = new DocumentCodec(
+ CODEC_REGISTRY
+ );
+ }
+
+ public LogEntryCodec(Codec<Document> documentCodec) {
+ this.documentCodec = documentCodec;
+ }
+
+
+ @Override
+ public LogEntry decode(BsonReader reader, DecoderContext decoderContext) {
+ Document document = documentCodec.decode(reader, decoderContext);
+ Integer entryVersion = document.getInteger("version");
+ if (entryVersion == null)
+ return LogEntry.EMPTY_ENTRY;
+ if (Objects.equals(LogEntry.EMPTY_ENTRY.getVersion(), entryVersion)) {
+ LogEntry entry = new LogEntry(
+ document.getLong(TIMESTAMP_MILLIS),
+ document.getString(SUBJECT_ID),
+ document.get(SUBJECT_UUID, UUID.class),
+ WorldPos.NULL_POS,
+// document.get(SUBJECT_POS, WorldPos.class),
+ ActionType.parseInt(document.getInteger(ACTION_TYPE)),
+ ObjectType.parseInt(document.getInteger(OBJECT_TYPE)),
+ document.getString(OBJECT_ID),
+ WorldPos.NULL_POS
+// document.get(OBJECT_POS, WorldPos.class)
+ );
+ return entry;
+ }
+ throw new RuntimeException(String.format("unsupported LogEntry version: %d. Perhaps your Blame is too old.", entryVersion));
+ }
+
+ @Override
+ public void encode(BsonWriter writer, LogEntry value, EncoderContext encoderContext) {
+ Document document = new Document();
+
+ document.put(VERSION, value.getVersion());
+ document.put(GAME_VERSION, value.getGameVersion());
+ document.put(TIMESTAMP_MILLIS, value.getTimeMillis());
+ document.put(SUBJECT_ID, value.getSubjectId());
+ document.put(SUBJECT_UUID, value.getSubjectUUID());
+// document.put(SUBJECT_POS, value.getSubjectPos());
+ document.put(ACTION_TYPE, value.getActionType().getValue());
+ document.put(OBJECT_TYPE, value.getObjectType().getValue());
+ document.put(OBJECT_ID, value.getObjectId());
+// document.put(OBJECT_POS, value.getObjectPos());
+
+ documentCodec.encode(writer, document, encoderContext);
+ }
+
+ @Override
+ public Class<LogEntry> getEncoderClass() {
+ return LogEntry.class;
+ }
+
+ @Override
+ public LogEntry generateIdIfAbsentFromDocument(LogEntry document) {
+ return document;
+ }
+
+ @Override
+ public boolean documentHasId(LogEntry document) {
+ return document.getObjectId() != null;
+ }
+
+ @Override
+ public BsonValue getDocumentId(LogEntry document) {
+ return new BsonString(document.getObjectId());
+ }
+}
diff --git a/src/main/java/com/keuin/blame/data/enums/codec/LogEntryNames.java b/src/main/java/com/keuin/blame/data/enums/codec/LogEntryNames.java
new file mode 100644
index 0000000..94edf4a
--- /dev/null
+++ b/src/main/java/com/keuin/blame/data/enums/codec/LogEntryNames.java
@@ -0,0 +1,14 @@
+package com.keuin.blame.data.enums.codec;
+
+public class LogEntryNames {
+ public static final String VERSION = "version";
+ public static final String GAME_VERSION = "game_version";
+ public static final String TIMESTAMP_MILLIS = "timestamp_millis";
+ public static final String SUBJECT_ID = "subject_id";
+ public static final String SUBJECT_UUID = "subject_uuid";
+ public static final String SUBJECT_POS = "subject_pos";
+ public static final String ACTION_TYPE = "action_type";
+ public static final String OBJECT_TYPE = "object_type";
+ public static final String OBJECT_ID = "object_id";
+ public static final String OBJECT_POS = "object_pos";
+}
diff --git a/src/main/java/com/keuin/blame/data/enums/codec/WorldPosCodec.java b/src/main/java/com/keuin/blame/data/enums/codec/WorldPosCodec.java
new file mode 100644
index 0000000..1f0fa91
--- /dev/null
+++ b/src/main/java/com/keuin/blame/data/enums/codec/WorldPosCodec.java
@@ -0,0 +1,51 @@
+package com.keuin.blame.data.enums.codec;
+
+import com.keuin.blame.data.WorldPos;
+import org.bson.BsonReader;
+import org.bson.BsonWriter;
+import org.bson.Document;
+import org.bson.codecs.Codec;
+import org.bson.codecs.DecoderContext;
+import org.bson.codecs.DocumentCodec;
+import org.bson.codecs.EncoderContext;
+
+import java.util.Optional;
+
+public class WorldPosCodec implements Codec<WorldPos> {
+
+ private final Codec<Document> documentCodec;
+
+ public WorldPosCodec() {
+ documentCodec = new DocumentCodec();
+ }
+
+ public WorldPosCodec(Codec<Document> documentCodec) {
+ this.documentCodec = documentCodec;
+ }
+
+ @Override
+ public WorldPos decode(BsonReader reader, DecoderContext decoderContext) {
+ Document document = documentCodec.decode(reader, decoderContext);
+ return new WorldPos(
+ document.getString("world"),
+ document.getDouble("x"),
+ document.getDouble("y"),
+ document.getDouble("z")
+ );
+ }
+
+ @Override
+ public void encode(BsonWriter writer, WorldPos value, EncoderContext encoderContext) {
+ Document document = new Document();
+ Optional.ofNullable(value.getWorld()).ifPresent(world -> document.put("world", world));
+ document.put("x", value.getX());
+ document.put("y", value.getY());
+ document.put("z", value.getZ());
+ documentCodec.encode(writer, document, encoderContext);
+ }
+
+ @Override
+ public Class<WorldPos> getEncoderClass() {
+ return WorldPos.class;
+ }
+}
diff --git a/src/main/java/com/keuin/blame/lookup/AbstractLookupFilter.java b/src/main/java/com/keuin/blame/lookup/AbstractLookupFilter.java
new file mode 100644
index 0000000..f8774e0
--- /dev/null
+++ b/src/main/java/com/keuin/blame/lookup/AbstractLookupFilter.java
@@ -0,0 +1,13 @@
+package com.keuin.blame.lookup;
+
+import com.keuin.blame.data.LogEntry;
+import com.mongodb.client.FindIterable;
+
+public abstract class AbstractLookupFilter {
+ // immutable
+
+ AbstractLookupFilter() {
+ }
+
+ abstract FindIterable<LogEntry> find(FindIterable<LogEntry> iterable);
+} \ No newline at end of file
diff --git a/src/main/java/com/keuin/blame/lookup/BlockPosLookupFilter.java b/src/main/java/com/keuin/blame/lookup/BlockPosLookupFilter.java
new file mode 100644
index 0000000..67a564f
--- /dev/null
+++ b/src/main/java/com/keuin/blame/lookup/BlockPosLookupFilter.java
@@ -0,0 +1,22 @@
+package com.keuin.blame.lookup;
+
+import com.keuin.blame.data.LogEntry;
+import com.keuin.blame.data.WorldPos;
+import com.mongodb.client.FindIterable;
+import com.mongodb.client.model.Filters;
+
+public class BlockPosLookupFilter extends AbstractLookupFilter {
+ private final WorldPos blockPos;
+
+ public BlockPosLookupFilter(WorldPos blockPos) {
+ this.blockPos = blockPos;
+ }
+
+ @Override
+ FindIterable<LogEntry> find(FindIterable<LogEntry> iterable) {
+ return iterable.filter(Filters.and(
+ Filters.eq("version", 1),
+ Filters.eq("object_pos", blockPos)
+ ));
+ }
+}
diff --git a/src/main/java/com/keuin/blame/lookup/LookupCallback.java b/src/main/java/com/keuin/blame/lookup/LookupCallback.java
new file mode 100644
index 0000000..eba954c
--- /dev/null
+++ b/src/main/java/com/keuin/blame/lookup/LookupCallback.java
@@ -0,0 +1,7 @@
+package com.keuin.blame.lookup;
+
+import com.keuin.blame.data.LogEntry;
+
+public interface LookupCallback {
+ void onLookupFinishes(Iterable<LogEntry> logEntries);
+}
diff --git a/src/main/java/com/keuin/blame/lookup/LookupFilterWithCallback.java b/src/main/java/com/keuin/blame/lookup/LookupFilterWithCallback.java
new file mode 100644
index 0000000..cd6743c
--- /dev/null
+++ b/src/main/java/com/keuin/blame/lookup/LookupFilterWithCallback.java
@@ -0,0 +1,41 @@
+package com.keuin.blame.lookup;
+
+import java.util.Objects;
+
+class LookupFilterWithCallback {
+
+ private final LookupCallback callback;
+ private final AbstractLookupFilter filter;
+
+ LookupFilterWithCallback(LookupCallback callback, AbstractLookupFilter filter) {
+ if (callback == null)
+ throw new IllegalArgumentException("callback cannot be null");
+ if (filter == null)
+ throw new IllegalArgumentException("filter cannot be null");
+ this.callback = callback;
+ this.filter = filter;
+ }
+
+ public LookupCallback getCallback() {
+ return callback;
+ }
+
+ public AbstractLookupFilter getFilter() {
+ return filter;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LookupFilterWithCallback that = (LookupFilterWithCallback) o;
+ return callback.equals(that.callback) &&
+ filter.equals(that.filter);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(callback, filter);
+ }
+
+}
diff --git a/src/main/java/com/keuin/blame/lookup/LookupManager.java b/src/main/java/com/keuin/blame/lookup/LookupManager.java
new file mode 100644
index 0000000..2ffa86b
--- /dev/null
+++ b/src/main/java/com/keuin/blame/lookup/LookupManager.java
@@ -0,0 +1,32 @@
+package com.keuin.blame.lookup;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingDeque;
+
+public class LookupManager {
+
+ public static final LookupManager INSTANCE = new LookupManager();
+
+ private final BlockingQueue<LookupFilterWithCallback> queue = new LinkedBlockingDeque<>();
+ private final List<LookupWorker> workers = new ArrayList<>();
+
+ private LookupManager() {
+ // initialize workers
+ for (int i = 0; i < 10; ++i) {
+ LookupWorker worker = new LookupWorker(i, queue);
+ worker.start();
+ workers.add(worker);
+ }
+ }
+
+ public void stop() {
+ workers.forEach(LookupWorker::disable);
+ }
+
+ public void lookup(AbstractLookupFilter filter, LookupCallback callback) {
+ queue.add(new LookupFilterWithCallback(callback, filter));
+ }
+
+}
diff --git a/src/main/java/com/keuin/blame/lookup/LookupWorker.java b/src/main/java/com/keuin/blame/lookup/LookupWorker.java
new file mode 100644
index 0000000..a8e5b7d
--- /dev/null
+++ b/src/main/java/com/keuin/blame/lookup/LookupWorker.java
@@ -0,0 +1,82 @@
+package com.keuin.blame.lookup;
+
+import com.keuin.blame.Blame;
+import com.keuin.blame.config.MongoConfig;
+import com.keuin.blame.data.LogEntry;
+import com.keuin.blame.data.enums.codec.ActionTypeCodec;
+import com.keuin.blame.data.enums.codec.ObjectTypeCodec;
+import com.keuin.blame.data.enums.codec.WorldPosCodec;
+import com.keuin.blame.util.DatabaseUtil;
+import com.mongodb.ConnectionString;
+import com.mongodb.MongoClientSettings;
+import com.mongodb.client.*;
+import org.bson.codecs.configuration.CodecRegistries;
+import org.bson.codecs.configuration.CodecRegistry;
+import org.bson.codecs.pojo.PojoCodecProvider;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Logger;
+
+import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
+
+public class LookupWorker extends Thread {
+
+ private final Logger logger;
+ private final BlockingQueue<LookupFilterWithCallback> queue;
+ private boolean running = true;
+
+ private static final MongoConfig MONGO_CONFIG = Blame.config.getMongoConfig();
+ private static final CodecRegistry CODEC_REGISTRY = CodecRegistries.fromRegistries(
+ com.mongodb.MongoClient.getDefaultCodecRegistry(),
+ CodecRegistries.fromCodecs(
+ new ActionTypeCodec(),
+ new ObjectTypeCodec(),
+ new WorldPosCodec()
+// new LogEntryCodec()
+ ),
+ fromProviders(PojoCodecProvider.builder().automatic(true).build())
+ );
+ private static final MongoClientSettings CLIENT_SETTINGS = MongoClientSettings.builder()
+ .applyConnectionString(new ConnectionString(MONGO_CONFIG.getAddress()))
+ .codecRegistry(CODEC_REGISTRY)
+ .build();
+
+ public LookupWorker(int id, BlockingQueue<LookupFilterWithCallback> queue) {
+ this.queue = queue;
+ this.logger = Logger.getLogger(String.format("LookupWorker-%d", id));
+ }
+
+ public void disable() {
+ interrupt();
+ running = false;
+ }
+
+ @Override
+ public void run() {
+ try (final MongoClient mongoClient = MongoClients.create(CLIENT_SETTINGS)) {
+ final MongoDatabase db = mongoClient.getDatabase(
+ DatabaseUtil.MONGO_CONFIG.getDatabaseName()
+ );
+ final MongoCollection<LogEntry> collection = db.getCollection(
+ DatabaseUtil.MONGO_CONFIG.getLogCollectionName(), LogEntry.class
+ );
+ long time;
+ while (running) {
+ LookupFilterWithCallback item = queue.take();
+ LookupCallback callback = item.getCallback();
+ AbstractLookupFilter filter = item.getFilter();
+
+ time = System.currentTimeMillis();
+// FindIterable<LogEntry> find = filter.find(
+// collection.find().sort(Sorts.descending("timestamp_millis"))
+// );
+ FindIterable<LogEntry> find = collection.find();//.sort(Sorts.descending("timestamp_millis"));
+ time = System.currentTimeMillis() - time;
+ logger.info(String.format("Lookup finished in %d ms.", time));
+ callback.onLookupFinishes(find);
+ }
+ } catch (InterruptedException e) {
+ logger.info("Interrupted. Quitting...");
+ }
+ }
+}
diff --git a/src/main/java/com/keuin/blame/lookup/TestableFilter.java b/src/main/java/com/keuin/blame/lookup/TestableFilter.java
new file mode 100644
index 0000000..8160234
--- /dev/null
+++ b/src/main/java/com/keuin/blame/lookup/TestableFilter.java
@@ -0,0 +1,14 @@
+package com.keuin.blame.lookup;
+
+import com.keuin.blame.data.LogEntry;
+import com.keuin.blame.data.enums.ActionType;
+import com.keuin.blame.data.enums.codec.LogEntryNames;
+import com.mongodb.client.FindIterable;
+import com.mongodb.client.model.Filters;
+
+public class TestableFilter extends AbstractLookupFilter {
+ @Override
+ protected FindIterable<LogEntry> find(FindIterable<LogEntry> iterable) {
+ return iterable.filter(Filters.eq(LogEntryNames.ACTION_TYPE, ActionType.NULL.getValue()));
+ }
+}
diff --git a/src/main/java/com/keuin/blame/util/DatabaseUtil.java b/src/main/java/com/keuin/blame/util/DatabaseUtil.java
new file mode 100644
index 0000000..4b1d3d4
--- /dev/null
+++ b/src/main/java/com/keuin/blame/util/DatabaseUtil.java
@@ -0,0 +1,38 @@
+package com.keuin.blame.util;
+
+import com.keuin.blame.Blame;
+import com.keuin.blame.config.MongoConfig;
+import com.keuin.blame.data.enums.codec.ActionTypeCodec;
+import com.keuin.blame.data.enums.codec.ObjectTypeCodec;
+import com.keuin.blame.data.enums.codec.WorldPosCodec;
+import com.mongodb.ConnectionString;
+import com.mongodb.MongoClientSettings;
+import org.bson.codecs.configuration.CodecRegistries;
+import org.bson.codecs.configuration.CodecRegistry;
+import org.bson.codecs.pojo.PojoCodecProvider;
+
+import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
+
+public class DatabaseUtil {
+
+// static {
+// BSON.addEncodingHook(ActionType.class, new ActionTypeTransformer());
+// BSON.addEncodingHook(ObjectType.class, new ObjectTypeTransformer());
+// }
+
+ public static final MongoConfig MONGO_CONFIG = Blame.config.getMongoConfig();
+ public static final CodecRegistry CODEC_REGISTRY = CodecRegistries.fromRegistries(
+ com.mongodb.MongoClient.getDefaultCodecRegistry(),
+ CodecRegistries.fromCodecs(
+ new ActionTypeCodec(),
+ new ObjectTypeCodec(),
+ new WorldPosCodec()
+// new LogEntryCodec()
+ ),
+ fromProviders(PojoCodecProvider.builder().automatic(true).build())
+ );
+ public static final MongoClientSettings CLIENT_SETTINGS = MongoClientSettings.builder()
+ .applyConnectionString(new ConnectionString(MONGO_CONFIG.getAddress()))
+ .codecRegistry(CODEC_REGISTRY)
+ .build();
+}
diff --git a/src/main/java/com/keuin/blame/util/PrettyUtil.java b/src/main/java/com/keuin/blame/util/PrettyUtil.java
new file mode 100644
index 0000000..0a53b6e
--- /dev/null
+++ b/src/main/java/com/keuin/blame/util/PrettyUtil.java
@@ -0,0 +1,10 @@
+package com.keuin.blame.util;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class PrettyUtil {
+ public static String timestampToString(long timeMillis) {
+ return (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(new Date(timeMillis));
+ }
+}
diff --git a/src/main/java/com/keuin/blame/util/PrintUtil.java b/src/main/java/com/keuin/blame/util/PrintUtil.java
index fdd1ea0..75b73d4 100644
--- a/src/main/java/com/keuin/blame/util/PrintUtil.java
+++ b/src/main/java/com/keuin/blame/util/PrintUtil.java
@@ -12,7 +12,7 @@ import net.minecraft.util.Formatting;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import java.util.UUID;
+import static com.keuin.blame.util.UuidUtils.UUID_NULL;
public final class PrintUtil implements ServerLifecycleEvents.ServerStarted {
@@ -30,8 +30,6 @@ public final class PrintUtil implements ServerLifecycleEvents.ServerStarted {
private static final String LOG_HEADING = "[Blame]";
private static PlayerManager playerManager = null;
- private static final UUID UUID_NULL = UUID.fromString("00000000-0000-0000-0000-000000000000");
-
// Used to handle server started event, to get player manager
// You should put `ServerLifecycleEvents.SERVER_STARTED.register(PrintUtil.INSTANCE);` in the plugin init method
public static final PrintUtil INSTANCE = new PrintUtil();
diff --git a/src/main/java/com/keuin/blame/util/UuidUtils.java b/src/main/java/com/keuin/blame/util/UuidUtils.java
index b282129..fa6dce1 100644
--- a/src/main/java/com/keuin/blame/util/UuidUtils.java
+++ b/src/main/java/com/keuin/blame/util/UuidUtils.java
@@ -4,6 +4,9 @@ import java.nio.ByteBuffer;
import java.util.UUID;
public class UuidUtils {
+
+ public static final UUID UUID_NULL = UUID.fromString("00000000-0000-0000-0000-000000000000");
+
public static UUID asUuid(byte[] bytes) {
ByteBuffer bb = ByteBuffer.wrap(bytes);
long firstLong = bb.getLong();