Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ This repository is TF-Minecraft's fork of CoreProtect, developed by [PlayPro](ht
- **Targeted recovery** — roll back selected changes and restore previously rolled-back activity.
- **Recovery previews** — review the effect of a rollback or restore before applying it.
- **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.
- **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.
- **Staff investigation tools** — paginated lookups, inventory history, and WorldEdit selection support help investigate incidents of different sizes.

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

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

## Documentation

[Project documentation](https://github.com/TF-Minecraft/Docs/blob/main/projects/CoreProtect/README.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
import net.tfminecraft.coreprotect.listener.channel.PluginChannelListener;
import net.tfminecraft.coreprotect.model.action.EntityActionFilter;
import net.tfminecraft.coreprotect.model.action.LockChange;
import net.tfminecraft.coreprotect.model.action.MythicIdentity;
import net.tfminecraft.coreprotect.database.statement.EntityStatement;
import net.tfminecraft.coreprotect.utility.AuditLabels;
import net.tfminecraft.coreprotect.model.action.LookupActions;
import net.tfminecraft.coreprotect.model.action.SessionActions;
import net.tfminecraft.coreprotect.model.entity.EntitySpawnRecord;
Expand Down Expand Up @@ -321,6 +324,17 @@ else if (pageStart < rows) {
? 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)
: lookupPage.getRows();

Set<Integer> killedEntities = new HashSet<>();
for (String[] row : lookupList) {
if (row.length > 11 && row[7] != null && row[10] != null
&& Integer.parseInt(row[7]) == LookupActions.ENTITY_KILL
&& Integer.parseInt(row[10]) == -1 && Integer.parseInt(row[5]) != 0
&& (row.length <= 13 || row[13] == null
|| Integer.parseInt(row[13]) != InventorySources.ENTITY_INTERACTION)) {
killedEntities.add(Integer.parseInt(row[6]));
}
}
Map<Integer, List<Object>> killedEntityData = EntityStatement.loadData(connection, killedEntities);
Map<Integer, EntitySpawnRecord> entitySpawnRecords = Collections.emptyMap();
Map<UUID, Location> loadedEntityLocations = Collections.emptyMap();
Set<Integer> entitySpawnRowIds = new HashSet<>();
Expand Down Expand Up @@ -462,6 +476,7 @@ else if (LookupActions.isInventoryLookup(actions)) {
Material blockType = ItemUtils.itemFilter(MaterialUtils.getType(dtype), (Integer.parseInt(data[13]) == 0));
String dname = StringUtils.nameFilter(blockType.name().toLowerCase(Locale.ROOT), ddata);
byte[] metadata = data[11] == null ? null : data[11].getBytes(StandardCharsets.ISO_8859_1);
dname = AuditLabels.item(metadata, dtype, amount, dname);
String tooltip = ItemUtils.getEnchantments(metadata, dtype, amount);
Integer itemId = ItemUtils.makeGivableItem(ItemUtils.getItemStack(metadata, dtype, amount));

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

// 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+")");

if (!entityInteraction && daction == LookupActions.ENTITY_KILL && amount == -1 && !isPlayer) {
MythicIdentity identity = MythicIdentity.fromLookupString(data[11]);
dname = AuditLabels.mob(dname, killedEntityData.get(ddata), identity == null ? null : identity.id());
}

Phrase phrase = Phrase.LOOKUP_BLOCK;
String selector = Selector.FIRST;
String action = "a:block";
LockChange lockChange = null;
if (actions.contains(LookupActions.CONTAINER) || actions.contains(5) || actions.contains(LookupActions.ITEM) || amount > -1) {
byte[] metadata = data[11] == null ? null : data[11].getBytes(StandardCharsets.ISO_8859_1);
dname = AuditLabels.item(metadata, dtype, amount, dname);
String tooltip = ItemUtils.getEnchantments(metadata, dtype, amount);
Integer itemId = ItemUtils.makeGivableItem(ItemUtils.getItemStack(metadata, dtype, amount));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,12 @@ protected static void queueEntityInsert(int id, String name) {
// These auxiliary queues still hold distinct legacy payloads consumed by the matching processors.
@SuppressWarnings("deprecation")
protected static void queueEntityKill(String user, Location location, List<Object> data, EntityType type) {
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());
queueEntityKill(user, location, data, type, null);
}

@SuppressWarnings("deprecation")
protected static void queueEntityKill(String user, Location location, List<Object> data, EntityType type, net.tfminecraft.coreprotect.model.action.MythicIdentity identity) {
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());
}

protected static void queueEntitySpawn(String user, BlockState block, EntityType type, int data) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.bukkit.entity.EntityType;

import net.tfminecraft.coreprotect.consumer.Consumer;
import net.tfminecraft.coreprotect.model.action.MythicIdentity;
import net.tfminecraft.coreprotect.database.logger.EntityKillLogger;
import net.tfminecraft.coreprotect.utility.EntityUtils;

Expand All @@ -29,7 +30,8 @@ static void process(ConsumerWriteBatch preparedStmt, ConsumerWriteBatch prepared
if (objectLists != null && objectLists.get(id) != null) {
List<Object> objectList = objectLists.get(id);
int entityId = EntityUtils.getEntityId(type);
EntityKillLogger.log(preparedStmt, preparedStmtEntities, preparedStmtEntityKillLinks, batchCount, user, location, objectList, entityId);
EntityKillLogger.log(preparedStmt, preparedStmtEntities, preparedStmtEntityKillLinks, batchCount, user, location, objectList, entityId,
values.length > 2 && values[2] instanceof MythicIdentity identity ? identity : null);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import net.tfminecraft.coreprotect.database.statement.UserStatement;
import net.tfminecraft.coreprotect.event.CoreProtectPreLogEvent;
import net.tfminecraft.coreprotect.model.action.LookupActions;
import net.tfminecraft.coreprotect.model.action.MythicIdentity;
import net.tfminecraft.coreprotect.utility.WorldUtils;

public class EntityKillLogger {
Expand All @@ -25,7 +26,7 @@ private EntityKillLogger() {
throw new IllegalStateException("Database class");
}

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

BlockStatement.insert(preparedStmt, batchCount, time, userId, wid, x, y, z, type, entity_key, null, null, LookupActions.ENTITY_KILL, 0);
BlockStatement.insert(preparedStmt, batchCount, time, userId, wid, x, y, z, type, entity_key, identity == null ? null : identity.toMetadata(), null, LookupActions.ENTITY_KILL, 0);
}
catch (Exception e) {
Database.handleWriteFailure(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ else if (exact) {
target = target.split(":")[1];
}

target = net.tfminecraft.coreprotect.utility.AuditLabels.item(resultMetadata, resultType, resultAmount, target);

String coordinateInfo = "";
if (entitySpawnRowId != null && (displayWorldId != resultWorldId || displayX != resultX || displayY != resultY || displayZ != resultZ)) {
coordinateInfo = ChatUtils.getCoordinateTooltip(resultWorldId, resultX, resultY, resultZ, Phrase.build(Phrase.LOOKUP_ENTITY_ORIGIN), true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ public ListenerHandler(CoreProtect plugin) {
pluginManager.registerEvents(new PlayerTakeLecternBookListener(), plugin);
pluginManager.registerEvents(new ProjectileLaunchListener(), plugin);
SkillCastListener.register(plugin);
net.tfminecraft.coreprotect.listener.entity.MythicMobListener.register(plugin);
net.tfminecraft.coreprotect.utility.AuditLabels.register(plugin);

// World Listeners
pluginManager.registerEvents(new ChunkPopulateListener(), plugin);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ else if (entity instanceof Zoglin) {
}

if (!(entity instanceof Player)) {
Queue.queueEntityKill(e, entity.getLocation(), data, type);
Queue.queueEntityKill(e, entity.getLocation(), data, type, MythicMobListener.take(entity));
}
else {
Queue.queuePlayerKill(e, entity.getLocation(), entity.getName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package net.tfminecraft.coreprotect.listener.entity;

import java.lang.reflect.Method;

import org.bukkit.entity.Entity;
import org.bukkit.event.Event;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.plugin.Plugin;

import net.tfminecraft.coreprotect.model.action.MythicIdentity;

/** Capture while Mythic still owns the mob, before Bukkit's MONITOR death logger. */
public final class MythicMobListener implements Listener {
private static final String KEY = "coreprotect:mythic-death";
private static volatile Plugin owner;
private final Plugin plugin;
private final Reader reader;
private boolean warned;

MythicMobListener(Plugin plugin, Reader reader) {
this.plugin = plugin;
this.reader = reader;
}

public static void register(Plugin plugin) {
owner = plugin;
Plugin mythic = plugin.getServer().getPluginManager().getPlugin("MythicMobs");
if (mythic == null || !mythic.isEnabled()) {
return;
}
try {
Class<? extends Event> eventClass = Class.forName("io.lumine.mythic.bukkit.events.MythicMobDeathEvent",
true, mythic.getClass().getClassLoader()).asSubclass(Event.class);
MythicMobListener listener = new MythicMobListener(plugin, Reader.of(eventClass));
plugin.getServer().getPluginManager().registerEvent(eventClass, listener, EventPriority.MONITOR,
(ignored, event) -> listener.onDeath(event), plugin, true);
}
catch (ReflectiveOperationException | LinkageError | RuntimeException exception) {
plugin.getLogger().warning("MythicMobs audit identity is unavailable: " + exception);
}
}

void onDeath(Event event) {
try {
Entity entity = (Entity) reader.entity.invoke(event);
Object type = reader.type.invoke(event);
MythicIdentity identity = new MythicIdentity((String) reader.id.invoke(type));
entity.setMetadata(KEY, new FixedMetadataValue(plugin, identity.id()));
}
catch (ReflectiveOperationException | RuntimeException exception) {
if (!warned) {
warned = true;
plugin.getLogger().warning("Unable to read MythicMobs death identity: " + exception);
}
}
}

public static MythicIdentity take(Entity entity) {
return consume(entity, owner);
}

static MythicIdentity consume(Entity entity, Plugin plugin) {
if (plugin == null) {
return null;
}
for (MetadataValue value : entity.getMetadata(KEY)) {
if (value.getOwningPlugin() == plugin) {
entity.removeMetadata(KEY, plugin);
try {
return new MythicIdentity(value.asString());
}
catch (IllegalArgumentException ignored) {
return null;
}
}
}
return null;
}

record Reader(Method entity, Method type, Method id) {
static Reader of(Class<?> event) throws ReflectiveOperationException {
Method type = event.getMethod("getMobType");
return new Reader(event.getMethod("getEntity"), type, type.getReturnType().getMethod("getInternalName"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package net.tfminecraft.coreprotect.model.action;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import net.tfminecraft.coreprotect.database.statement.BlockStatement;

/** Audit identity only; the original entity rollback payload remains unchanged. */
public record MythicIdentity(String id) {
public static final String MARKER = "coreprotect:mythic";
private static final Pattern ID = Pattern.compile("[A-Za-z0-9_.:-]{1,128}");

public MythicIdentity {
if (id == null || !ID.matcher(id).matches()) {
throw new IllegalArgumentException("Invalid MythicMobs identity");
}
}

public List<Object> toMetadata() {
return new ArrayList<>(List.of(MARKER, id));
}

public static MythicIdentity fromMetadata(List<Object> data) {
if (data == null || data.size() != 2 || !MARKER.equals(data.get(0))
|| !(data.get(1) instanceof String id) || !ID.matcher(id).matches()) {
return null;
}
return new MythicIdentity(id);
}

public static MythicIdentity fromLookupString(String data) {
return data == null || data.isEmpty() ? null
: fromMetadata(BlockStatement.deserializeMetadata(data.getBytes(StandardCharsets.ISO_8859_1)));
}
}
Loading