Merge pull request 'feat/3-handle_xp_from_block_breaking' (#4) from feat/3-handle_xp_from_block_breaking into 1.20.1

Reviewed-on: https://gitea.local.micle.dev/minecraft-mods/xp_tools/pulls/4
This commit is contained in:
2025-05-24 21:58:37 +00:00
9 changed files with 422 additions and 1 deletions

View File

@ -11,7 +11,7 @@ import org.slf4j.Logger;
@Mod(XpTools.MOD_ID)
public class XpTools {
public static final String MOD_ID = "xp_tools";
private static final Logger LOGGER = LogUtils.getLogger();
public static final Logger LOGGER = LogUtils.getLogger();
private static FMLJavaModLoadingContext fmlJavaModLoadingContext;
private static IProxy proxy;

View File

@ -0,0 +1,128 @@
package dev.micle.xptools.config;
import dev.micle.xptools.XpTools;
import dev.micle.xptools.operation.GlobalOperationItem;
import dev.micle.xptools.operation.OperationCache;
import dev.micle.xptools.operation.OperationItem;
import dev.micle.xptools.operation.OperationType;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.config.ModConfigEvent;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
@Mod.EventBusSubscriber(modid = XpTools.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
public final class Config {
public static final Client CLIENT;
public static final ForgeConfigSpec CLIENT_SPEC;
public static final Common COMMON;
public static final ForgeConfigSpec COMMON_SPEC;
public static final Server SERVER;
public static final ForgeConfigSpec SERVER_SPEC;
static {
Pair<Client, ForgeConfigSpec> clientSpecPair = new ForgeConfigSpec.Builder().configure(Client::new);
CLIENT = clientSpecPair.getLeft();
CLIENT_SPEC = clientSpecPair.getRight();
Pair<Common, ForgeConfigSpec> commonSpecPair = new ForgeConfigSpec.Builder().configure(Common::new);
COMMON = commonSpecPair.getLeft();
COMMON_SPEC = commonSpecPair.getRight();
Pair<Server, ForgeConfigSpec> serverSpecPair = new ForgeConfigSpec.Builder().configure(Server::new);
SERVER = serverSpecPair.getLeft();
SERVER_SPEC = serverSpecPair.getRight();
}
public static void register() {
XpTools.getFMLJavaModLoadingContext().registerConfig(ModConfig.Type.CLIENT, CLIENT_SPEC);
XpTools.getFMLJavaModLoadingContext().registerConfig(ModConfig.Type.COMMON, COMMON_SPEC);
XpTools.getFMLJavaModLoadingContext().registerConfig(ModConfig.Type.SERVER, SERVER_SPEC);
}
@SubscribeEvent
public static void onConfigReloadEvent(ModConfigEvent event) {
if (event.getConfig().getSpec() == CLIENT_SPEC) {
Client.onConfigReload();
} else if (event.getConfig().getSpec() == COMMON_SPEC) {
Common.onConfigReload();
} else if (event.getConfig().getSpec() == SERVER_SPEC) {
Server.onConfigReload();
}
}
public static class Client {
Client(ForgeConfigSpec.Builder builder) {}
private static void onConfigReload() {}
}
public static class Common {
Common(ForgeConfigSpec.Builder builder) {}
private static void onConfigReload() {}
}
public static class Server {
public static ForgeConfigSpec.BooleanValue debugExtra;
public static ForgeConfigSpec.BooleanValue optimizationUseCache;
private static ForgeConfigSpec.ConfigValue<List<? extends String>> blockBreakGlobalOperationsRaw;
public static List<GlobalOperationItem> blockBreakGlobalOperationItems;
private static ForgeConfigSpec.ConfigValue<List<? extends String>> blockBreakOperationsRaw;
public static List<OperationItem> blockBreakOperationItems;
Server(ForgeConfigSpec.Builder builder) {
builder.comment("Settings for debugging").push("debug");
debugExtra = builder
.comment("Whether to log more extensive debug information.")
.define("debugExtra", false);
builder.pop();
builder.comment("Settings for optimizations").push("optimization");
optimizationUseCache = builder
.comment("When enabled, the list of operations to perform per unique_id will be cached after the first calculation.")
.comment("Although this does increase performance at the cost of RAM, the overall performance hit of this mod is tiny anyway... but oh well")
.define("optimizationUseCache", true);
builder.pop();
builder.comment("Settings for block breaking").push("block_breaking");
builder.comment("Available operations: " + Arrays.toString(OperationType.values()));
blockBreakGlobalOperationsRaw = 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 blocks to 0.")
.define("blockBreakGlobalOperations", new ArrayList<>());
blockBreakOperationsRaw = builder
.comment("List of unique operations. Format: '[block_id/tag_id],[operation],[min],[max],[priority],[is_last]'")
.comment("Examples:")
.comment("'minecraft:dirt,set,2,2,0,true' - Sets the xp drop of the dirt block to 2, takes highest priority and stops any 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<>());
builder.pop();
}
private static void onConfigReload() {
// Clear cache
OperationCache.clearBlockBreakCache();
// Parse all block break global operations
blockBreakGlobalOperationItems = new ArrayList<>();
for (String s : blockBreakGlobalOperationsRaw.get()) {
blockBreakGlobalOperationItems.add(GlobalOperationItem.fromConfig(s));
}
blockBreakGlobalOperationItems.sort(Comparator.comparingInt(OperationItem::getPriority));
// Parse all block break unique operations
blockBreakOperationItems = new ArrayList<>();
for (String s : blockBreakOperationsRaw.get()) {
blockBreakOperationItems.add(OperationItem.fromConfig(s));
}
}
}
}

View File

@ -0,0 +1,123 @@
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 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.eventbus.api.SubscribeEvent;
import net.minecraftforge.registries.ForgeRegistries;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class OnBlockBreakEventHandler {
@SubscribeEvent
public void OnBlockBreakEvent(BlockEvent.BreakEvent event) {
Instant start = Instant.now();
float xpToDrop = event.getExpToDrop();
// Get Block id
ResourceLocation block_rl = ForgeRegistries.BLOCKS.getKey(event.getState().getBlock());
String block_id = "";
if (block_rl != null) {
block_id = block_rl.toString();
}
// Collect operations
List<OperationItem> operations = null;
if (Config.Server.optimizationUseCache.get()) {
operations = OperationCache.getBlockBreakCacheEntry(block_id);
}
if (operations == null) {
operations = new ArrayList<>();
// Collect operations on relevant block_id
if (!block_id.isEmpty()) {
for (OperationItem operationItem : Config.Server.blockBreakOperationItems) {
if (!operationItem.isTag() && operationItem.getId().equals(block_id)) {
operations.add(operationItem);
}
}
}
// 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
OperationCache.addBlockBreakCacheEntry(block_id, operations);
}
// Add global operations before all others
operations.addAll(0, Config.Server.blockBreakGlobalOperationItems);
// Apply operations to xp drops
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:
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
if (Config.Server.debugExtra.get()) {
XpTools.LOGGER.debug("Completed block break event:");
XpTools.LOGGER.debug("\tOperations: {}", operations);
XpTools.LOGGER.debug("\tTime taken (nano seconds): {}", Duration.between(start, Instant.now()).toNanos());
XpTools.LOGGER.debug("\tXP: {} -> {}", event.getExpToDrop(), xpToDrop);
}
// Apply xp drop
event.setExpToDrop((int)xpToDrop);
}
}

View File

@ -0,0 +1,28 @@
package dev.micle.xptools.operation;
import dev.micle.xptools.util.EnumUtils;
public class GlobalOperationItem extends OperationItem {
public GlobalOperationItem(OperationType type, float min, float max, int priority) {
super(false, "", type, min, max, priority, false);
}
public static GlobalOperationItem fromConfig(String configString) {
String[] splitString = configString.split(",");
if (splitString.length == 4) {
OperationType type = EnumUtils.valueOf(OperationType.class, splitString[0]);
float min = Float.parseFloat(splitString[1]);
float max = Float.parseFloat(splitString[2]);
int priority = Integer.parseInt(splitString[3]);
return new GlobalOperationItem(type, min, max, priority);
}
return null;
}
@Override
public String toString() {
return String.format("%s,%f,%f,%d", getType().toString(), getMin(), getMax(), getPriority());
}
}

View File

@ -0,0 +1,25 @@
package dev.micle.xptools.operation;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class OperationCache {
private static HashMap<String, List<OperationItem>> blockBreakCache;
public static void clearBlockBreakCache() {
blockBreakCache = new HashMap<>();
}
public static @Nullable List<OperationItem> getBlockBreakCacheEntry(String key) {
if (blockBreakCache.containsKey(key)) {
return new ArrayList<>(blockBreakCache.get(key));
}
return null;
}
public static void addBlockBreakCacheEntry(String key, List<OperationItem> value) {
blockBreakCache.putIfAbsent(key, new ArrayList<>(value));
}
}

View File

@ -0,0 +1,84 @@
package dev.micle.xptools.operation;
import dev.micle.xptools.util.EnumUtils;
public class OperationItem {
private final boolean isTag;
private final String id;
private final OperationType type;
private final float min;
private final float max;
private final int priority;
private final boolean isLast;
public OperationItem(boolean isTag, String id, OperationType type, float min, float max, int priority, boolean isLast) {
this.isTag = isTag;
this.id = id;
this.type = type;
this.min = min;
this.max = max;
this.priority = priority;
this.isLast = isLast;
}
public static OperationItem fromConfig(String configString) {
String[] splitString = configString.split(",");
if (splitString.length == 6) {
boolean isTag = splitString[0].startsWith("#");
String id = isTag ? splitString[0].substring(1) : splitString[0];
OperationType type = EnumUtils.valueOf(OperationType.class, splitString[1]);
float min = Float.parseFloat(splitString[2]);
float max = Float.parseFloat(splitString[3]);
int priority = Integer.parseInt(splitString[4]);
boolean isLast = Boolean.parseBoolean(splitString[5]);
return new OperationItem(isTag, id, type, min, max, priority, isLast);
}
return null;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
if (isTag) {
builder.append("#");
}
builder
.append(id).append(",")
.append(type.toString()).append(",")
.append(min).append(",").append(max).append(",")
.append(priority).append(",")
.append(isLast);
return builder.toString();
}
public boolean isTag() {
return isTag;
}
public String getId() {
return id;
}
public OperationType getType() {
return type;
}
public float getMin() {
return min;
}
public float getMax() {
return max;
}
public int getPriority() {
return priority;
}
public boolean isLast() {
return isLast;
}
}

View File

@ -0,0 +1,9 @@
package dev.micle.xptools.operation;
public enum OperationType {
SET,
ADD,
SUBTRACT,
MULTIPLY,
DIVIDE
}

View File

@ -1,6 +1,8 @@
package dev.micle.xptools.proxy;
import dev.micle.xptools.XpTools;
import dev.micle.xptools.config.Config;
import dev.micle.xptools.events.common.OnBlockBreakEventHandler;
import net.minecraft.client.Minecraft;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.player.Player;
@ -17,6 +19,8 @@ public class Proxy implements IProxy {
// Common setup
public Proxy() {
Config.register();
// Register mod event bus listeners
IEventBus modEventBus = XpTools.getFMLJavaModLoadingContext().getModEventBus();
modEventBus.addListener(Proxy::setup);
@ -27,6 +31,9 @@ public class Proxy implements IProxy {
MinecraftForge.EVENT_BUS.addListener(Proxy::onAddReloadListeners);
MinecraftForge.EVENT_BUS.addListener(Proxy::serverStarted);
MinecraftForge.EVENT_BUS.addListener(Proxy::serverStopping);
// Register event handlers
MinecraftForge.EVENT_BUS.register(new OnBlockBreakEventHandler());
}
private static void setup(FMLCommonSetupEvent event) {}

View File

@ -0,0 +1,17 @@
package dev.micle.xptools.util;
public class EnumUtils {
public static <T extends Enum<?>> T valueOf(Class<T> clazz, String name) {
for (T e : clazz.getEnumConstants()) {
if (e.name().equalsIgnoreCase(name)) {
return e;
}
}
return null;
}
public static <T extends Enum<?>> T valueOfOrDefault(Class<T> clazz, String name, T defaultValue) {
T value = valueOf(clazz, name);
return (value == null) ? defaultValue : value;
}
}