Merge pull request 'feat/5-handle_xp_from_entities' (#6) from feat/5-handle_xp_from_entities into 1.20.1

Reviewed-on: https://gitea.local.micle.dev/minecraft-mods/xp_tools/pulls/6
This commit is contained in:
2025-05-25 00:15:30 +00:00
6 changed files with 214 additions and 68 deletions

View File

@ -76,6 +76,11 @@ public final class Config {
private static ForgeConfigSpec.ConfigValue<List<? extends String>> blockBreakOperationsRaw; private static ForgeConfigSpec.ConfigValue<List<? extends String>> blockBreakOperationsRaw;
public static List<OperationItem> blockBreakOperationItems; public static List<OperationItem> blockBreakOperationItems;
private static ForgeConfigSpec.ConfigValue<List<? extends String>> entityKillGlobalOperationsRaw;
public static List<GlobalOperationItem> entityKillGlobalOperationItems;
private static ForgeConfigSpec.ConfigValue<List<? extends String>> entityKillOperationsRaw;
public static List<OperationItem> entityKillOperationItems;
Server(ForgeConfigSpec.Builder builder) { Server(ForgeConfigSpec.Builder builder) {
builder.comment("Settings for debugging").push("debug"); builder.comment("Settings for debugging").push("debug");
debugExtra = builder debugExtra = builder
@ -85,9 +90,11 @@ public final class Config {
builder.comment("Settings for optimizations").push("optimization"); builder.comment("Settings for optimizations").push("optimization");
optimizationUseCache = builder optimizationUseCache = builder
.comment("When enabled, the list of operations to perform per unique_id will be cached after the first calculation.") .comment("Allows saving lists of operations per unique id (block_id/entity_id etc.).")
.comment("Although this does increase performance at the cost of RAM, the overall performance hit of this mod is tiny anyway... but oh well") .comment("This will speed up getting the order of operations after the initial calculation.")
.define("optimizationUseCache", true); .comment("The downside is that it uses more RAM of course (I don't know how much), while it shouldn't be a lot it might not be worth it if you don't have many operations.")
.comment("The cache is not persistent and gets cleared whenever the config gets reloaded.")
.define("optimizationUseCache", false);
builder.pop(); builder.pop();
builder.comment("Settings for block breaking").push("block_breaking"); builder.comment("Settings for block breaking").push("block_breaking");
@ -105,11 +112,28 @@ public final class Config {
.comment("'#forge:ores,multiply,1,2,1,false' - Multiplies xp drop of all blocks tagged forge:ores by 1-2, allows additional operations.") .comment("'#forge:ores,multiply,1,2,1,false' - Multiplies xp drop of all blocks tagged forge:ores by 1-2, allows additional operations.")
.define("blockBreakOperations", new ArrayList<>()); .define("blockBreakOperations", new ArrayList<>());
builder.pop(); builder.pop();
builder.comment("Settings for entity killing").push("entity_killing");
builder.comment("Available operations: " + Arrays.toString(OperationType.values()));
entityKillGlobalOperationsRaw = builder
.comment("List of global operations. Format: '[operation],[min],[max],[priority]'")
.comment("Global operations are run before any unique operations.")
.comment("Examples:")
.comment("'set,0,0,0' - Sets the xp of all entities to 0.")
.define("entityKillGlobalOperations", new ArrayList<>());
entityKillOperationsRaw = builder
.comment("List of unique operations. Format: '[entity_id/tag_id],[operation],[min],[max],[priority],[is_last]'")
.comment("Examples:")
.comment("'minecraft:creeper,set,2,2,0,true' - Sets the xp drop for killing creepers to 2, takes highest priority and stops any additional operations.")
.comment("'#some_mod:some_tag,multiply,1,2,1,false' - Multiplies the xp drop for killing all entities tagged some_mod:some_tag by 1-2, allows additional operations.")
.define("entityKillOperations", new ArrayList<>());
builder.pop();
} }
private static void onConfigReload() { private static void onConfigReload() {
// Clear cache // Clear cache
OperationCache.clearBlockBreakCache(); OperationCache.clearBlockBreakCache();
OperationCache.clearEntityKillCache();
// Parse all block break global operations // Parse all block break global operations
blockBreakGlobalOperationItems = new ArrayList<>(); blockBreakGlobalOperationItems = new ArrayList<>();
@ -123,6 +147,19 @@ public final class Config {
for (String s : blockBreakOperationsRaw.get()) { for (String s : blockBreakOperationsRaw.get()) {
blockBreakOperationItems.add(OperationItem.fromConfig(s)); blockBreakOperationItems.add(OperationItem.fromConfig(s));
} }
// Parse all entity kill global operations
entityKillGlobalOperationItems = new ArrayList<>();
for (String s : entityKillGlobalOperationsRaw.get()) {
entityKillGlobalOperationItems.add(GlobalOperationItem.fromConfig(s));
}
entityKillGlobalOperationItems.sort(Comparator.comparingInt(OperationItem::getPriority));
// Parse all entity kill unique operations
entityKillOperationItems = new ArrayList<>();
for (String s : entityKillOperationsRaw.get()) {
entityKillOperationItems.add(OperationItem.fromConfig(s));
}
} }
} }
} }

View File

@ -4,24 +4,21 @@ import dev.micle.xptools.XpTools;
import dev.micle.xptools.config.Config; import dev.micle.xptools.config.Config;
import dev.micle.xptools.operation.OperationCache; import dev.micle.xptools.operation.OperationCache;
import dev.micle.xptools.operation.OperationItem; import dev.micle.xptools.operation.OperationItem;
import dev.micle.xptools.operation.OperationUtils;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block;
import net.minecraftforge.event.level.BlockEvent; import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.registries.ForgeRegistries; import net.minecraftforge.registries.ForgeRegistries;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class OnBlockBreakEventHandler { public class OnBlockBreakEventHandler {
@SubscribeEvent @SubscribeEvent
public void OnBlockBreakEvent(BlockEvent.BreakEvent event) { public void OnBlockBreakEvent(BlockEvent.BreakEvent event) {
Instant start = Instant.now(); Instant start = Instant.now();
boolean usedCache = true;
float xpToDrop = event.getExpToDrop(); float xpToDrop = event.getExpToDrop();
// Get Block id // Get Block id
@ -38,37 +35,14 @@ public class OnBlockBreakEventHandler {
} }
if (operations == null) { if (operations == null) {
operations = new ArrayList<>(); usedCache = false;
// Collect operations on relevant block_id // Calculate operations
if (!block_id.isEmpty()) { operations = OperationUtils.calculateOperationList(
for (OperationItem operationItem : Config.Server.blockBreakOperationItems) { block_id,
if (!operationItem.isTag() && operationItem.getId().equals(block_id)) { event.getState().getTags().toList(),
operations.add(operationItem); Config.Server.blockBreakOperationItems
} );
}
}
// Collect operations on relevant tag_id
for (TagKey<Block> tagKey : event.getState().getTags().toList()) {
String tag_id = tagKey.location().toString();
for (OperationItem operationItem : Config.Server.blockBreakOperationItems) {
if (operationItem.isTag() && operationItem.getId().equals(tag_id)) {
operations.add(operationItem);
}
}
}
// Sort operations based on priority
operations.sort(Comparator.comparingInt(OperationItem::getPriority));
// Remove any operations after last operation
for (OperationItem operationItem : operations) {
if (operationItem.isLast()) {
operations = operations.subList(0, operations.indexOf(operationItem) + 1);
break;
}
}
// Save operations to cache // Save operations to cache
OperationCache.addBlockBreakCacheEntry(block_id, operations); OperationCache.addBlockBreakCacheEntry(block_id, operations);
@ -78,41 +52,13 @@ public class OnBlockBreakEventHandler {
operations.addAll(0, Config.Server.blockBreakGlobalOperationItems); operations.addAll(0, Config.Server.blockBreakGlobalOperationItems);
// Apply operations to xp drops // Apply operations to xp drops
for (OperationItem operation : operations) { xpToDrop = OperationUtils.calculateNewXpAmount(xpToDrop, operations);
// Calculate operation value
float opValue = (operation.getMin() == operation.getMax()) ?
operation.getMin() :
ThreadLocalRandom.current().nextFloat(operation.getMin(), operation.getMax());
// Apply operation
switch (operation.getType()) {
case SET:
xpToDrop = opValue;
break;
case ADD:
xpToDrop += opValue;
break;
case SUBTRACT:
xpToDrop -= opValue;
break;
case MULTIPLY:
xpToDrop *= opValue;
break;
case DIVIDE:
xpToDrop /= opValue;
break;
}
// Stop if this is the last operation
if (operation.isLast()) {
break;
}
}
// Debug logging // Debug logging
if (Config.Server.debugExtra.get()) { if (Config.Server.debugExtra.get()) {
XpTools.LOGGER.debug("Completed block break event:"); XpTools.LOGGER.debug("Completed block break event:");
XpTools.LOGGER.debug("\tOperations: {}", operations); XpTools.LOGGER.debug("\tOperations: {}", operations);
XpTools.LOGGER.debug("\tUsed cache: {}", usedCache);
XpTools.LOGGER.debug("\tTime taken (nano seconds): {}", Duration.between(start, Instant.now()).toNanos()); XpTools.LOGGER.debug("\tTime taken (nano seconds): {}", Duration.between(start, Instant.now()).toNanos());
XpTools.LOGGER.debug("\tXP: {} -> {}", event.getExpToDrop(), xpToDrop); XpTools.LOGGER.debug("\tXP: {} -> {}", event.getExpToDrop(), xpToDrop);
} }

View File

@ -0,0 +1,69 @@
package dev.micle.xptools.events.common;
import dev.micle.xptools.XpTools;
import dev.micle.xptools.config.Config;
import dev.micle.xptools.operation.OperationCache;
import dev.micle.xptools.operation.OperationItem;
import dev.micle.xptools.operation.OperationUtils;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.event.entity.living.LivingExperienceDropEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.registries.ForgeRegistries;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
public class OnLivingExperienceDropEventHandler {
@SubscribeEvent
public void onLivingExperienceDropEvent(LivingExperienceDropEvent event) {
Instant start = Instant.now();
boolean usedCache = true;
float xpToDrop = event.getDroppedExperience();
// Get Entity id
ResourceLocation entity_rl = ForgeRegistries.ENTITY_TYPES.getKey(event.getEntity().getType());
String entity_id = "";
if (entity_rl != null) {
entity_id = entity_rl.toString();
}
// Collect operations
List<OperationItem> operations = null;
if (Config.Server.optimizationUseCache.get()) {
operations = OperationCache.getEntityKillCacheEntry(entity_id);
}
if (operations == null) {
usedCache = false;
// Calculate operations
operations = OperationUtils.calculateOperationList(
entity_id,
event.getEntity().getType().getTags().toList(),
Config.Server.entityKillOperationItems
);
// Save operations to cache
OperationCache.addEntityKillCacheEntry(entity_id, operations);
}
// Add global operations before all others
operations.addAll(0, Config.Server.entityKillGlobalOperationItems);
// Apply operations to xp drops
xpToDrop = OperationUtils.calculateNewXpAmount(xpToDrop, operations);
// Debug logging
if (Config.Server.debugExtra.get()) {
XpTools.LOGGER.debug("Completed entity kill event:");
XpTools.LOGGER.debug("\tOperations: {}", operations);
XpTools.LOGGER.debug("\tUsed cache: {}", usedCache);
XpTools.LOGGER.debug("\tTime taken (nano seconds): {}", Duration.between(start, Instant.now()).toNanos());
XpTools.LOGGER.debug("\tXP: {} -> {}", event.getDroppedExperience(), xpToDrop);
}
// Apply xp drop
event.setDroppedExperience((int)xpToDrop);
}
}

View File

@ -7,11 +7,16 @@ import java.util.List;
public class OperationCache { public class OperationCache {
private static HashMap<String, List<OperationItem>> blockBreakCache; private static HashMap<String, List<OperationItem>> blockBreakCache;
private static HashMap<String, List<OperationItem>> entityKillCache;
public static void clearBlockBreakCache() { public static void clearBlockBreakCache() {
blockBreakCache = new HashMap<>(); blockBreakCache = new HashMap<>();
} }
public static void clearEntityKillCache() {
entityKillCache = new HashMap<>();
}
public static @Nullable List<OperationItem> getBlockBreakCacheEntry(String key) { public static @Nullable List<OperationItem> getBlockBreakCacheEntry(String key) {
if (blockBreakCache.containsKey(key)) { if (blockBreakCache.containsKey(key)) {
return new ArrayList<>(blockBreakCache.get(key)); return new ArrayList<>(blockBreakCache.get(key));
@ -19,7 +24,18 @@ public class OperationCache {
return null; return null;
} }
public static @Nullable List<OperationItem> getEntityKillCacheEntry(String key) {
if (entityKillCache.containsKey(key)) {
return new ArrayList<>(entityKillCache.get(key));
}
return null;
}
public static void addBlockBreakCacheEntry(String key, List<OperationItem> value) { public static void addBlockBreakCacheEntry(String key, List<OperationItem> value) {
blockBreakCache.putIfAbsent(key, new ArrayList<>(value)); blockBreakCache.putIfAbsent(key, new ArrayList<>(value));
} }
public static void addEntityKillCacheEntry(String key, List<OperationItem> value) {
entityKillCache.putIfAbsent(key, new ArrayList<>(value));
}
} }

View File

@ -0,0 +1,76 @@
package dev.micle.xptools.operation;
import net.minecraft.tags.TagKey;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class OperationUtils {
public static <T extends TagKey<?>> List<OperationItem> calculateOperationList(String id, List<T> tagList, List<OperationItem> operationItems) {
List<OperationItem> operations = new ArrayList<>();
// Collect operations on relevant id
if (!id.isEmpty()) {
for (OperationItem operationItem : operationItems) {
if (!operationItem.isTag() && operationItem.getId().equals(id)) {
operations.add(operationItem);
}
}
}
// Collect operations on relevant tag_id
for (T tagKey : tagList) {
String tag_id = tagKey.location().toString();
for (OperationItem operationItem : operationItems) {
if (operationItem.isTag() && operationItem.getId().equals(tag_id)) {
operations.add(operationItem);
}
}
}
// Sort operations based on priority
operations.sort(Comparator.comparingInt(OperationItem::getPriority));
// Remove any operations after last operation
for (OperationItem operationItem : operations) {
if (operationItem.isLast()) {
operations = operations.subList(0, operations.indexOf(operationItem) + 1);
break;
}
}
return operations;
}
public static float calculateNewXpAmount(float xp, List<OperationItem> operations) {
for (OperationItem operation : operations) {
// Calculate operation value
float opValue = (operation.getMin() == operation.getMax()) ?
operation.getMin() :
ThreadLocalRandom.current().nextFloat(operation.getMin(), operation.getMax());
// Apply operation
switch (operation.getType()) {
case SET:
xp = opValue;
break;
case ADD:
xp += opValue;
break;
case SUBTRACT:
xp -= opValue;
break;
case MULTIPLY:
xp *= opValue;
break;
case DIVIDE:
xp /= opValue;
break;
}
}
return xp;
}
}

View File

@ -3,6 +3,7 @@ package dev.micle.xptools.proxy;
import dev.micle.xptools.XpTools; import dev.micle.xptools.XpTools;
import dev.micle.xptools.config.Config; import dev.micle.xptools.config.Config;
import dev.micle.xptools.events.common.OnBlockBreakEventHandler; import dev.micle.xptools.events.common.OnBlockBreakEventHandler;
import dev.micle.xptools.events.common.OnLivingExperienceDropEventHandler;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
@ -34,6 +35,7 @@ public class Proxy implements IProxy {
// Register event handlers // Register event handlers
MinecraftForge.EVENT_BUS.register(new OnBlockBreakEventHandler()); MinecraftForge.EVENT_BUS.register(new OnBlockBreakEventHandler());
MinecraftForge.EVENT_BUS.register(new OnLivingExperienceDropEventHandler());
} }
private static void setup(FMLCommonSetupEvent event) {} private static void setup(FMLCommonSetupEvent event) {}