diff --git a/src/main/java/dev/micle/xptools/XpTools.java b/src/main/java/dev/micle/xptools/XpTools.java index 257d91c..a1390a4 100644 --- a/src/main/java/dev/micle/xptools/XpTools.java +++ b/src/main/java/dev/micle/xptools/XpTools.java @@ -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; diff --git a/src/main/java/dev/micle/xptools/config/Config.java b/src/main/java/dev/micle/xptools/config/Config.java new file mode 100644 index 0000000..687ee33 --- /dev/null +++ b/src/main/java/dev/micle/xptools/config/Config.java @@ -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 clientSpecPair = new ForgeConfigSpec.Builder().configure(Client::new); + CLIENT = clientSpecPair.getLeft(); + CLIENT_SPEC = clientSpecPair.getRight(); + + Pair commonSpecPair = new ForgeConfigSpec.Builder().configure(Common::new); + COMMON = commonSpecPair.getLeft(); + COMMON_SPEC = commonSpecPair.getRight(); + + Pair 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> blockBreakGlobalOperationsRaw; + public static List blockBreakGlobalOperationItems; + private static ForgeConfigSpec.ConfigValue> blockBreakOperationsRaw; + public static List 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)); + } + } + } +} diff --git a/src/main/java/dev/micle/xptools/events/common/OnBlockBreakEventHandler.java b/src/main/java/dev/micle/xptools/events/common/OnBlockBreakEventHandler.java new file mode 100644 index 0000000..cce9704 --- /dev/null +++ b/src/main/java/dev/micle/xptools/events/common/OnBlockBreakEventHandler.java @@ -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 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 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); + } +} diff --git a/src/main/java/dev/micle/xptools/operation/GlobalOperationItem.java b/src/main/java/dev/micle/xptools/operation/GlobalOperationItem.java new file mode 100644 index 0000000..a955c4c --- /dev/null +++ b/src/main/java/dev/micle/xptools/operation/GlobalOperationItem.java @@ -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()); + } +} diff --git a/src/main/java/dev/micle/xptools/operation/OperationCache.java b/src/main/java/dev/micle/xptools/operation/OperationCache.java new file mode 100644 index 0000000..446d716 --- /dev/null +++ b/src/main/java/dev/micle/xptools/operation/OperationCache.java @@ -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> blockBreakCache; + + public static void clearBlockBreakCache() { + blockBreakCache = new HashMap<>(); + } + + public static @Nullable List getBlockBreakCacheEntry(String key) { + if (blockBreakCache.containsKey(key)) { + return new ArrayList<>(blockBreakCache.get(key)); + } + return null; + } + + public static void addBlockBreakCacheEntry(String key, List value) { + blockBreakCache.putIfAbsent(key, new ArrayList<>(value)); + } +} diff --git a/src/main/java/dev/micle/xptools/operation/OperationItem.java b/src/main/java/dev/micle/xptools/operation/OperationItem.java new file mode 100644 index 0000000..2cb4593 --- /dev/null +++ b/src/main/java/dev/micle/xptools/operation/OperationItem.java @@ -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; + } +} diff --git a/src/main/java/dev/micle/xptools/operation/OperationType.java b/src/main/java/dev/micle/xptools/operation/OperationType.java new file mode 100644 index 0000000..7324ba6 --- /dev/null +++ b/src/main/java/dev/micle/xptools/operation/OperationType.java @@ -0,0 +1,9 @@ +package dev.micle.xptools.operation; + +public enum OperationType { + SET, + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE +} diff --git a/src/main/java/dev/micle/xptools/proxy/Proxy.java b/src/main/java/dev/micle/xptools/proxy/Proxy.java index 1339e17..eefac49 100644 --- a/src/main/java/dev/micle/xptools/proxy/Proxy.java +++ b/src/main/java/dev/micle/xptools/proxy/Proxy.java @@ -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) {} diff --git a/src/main/java/dev/micle/xptools/util/EnumUtils.java b/src/main/java/dev/micle/xptools/util/EnumUtils.java new file mode 100644 index 0000000..00a7a1f --- /dev/null +++ b/src/main/java/dev/micle/xptools/util/EnumUtils.java @@ -0,0 +1,17 @@ +package dev.micle.xptools.util; + +public class EnumUtils { + public static > T valueOf(Class clazz, String name) { + for (T e : clazz.getEnumConstants()) { + if (e.name().equalsIgnoreCase(name)) { + return e; + } + } + return null; + } + + public static > T valueOfOrDefault(Class clazz, String name, T defaultValue) { + T value = valueOf(clazz, name); + return (value == null) ? defaultValue : value; + } +}