Skip to content

Commit 5121d37

Browse files
authored
feat: preserve custom item and mob audit identities (#21)
1 parent aae186f commit 5121d37

16 files changed

Lines changed: 527 additions & 6 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ This repository is TF-Minecraft's fork of CoreProtect, developed by [PlayPro](ht
1313
- **Targeted recovery** — roll back selected changes and restore previously rolled-back activity.
1414
- **Recovery previews** — review the effect of a rollback or restore before applying it.
1515
- **Broad world coverage** — logging includes player block changes, container contents, explosions, fire, liquids, many entity interactions, skills players use and where they teleport them, and where each online player is once a minute.
16+
- **Custom item and mob labels** — item and chest lookups show saved MMOItems names and IDs; kill lookups show recorded mob names and, for new MythicMobs deaths, the internal mob ID.
1617
- **Staff investigation tools** — paginated lookups, inventory history, and WorldEdit selection support help investigate incidents of different sizes.
1718

1819
CoreProtect supports both investigation and recovery; the recorded history available to staff depends on the server's logging settings.
1920

21+
Named ordinary items are marked as renamed; a display name alone does not prove
22+
MMOItems or MythicMobs identity. These labels also apply to historical records
23+
where the required metadata was saved. MythicLib and MythicMobs integrations are
24+
optional, and the vanilla type remains the fallback. Existing material filters,
25+
aggregate counts and rollback payloads keep their original meaning; custom-ID
26+
search and custom mob restoration are not added by this change.
27+
2028
## Documentation
2129

2230
[Project documentation](https://github.com/TF-Minecraft/Docs/blob/main/projects/CoreProtect/README.md)

‎src/main/java/net/tfminecraft/coreprotect/command/lookup/StandardLookupThread.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
import net.tfminecraft.coreprotect.listener.channel.PluginChannelListener;
3737
import net.tfminecraft.coreprotect.model.action.EntityActionFilter;
3838
import net.tfminecraft.coreprotect.model.action.LockChange;
39+
import net.tfminecraft.coreprotect.model.action.MythicIdentity;
40+
import net.tfminecraft.coreprotect.database.statement.EntityStatement;
41+
import net.tfminecraft.coreprotect.utility.AuditLabels;
3942
import net.tfminecraft.coreprotect.model.action.LookupActions;
4043
import net.tfminecraft.coreprotect.model.action.SessionActions;
4144
import net.tfminecraft.coreprotect.model.entity.EntitySpawnRecord;
@@ -321,6 +324,17 @@ else if (pageStart < rows) {
321324
? Lookup.performPartialLookup(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, entityActionFilter, messageFilters, entityContext, finalLocation, radius, rowData, timeStart, timeEnd, (int) pageStart, displayResults, restrict_world, true, entityContainerId, rollbackState)
322325
: lookupPage.getRows();
323326

327+
Set<Integer> killedEntities = new HashSet<>();
328+
for (String[] row : lookupList) {
329+
if (row.length > 11 && row[7] != null && row[10] != null
330+
&& Integer.parseInt(row[7]) == LookupActions.ENTITY_KILL
331+
&& Integer.parseInt(row[10]) == -1 && Integer.parseInt(row[5]) != 0
332+
&& (row.length <= 13 || row[13] == null
333+
|| Integer.parseInt(row[13]) != InventorySources.ENTITY_INTERACTION)) {
334+
killedEntities.add(Integer.parseInt(row[6]));
335+
}
336+
}
337+
Map<Integer, List<Object>> killedEntityData = EntityStatement.loadData(connection, killedEntities);
324338
Map<Integer, EntitySpawnRecord> entitySpawnRecords = Collections.emptyMap();
325339
Map<UUID, Location> loadedEntityLocations = Collections.emptyMap();
326340
Set<Integer> entitySpawnRowIds = new HashSet<>();
@@ -462,6 +476,7 @@ else if (LookupActions.isInventoryLookup(actions)) {
462476
Material blockType = ItemUtils.itemFilter(MaterialUtils.getType(dtype), (Integer.parseInt(data[13]) == 0));
463477
String dname = StringUtils.nameFilter(blockType.name().toLowerCase(Locale.ROOT), ddata);
464478
byte[] metadata = data[11] == null ? null : data[11].getBytes(StandardCharsets.ISO_8859_1);
479+
dname = AuditLabels.item(metadata, dtype, amount, dname);
465480
String tooltip = ItemUtils.getEnchantments(metadata, dtype, amount);
466481
Integer itemId = ItemUtils.makeGivableItem(ItemUtils.getItemStack(metadata, dtype, amount));
467482

@@ -566,12 +581,18 @@ else if ((daction == LookupActions.ENTITY_KILL || daction == LookupActions.ENTIT
566581

567582
// Functions.sendMessage(player2, timeago+" " + ChatColors.WHITE + "- " + ChatColors.DARK_AQUA+rbd+""+dplayer+" " + ChatColors.WHITE+rbd+""+a+" " + ChatColors.DARK_AQUA+rbd+"#"+dtype+ChatColors.WHITE + ". " + ChatColors.GREY + "(x"+x+"/y"+y+"/z"+z+")");
568583

584+
if (!entityInteraction && daction == LookupActions.ENTITY_KILL && amount == -1 && !isPlayer) {
585+
MythicIdentity identity = MythicIdentity.fromLookupString(data[11]);
586+
dname = AuditLabels.mob(dname, killedEntityData.get(ddata), identity == null ? null : identity.id());
587+
}
588+
569589
Phrase phrase = Phrase.LOOKUP_BLOCK;
570590
String selector = Selector.FIRST;
571591
String action = "a:block";
572592
LockChange lockChange = null;
573593
if (actions.contains(LookupActions.CONTAINER) || actions.contains(5) || actions.contains(LookupActions.ITEM) || amount > -1) {
574594
byte[] metadata = data[11] == null ? null : data[11].getBytes(StandardCharsets.ISO_8859_1);
595+
dname = AuditLabels.item(metadata, dtype, amount, dname);
575596
String tooltip = ItemUtils.getEnchantments(metadata, dtype, amount);
576597
Integer itemId = ItemUtils.makeGivableItem(ItemUtils.getItemStack(metadata, dtype, amount));
577598

‎src/main/java/net/tfminecraft/coreprotect/consumer/Queue.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,12 @@ protected static void queueEntityInsert(int id, String name) {
410410
// These auxiliary queues still hold distinct legacy payloads consumed by the matching processors.
411411
@SuppressWarnings("deprecation")
412412
protected static void queueEntityKill(String user, Location location, List<Object> data, EntityType type) {
413-
queueStandardData(new Object[] { null, Process.ENTITY_KILL, null, 0, null, 0, 0 }, new String[] { user, null }, new Object[] { getBlockLocation(location), type, null }, false, Consumer.consumerObjectList, data, Consumer.reserveConsumer());
413+
queueEntityKill(user, location, data, type, null);
414+
}
415+
416+
@SuppressWarnings("deprecation")
417+
protected static void queueEntityKill(String user, Location location, List<Object> data, EntityType type, net.tfminecraft.coreprotect.model.action.MythicIdentity identity) {
418+
queueStandardData(new Object[] { null, Process.ENTITY_KILL, null, 0, null, 0, 0 }, new String[] { user, null }, new Object[] { getBlockLocation(location), type, identity }, false, Consumer.consumerObjectList, data, Consumer.reserveConsumer());
414419
}
415420

416421
protected static void queueEntitySpawn(String user, BlockState block, EntityType type, int data) {

‎src/main/java/net/tfminecraft/coreprotect/consumer/process/EntityKillProcess.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.bukkit.entity.EntityType;
1010

1111
import net.tfminecraft.coreprotect.consumer.Consumer;
12+
import net.tfminecraft.coreprotect.model.action.MythicIdentity;
1213
import net.tfminecraft.coreprotect.database.logger.EntityKillLogger;
1314
import net.tfminecraft.coreprotect.utility.EntityUtils;
1415

@@ -29,7 +30,8 @@ static void process(ConsumerWriteBatch preparedStmt, ConsumerWriteBatch prepared
2930
if (objectLists != null && objectLists.get(id) != null) {
3031
List<Object> objectList = objectLists.get(id);
3132
int entityId = EntityUtils.getEntityId(type);
32-
EntityKillLogger.log(preparedStmt, preparedStmtEntities, preparedStmtEntityKillLinks, batchCount, user, location, objectList, entityId);
33+
EntityKillLogger.log(preparedStmt, preparedStmtEntities, preparedStmtEntityKillLinks, batchCount, user, location, objectList, entityId,
34+
values.length > 2 && values[2] instanceof MythicIdentity identity ? identity : null);
3335
}
3436
}
3537
}

‎src/main/java/net/tfminecraft/coreprotect/database/logger/EntityKillLogger.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import net.tfminecraft.coreprotect.database.statement.UserStatement;
1818
import net.tfminecraft.coreprotect.event.CoreProtectPreLogEvent;
1919
import net.tfminecraft.coreprotect.model.action.LookupActions;
20+
import net.tfminecraft.coreprotect.model.action.MythicIdentity;
2021
import net.tfminecraft.coreprotect.utility.WorldUtils;
2122

2223
public class EntityKillLogger {
@@ -25,7 +26,7 @@ private EntityKillLogger() {
2526
throw new IllegalStateException("Database class");
2627
}
2728

28-
public static void log(ConsumerWriteBatch preparedStmt, ConsumerWriteBatch preparedStmt2, ConsumerWriteBatch preparedStmtEntityKillLinks, int batchCount, String user, Location location, List<Object> data, int type) {
29+
public static void log(ConsumerWriteBatch preparedStmt, ConsumerWriteBatch preparedStmt2, ConsumerWriteBatch preparedStmtEntityKillLinks, int batchCount, String user, Location location, List<Object> data, int type, MythicIdentity identity) {
2930
try {
3031
if (ConfigHandler.isBlacklisted(user)){
3132
return;
@@ -68,7 +69,7 @@ public static void log(ConsumerWriteBatch preparedStmt, ConsumerWriteBatch prepa
6869
EntitySpawnStatement.addKillLink(preparedStmtEntityKillLinks, (String) data.get(7), entity_key);
6970
}
7071

71-
BlockStatement.insert(preparedStmt, batchCount, time, userId, wid, x, y, z, type, entity_key, null, null, LookupActions.ENTITY_KILL, 0);
72+
BlockStatement.insert(preparedStmt, batchCount, time, userId, wid, x, y, z, type, entity_key, identity == null ? null : identity.toMetadata(), null, LookupActions.ENTITY_KILL, 0);
7273
}
7374
catch (Exception e) {
7475
Database.handleWriteFailure(e);

‎src/main/java/net/tfminecraft/coreprotect/database/lookup/ChestTransactionLookup.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ else if (exact) {
186186
target = target.split(":")[1];
187187
}
188188

189+
target = net.tfminecraft.coreprotect.utility.AuditLabels.item(resultMetadata, resultType, resultAmount, target);
190+
189191
String coordinateInfo = "";
190192
if (entitySpawnRowId != null && (displayWorldId != resultWorldId || displayX != resultX || displayY != resultY || displayZ != resultZ)) {
191193
coordinateInfo = ChatUtils.getCoordinateTooltip(resultWorldId, resultX, resultY, resultZ, Phrase.build(Phrase.LOOKUP_ENTITY_ORIGIN), true);

‎src/main/java/net/tfminecraft/coreprotect/listener/ListenerHandler.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ public ListenerHandler(CoreProtect plugin) {
205205
pluginManager.registerEvents(new PlayerTakeLecternBookListener(), plugin);
206206
pluginManager.registerEvents(new ProjectileLaunchListener(), plugin);
207207
SkillCastListener.register(plugin);
208+
net.tfminecraft.coreprotect.listener.entity.MythicMobListener.register(plugin);
209+
net.tfminecraft.coreprotect.utility.AuditLabels.register(plugin);
208210

209211
// World Listeners
210212
pluginManager.registerEvents(new ChunkPopulateListener(), plugin);

‎src/main/java/net/tfminecraft/coreprotect/listener/entity/EntityDeathListener.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ else if (entity instanceof Zoglin) {
577577
}
578578

579579
if (!(entity instanceof Player)) {
580-
Queue.queueEntityKill(e, entity.getLocation(), data, type);
580+
Queue.queueEntityKill(e, entity.getLocation(), data, type, MythicMobListener.take(entity));
581581
}
582582
else {
583583
Queue.queuePlayerKill(e, entity.getLocation(), entity.getName());
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package net.tfminecraft.coreprotect.listener.entity;
2+
3+
import java.lang.reflect.Method;
4+
5+
import org.bukkit.entity.Entity;
6+
import org.bukkit.event.Event;
7+
import org.bukkit.event.EventPriority;
8+
import org.bukkit.event.Listener;
9+
import org.bukkit.metadata.FixedMetadataValue;
10+
import org.bukkit.metadata.MetadataValue;
11+
import org.bukkit.plugin.Plugin;
12+
13+
import net.tfminecraft.coreprotect.model.action.MythicIdentity;
14+
15+
/** Capture while Mythic still owns the mob, before Bukkit's MONITOR death logger. */
16+
public final class MythicMobListener implements Listener {
17+
private static final String KEY = "coreprotect:mythic-death";
18+
private static volatile Plugin owner;
19+
private final Plugin plugin;
20+
private final Reader reader;
21+
private boolean warned;
22+
23+
MythicMobListener(Plugin plugin, Reader reader) {
24+
this.plugin = plugin;
25+
this.reader = reader;
26+
}
27+
28+
public static void register(Plugin plugin) {
29+
owner = plugin;
30+
Plugin mythic = plugin.getServer().getPluginManager().getPlugin("MythicMobs");
31+
if (mythic == null || !mythic.isEnabled()) {
32+
return;
33+
}
34+
try {
35+
Class<? extends Event> eventClass = Class.forName("io.lumine.mythic.bukkit.events.MythicMobDeathEvent",
36+
true, mythic.getClass().getClassLoader()).asSubclass(Event.class);
37+
MythicMobListener listener = new MythicMobListener(plugin, Reader.of(eventClass));
38+
plugin.getServer().getPluginManager().registerEvent(eventClass, listener, EventPriority.MONITOR,
39+
(ignored, event) -> listener.onDeath(event), plugin, true);
40+
}
41+
catch (ReflectiveOperationException | LinkageError | RuntimeException exception) {
42+
plugin.getLogger().warning("MythicMobs audit identity is unavailable: " + exception);
43+
}
44+
}
45+
46+
void onDeath(Event event) {
47+
try {
48+
Entity entity = (Entity) reader.entity.invoke(event);
49+
Object type = reader.type.invoke(event);
50+
MythicIdentity identity = new MythicIdentity((String) reader.id.invoke(type));
51+
entity.setMetadata(KEY, new FixedMetadataValue(plugin, identity.id()));
52+
}
53+
catch (ReflectiveOperationException | RuntimeException exception) {
54+
if (!warned) {
55+
warned = true;
56+
plugin.getLogger().warning("Unable to read MythicMobs death identity: " + exception);
57+
}
58+
}
59+
}
60+
61+
public static MythicIdentity take(Entity entity) {
62+
return consume(entity, owner);
63+
}
64+
65+
static MythicIdentity consume(Entity entity, Plugin plugin) {
66+
if (plugin == null) {
67+
return null;
68+
}
69+
for (MetadataValue value : entity.getMetadata(KEY)) {
70+
if (value.getOwningPlugin() == plugin) {
71+
entity.removeMetadata(KEY, plugin);
72+
try {
73+
return new MythicIdentity(value.asString());
74+
}
75+
catch (IllegalArgumentException ignored) {
76+
return null;
77+
}
78+
}
79+
}
80+
return null;
81+
}
82+
83+
record Reader(Method entity, Method type, Method id) {
84+
static Reader of(Class<?> event) throws ReflectiveOperationException {
85+
Method type = event.getMethod("getMobType");
86+
return new Reader(event.getMethod("getEntity"), type, type.getReturnType().getMethod("getInternalName"));
87+
}
88+
}
89+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package net.tfminecraft.coreprotect.model.action;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import java.util.regex.Pattern;
7+
8+
import net.tfminecraft.coreprotect.database.statement.BlockStatement;
9+
10+
/** Audit identity only; the original entity rollback payload remains unchanged. */
11+
public record MythicIdentity(String id) {
12+
public static final String MARKER = "coreprotect:mythic";
13+
private static final Pattern ID = Pattern.compile("[A-Za-z0-9_.:-]{1,128}");
14+
15+
public MythicIdentity {
16+
if (id == null || !ID.matcher(id).matches()) {
17+
throw new IllegalArgumentException("Invalid MythicMobs identity");
18+
}
19+
}
20+
21+
public List<Object> toMetadata() {
22+
return new ArrayList<>(List.of(MARKER, id));
23+
}
24+
25+
public static MythicIdentity fromMetadata(List<Object> data) {
26+
if (data == null || data.size() != 2 || !MARKER.equals(data.get(0))
27+
|| !(data.get(1) instanceof String id) || !ID.matcher(id).matches()) {
28+
return null;
29+
}
30+
return new MythicIdentity(id);
31+
}
32+
33+
public static MythicIdentity fromLookupString(String data) {
34+
return data == null || data.isEmpty() ? null
35+
: fromMetadata(BlockStatement.deserializeMetadata(data.getBytes(StandardCharsets.ISO_8859_1)));
36+
}
37+
}

0 commit comments

Comments
 (0)