diff --git a/gradle.properties b/gradle.properties index 9ff824c..97d0570 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ mcVersion=1.21.8 release=2 # 0 for skip release, 1 for pre-release, 2 for release -luminolRef=b0decfdae082c290eeb39c553ba4e44f829dbad1 +luminolRef=db9feeb2b0114cff9815b8b16ac3416846985693 org.gradle.configuration-cache=true org.gradle.caching=true diff --git a/lophine-server/minecraft-patches/features/0030-Leaves-Wool-Hopper-Counter.patch b/lophine-server/minecraft-patches/features/0030-Leaves-Wool-Hopper-Counter.patch new file mode 100644 index 0000000..899e6e2 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0030-Leaves-Wool-Hopper-Counter.patch @@ -0,0 +1,95 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Mon, 27 Oct 2025 15:38:29 +0800 +Subject: [PATCH] Leaves: Wool Hopper Counter + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As a part of : Leaves (https://github.com/LeavesMC/Leaves) +Licensed under: MIT + +This patch is Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet) + +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 55cbf9b4cc568fb9b418433f597ed6b64e4409b0..8449623a313edbe95941a2e328fe47269015f7fb 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -232,8 +232,30 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + flag |= validator.getAsBoolean(); // Paper - note: this is not a validator, it's what adds/sucks in items + } + ++ // Leaves start - Wool hopper counter ++ if (fun.bm.lophine.config.modules.function.WoolHopperCounterConfig.unlimitedSpeed && org.leavesmc.leaves.util.HopperCounter.isEnabled()) { ++ net.minecraft.world.item.DyeColor woolColor = org.leavesmc.leaves.util.WoolUtils.getWoolColorAtPosition(level, blockEntity.getBlockPos().relative(state.getValue(HopperBlock.FACING))); ++ if (woolColor != null) { ++ for (int i = 0; i < Short.MAX_VALUE; i++) { ++ flag |= suckInItems(level, blockEntity); ++ if (!flag) { ++ break; ++ } else { ++ woolHopperCounter(level, pos, state, HopperBlockEntity.getContainerAt(level, pos)); ++ } ++ } ++ } ++ } ++ // Leaves end - Wool hopper counter ++ + if (flag) { + blockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot ++ // Leaves start - Wool hopper counter ++ if (fun.bm.lophine.config.modules.function.WoolHopperCounterConfig.unlimitedSpeed && org.leavesmc.leaves.util.HopperCounter.isEnabled() && woolHopperCounter(level, pos, state, HopperBlockEntity.getContainerAt(level, pos))) { ++ blockEntity.setCooldown(0); ++ return true; ++ } ++ // Leaves end - Wool hopper counter + setChanged(level, pos, state); + // Leaves start - Lithium Sleeping Block Entity + if (me.earthme.luminol.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled +@@ -464,6 +486,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + // Paper end - Perf: Optimize Hoppers + + private static boolean ejectItems(Level level, BlockPos pos, HopperBlockEntity blockEntity) { ++ // Leaves start - hopper counter ++ if (org.leavesmc.leaves.util.HopperCounter.isEnabled()) { ++ if (woolHopperCounter(level, pos, level.getBlockState(pos), HopperBlockEntity.getContainerAt(level, pos))) { ++ return true; ++ } ++ } ++ // Leaves end - hopper counter + Container attachedContainer = me.earthme.luminol.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled ? blockEntity.getInsertInventory(level, pos, blockEntity) : getAttachedContainer(level, pos, blockEntity); // Leaves - Lithium Sleeping Block Entity + if (attachedContainer == null) { + return false; +@@ -554,6 +583,26 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + } + ++ // Leaves start - hopper counter ++ private static boolean woolHopperCounter(Level level, BlockPos blockPos, BlockState state, @Nullable Container container) { ++ if (container == null) { ++ return false; ++ } ++ net.minecraft.world.item.DyeColor woolColor = org.leavesmc.leaves.util.WoolUtils.getWoolColorAtPosition(level, blockPos.relative(state.getValue(HopperBlock.FACING))); ++ if (woolColor != null) { ++ for (int i = 0; i < container.getContainerSize(); ++i) { ++ if (!container.getItem(i).isEmpty()) { ++ ItemStack itemstack = container.getItem(i); ++ org.leavesmc.leaves.util.HopperCounter.getCounter(woolColor).add(level, itemstack); ++ container.setItem(i, ItemStack.EMPTY); ++ } ++ } ++ return true; ++ } ++ return false; ++ } ++ // Leaves end - hopper counter ++ + private static int[] getSlots(Container container, Direction direction) { + if (container instanceof WorldlyContainer worldlyContainer) { + return worldlyContainer.getSlotsForFace(direction); +@@ -710,6 +759,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + + public static boolean addItem(Container container, ItemEntity item) { ++ if (fun.bm.lophine.config.modules.function.WoolHopperCounterConfig.unlimitedSpeed && org.leavesmc.leaves.util.HopperCounter.isEnabled() && item.isRemoved()) return false; // Leaves - Wool hopper counter + boolean flag = false; + // CraftBukkit start + if (org.bukkit.event.inventory.InventoryPickupItemEvent.getHandlerList().getRegisteredListeners().length > 0) { // Paper - optimize hoppers diff --git a/lophine-server/src/main/java/fun/bm/lophine/command/counter/CounterCommand.java b/lophine-server/src/main/java/fun/bm/lophine/command/counter/CounterCommand.java new file mode 100644 index 0000000..51d370b --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/command/counter/CounterCommand.java @@ -0,0 +1,41 @@ +package fun.bm.lophine.command.counter; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import fun.bm.lophine.command.counter.sub.DisplayCommand; +import fun.bm.lophine.command.counter.sub.ResetCommand; +import fun.bm.lophine.command.counter.sub.ToggleCommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.CommandContext; +import org.leavesmc.leaves.command.RootNode; +import org.leavesmc.leaves.util.HopperCounter; + +public class CounterCommand extends RootNode { + private final String PERM_BASE; + + public CounterCommand() { + super("counter", "lophine.commands.counter"); + this.PERM_BASE = "lophine.commands.counter"; + children( + new ToggleCommand(this), + new ResetCommand(this), + new DisplayCommand(this) + ); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + context.getSender().sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Hopper Counter: ", NamedTextColor.GRAY), + Component.text(HopperCounter.isEnabled(), HopperCounter.isEnabled() ? NamedTextColor.AQUA : NamedTextColor.GRAY) + )); + return true; + } + + public boolean hasPermission(@NotNull CommandSender sender, String... subcommand) { + return hasPermission(PERM_BASE, sender, subcommand); + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/command/counter/CounterSubCommand.java b/lophine-server/src/main/java/fun/bm/lophine/command/counter/CounterSubCommand.java new file mode 100644 index 0000000..0174d2a --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/command/counter/CounterSubCommand.java @@ -0,0 +1,24 @@ +package fun.bm.lophine.command.counter; + +import net.minecraft.commands.CommandSourceStack; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.LiteralNode; + +public class CounterSubCommand extends LiteralNode { + protected final CounterCommand parent; + + protected CounterSubCommand(String name, CounterCommand parent) { + super(name); + this.parent = parent; + } + + @Override + public boolean requires(@NotNull CommandSourceStack source) { + return hasPermission(source.getSender()); + } + + protected boolean hasPermission(CommandSender sender) { + return parent.hasPermission(sender, this.name); + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/command/counter/sub/DisplayCommand.java b/lophine-server/src/main/java/fun/bm/lophine/command/counter/sub/DisplayCommand.java new file mode 100644 index 0000000..07e19f9 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/command/counter/sub/DisplayCommand.java @@ -0,0 +1,92 @@ +package fun.bm.lophine.command.counter.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import fun.bm.lophine.command.counter.CounterCommand; +import fun.bm.lophine.command.counter.CounterSubCommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.item.DyeColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; +import org.leavesmc.leaves.util.HopperCounter; + +import java.util.concurrent.CompletableFuture; + +public class DisplayCommand extends CounterSubCommand { + public DisplayCommand(CounterCommand parent) { + super("display", parent); + children( + DyeColorArg::new + ); + } + + public static void displayCounter(CommandContext context, @NotNull HopperCounter counter, boolean realTime) { + for (Component component : counter.format(MinecraftServer.getServer(), context.getSource().getLevel(), realTime)) { + context.getSender().sendMessage(component); + } + } + + private static class DyeColorArg extends ArgumentNode { + protected DyeColorArg() { + super("color", StringArgumentType.string()); + children( + TimeArg::new + ); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + String color0 = context.getArgument(DyeColorArg.class); + DyeColor color = DyeColor.byName(color0, null); + if (color == null) return true; + HopperCounter counter = HopperCounter.getCounter(color); + displayCounter(context, counter, false); + return true; + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + String path = context.getArgumentOrDefault(DyeColorArg.class, ""); + for (DyeColor value : DyeColor.values()) { + String color = value.getName(); + if (color.startsWith(path)) { + builder.suggest(color); + } + } + return builder.buildFuture(); + } + + private static class TimeArg extends ArgumentNode { + protected TimeArg() { + super("time", StringArgumentType.string()); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + String color0 = context.getArgument(DyeColorArg.class); + DyeColor color = DyeColor.byName(color0, null); + if (color == null) return true; + HopperCounter counter = HopperCounter.getCounter(color); + String timeType = context.getArgument(TimeArg.class); + switch (timeType) { + case "realtime" -> displayCounter(context, counter, true); + case "gametick" -> displayCounter(context, counter, false); + default -> + context.getSender().sendMessage(Component.text("Invalid time type: " + timeType, NamedTextColor.RED)); + } + return true; + } + + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + builder.suggest("realtime"); + builder.suggest("gametick"); + return builder.buildFuture(); + } + } + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/command/counter/sub/ResetCommand.java b/lophine-server/src/main/java/fun/bm/lophine/command/counter/sub/ResetCommand.java new file mode 100644 index 0000000..058fe45 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/command/counter/sub/ResetCommand.java @@ -0,0 +1,76 @@ +package fun.bm.lophine.command.counter.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import fun.bm.lophine.command.counter.CounterCommand; +import fun.bm.lophine.command.counter.CounterSubCommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.format.TextColor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.item.DyeColor; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; +import org.leavesmc.leaves.util.HopperCounter; + +import java.util.concurrent.CompletableFuture; + +public class ResetCommand extends CounterSubCommand { + public ResetCommand(CounterCommand parent) { + super("reset", parent); + children( + DyeColorArg::new + ); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + HopperCounter.resetAll(context.getSource().getLevel(), false); + context.getSender().sendMessage(Component.text("Restarted all counters.")); + return true; + } + + private static class DyeColorArg extends ArgumentNode { + protected DyeColorArg() { + super("color", StringArgumentType.string()); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + String color0 = context.getArgument(DyeColorArg.class); + if (color0.equals("all")) { + HopperCounter.resetAll(context.getSource().getLevel(), false); + context.getSender().sendMessage(Component.text("Restarted all counters.")); + return true; + } + DyeColor color = DyeColor.byName(color0, null); + if (color == null) return true; + HopperCounter counter = HopperCounter.getCounter(color); + counter.reset(context.getSource().getLevel()); + context.getSender().sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Restarted "), + Component.text(color.getName(), TextColor.color(color.getTextColor())), + Component.text(" counter.") + )); + return true; + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + String path = context.getArgumentOrDefault(DyeColorArg.class, ""); + if ("all".startsWith(path)) { + builder.suggest("all"); + } + for (DyeColor value : DyeColor.values()) { + String color = value.getName(); + if (color.startsWith(path)) { + builder.suggest(color); + } + } + return builder.buildFuture(); + } + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/command/counter/sub/ToggleCommand.java b/lophine-server/src/main/java/fun/bm/lophine/command/counter/sub/ToggleCommand.java new file mode 100644 index 0000000..50f7e12 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/command/counter/sub/ToggleCommand.java @@ -0,0 +1,55 @@ +package fun.bm.lophine.command.counter.sub; + +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import fun.bm.lophine.command.counter.CounterCommand; +import fun.bm.lophine.command.counter.CounterSubCommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; +import org.leavesmc.leaves.util.HopperCounter; + +public class ToggleCommand extends CounterSubCommand { + public ToggleCommand(CounterCommand parent) { + super("toggle", parent); + children( + BooleanArg::new + ); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + if (!HopperCounter.isEnabled()) { + HopperCounter.setEnabled(true); + context.getSender().sendMessage(Component.text("Hopper Counter now is enabled.", NamedTextColor.AQUA)); + } else { + HopperCounter.setEnabled(false); + context.getSender().sendMessage(Component.text("Hopper Counter now is disabled.", NamedTextColor.RED)); + } + return true; + } + + private static class BooleanArg extends ArgumentNode { + protected BooleanArg() { + super("enabled", BoolArgumentType.bool()); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + boolean enabled = context.getArgument(BooleanArg.class); + if (enabled == HopperCounter.isEnabled()) { + context.getSender().sendMessage(Component.text("Hopper Counter is already " + (enabled ? "enabled" : "disabled") + ".", NamedTextColor.GRAY)); + } else { + HopperCounter.setEnabled(enabled); + if (enabled) { + context.getSender().sendMessage(Component.text("Hopper Counter now is enabled.", NamedTextColor.AQUA)); + } else { + context.getSender().sendMessage(Component.text("Hopper Counter now is disabled.", NamedTextColor.RED)); + } + } + return true; + } + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/WoolHopperCounterConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/WoolHopperCounterConfig.java new file mode 100644 index 0000000..1ac8fee --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/WoolHopperCounterConfig.java @@ -0,0 +1,28 @@ +package fun.bm.lophine.config.modules.function; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import fun.bm.lophine.command.counter.CounterCommand; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; +import org.bukkit.Bukkit; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "wool-hopper-counter") +public class WoolHopperCounterConfig implements IConfigModule { + @ConfigInfo(name = "enabled") + public static boolean enabled = false; + + @ConfigInfo(name = "unlimited-speed") + public static boolean unlimitedSpeed = false; + + @Override + public void onLoaded(CommentedFileConfig configInstance) { + if (enabled) new CounterCommand().register(); + } + + @Override + public void onUnloaded(CommentedFileConfig configInstance) { + Bukkit.getCommandMap().getKnownCommands().remove("luminol:tpsbar"); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/util/HopperCounter.java b/lophine-server/src/main/java/org/leavesmc/leaves/util/HopperCounter.java new file mode 100644 index 0000000..a3cdb14 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/util/HopperCounter.java @@ -0,0 +1,351 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.util; + +import fun.bm.lophine.config.modules.function.WoolHopperCounterConfig; +import it.unimi.dsi.fastutil.objects.Object2LongLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.item.*; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.AbstractBannerBlock; +import net.minecraft.world.level.block.BeaconBeamBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.material.MapColor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +import static java.util.Map.entry; + +// Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet) + +public class HopperCounter { + + private static boolean enabled = false; + private static final Map COUNTERS; + + static { + EnumMap counterMap = new EnumMap<>(DyeColor.class); + for (DyeColor color : DyeColor.values()) { + counterMap.put(color, new HopperCounter(color)); + } + COUNTERS = Collections.unmodifiableMap(counterMap); + } + + public final DyeColor color; + private final TextComponent coloredName; + private final Object2LongMap counter = new Object2LongLinkedOpenHashMap<>(); + private long startTick; + private long startMillis; + + private HopperCounter(DyeColor color) { + this.startTick = -1; + this.color = color; + this.coloredName = Component.text(color.getName(), TextColor.color(color.getTextColor())); + } + + public void add(Level level, ItemStack stack) { + if (startTick < 0) { + startTick = level.getGameTime(); + startMillis = System.currentTimeMillis(); + } + Item item = stack.getItem(); + counter.put(item, counter.getLong(item) + stack.getCount()); + } + + public void reset(Level level) { + counter.clear(); + startTick = level.getGameTime(); + startMillis = System.currentTimeMillis(); + } + + public static void resetAll(Level level, boolean fresh) { + for (HopperCounter counter : COUNTERS.values()) { + counter.reset(level); + if (fresh) { + counter.startTick = -1; + } + } + } + + public List format(MinecraftServer server, Level level, boolean realTime) { + long ticks = Math.max(realTime ? (System.currentTimeMillis() - startMillis) / 50 : level.getGameTime() - startTick, -1); + + if (startTick < 0 || ticks == -1) { + return Collections.singletonList(Component.text().append(coloredName, Component.text(" hasn't started counting yet")).build()); + } + + long total = getTotalItems(); + if (total <= 0) { + return Collections.singletonList(Component.text() + .append(Component.text("No items for "), coloredName) + .append(Component.text(" yet ("), Component.text(String.format("%.2f ", ticks / (20.0 * 60.0)), Style.style(TextDecoration.BOLD))) + .append(Component.text("min"), Component.text(realTime ? " - real time" : ""), Component.text(")")) + .build()); + } + + List items = new ArrayList<>(); + items.add(Component.text() + .append(Component.text("Items for "), coloredName, Component.text(" ")) + .append(Component.text("("), Component.text(String.format("%.2f ", ticks * 1.0 / (20 * 60)), Style.style(TextDecoration.BOLD))) + .append(Component.text("min"), Component.text(realTime ? " - real time" : ""), Component.text("), ")) + .append(Component.text("total: "), Component.text(total, Style.style(TextDecoration.BOLD)), Component.text(", ")) + .append(Component.text("("), Component.text(String.format("%.1f", total * 1.0 * (20 * 60 * 60) / ticks), Style.style(TextDecoration.BOLD))) + .append(Component.text("/h):")) + .build()); + + items.addAll(counter.object2LongEntrySet().stream().sorted((e, f) -> Long.compare(f.getLongValue(), e.getLongValue())).map(entry -> { + Item item = entry.getKey(); + Component name = Component.translatable(item.getDescriptionId()); + TextColor textColor = guessColor(server, item); + + if (textColor != null) { + name = name.style(name.style().merge(Style.style(textColor))); + } else { + name = name.style(name.style().merge(Style.style(TextDecoration.ITALIC))); + } + + long count = entry.getLongValue(); + return Component.text() + .append(Component.text("- ", NamedTextColor.GRAY)) + .append(name) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(count, Style.style(TextDecoration.BOLD)), Component.text(", ", NamedTextColor.GRAY)) + .append(Component.text(String.format("%.1f", count * (20.0 * 60.0 * 60.0) / ticks), Style.style(TextDecoration.BOLD))) + .append(Component.text("/h")) + .build(); + }).toList()); + return items; + } + + private static final Map DEFAULTS = Map.ofEntries( + entry(Items.DANDELION, Blocks.YELLOW_WOOL), + entry(Items.POPPY, Blocks.RED_WOOL), + entry(Items.BLUE_ORCHID, Blocks.LIGHT_BLUE_WOOL), + entry(Items.ALLIUM, Blocks.MAGENTA_WOOL), + entry(Items.AZURE_BLUET, Blocks.SNOW_BLOCK), + entry(Items.RED_TULIP, Blocks.RED_WOOL), + entry(Items.ORANGE_TULIP, Blocks.ORANGE_WOOL), + entry(Items.WHITE_TULIP, Blocks.SNOW_BLOCK), + entry(Items.PINK_TULIP, Blocks.PINK_WOOL), + entry(Items.OXEYE_DAISY, Blocks.SNOW_BLOCK), + entry(Items.CORNFLOWER, Blocks.BLUE_WOOL), + entry(Items.WITHER_ROSE, Blocks.BLACK_WOOL), + entry(Items.LILY_OF_THE_VALLEY, Blocks.WHITE_WOOL), + entry(Items.BROWN_MUSHROOM, Blocks.BROWN_MUSHROOM_BLOCK), + entry(Items.RED_MUSHROOM, Blocks.RED_MUSHROOM_BLOCK), + entry(Items.STICK, Blocks.OAK_PLANKS), + entry(Items.GOLD_INGOT, Blocks.GOLD_BLOCK), + entry(Items.IRON_INGOT, Blocks.IRON_BLOCK), + entry(Items.DIAMOND, Blocks.DIAMOND_BLOCK), + entry(Items.NETHERITE_INGOT, Blocks.NETHERITE_BLOCK), + entry(Items.SUNFLOWER, Blocks.YELLOW_WOOL), + entry(Items.LILAC, Blocks.MAGENTA_WOOL), + entry(Items.ROSE_BUSH, Blocks.RED_WOOL), + entry(Items.PEONY, Blocks.PINK_WOOL), + entry(Items.CARROT, Blocks.ORANGE_WOOL), + entry(Items.APPLE, Blocks.RED_WOOL), + entry(Items.WHEAT, Blocks.HAY_BLOCK), + entry(Items.PORKCHOP, Blocks.PINK_WOOL), + entry(Items.RABBIT, Blocks.PINK_WOOL), + entry(Items.CHICKEN, Blocks.WHITE_TERRACOTTA), + entry(Items.BEEF, Blocks.NETHERRACK), + entry(Items.ENCHANTED_GOLDEN_APPLE, Blocks.GOLD_BLOCK), + entry(Items.COD, Blocks.WHITE_TERRACOTTA), + entry(Items.SALMON, Blocks.ACACIA_PLANKS), + entry(Items.ROTTEN_FLESH, Blocks.BROWN_WOOL), + entry(Items.PUFFERFISH, Blocks.YELLOW_TERRACOTTA), + entry(Items.TROPICAL_FISH, Blocks.ORANGE_WOOL), + entry(Items.POTATO, Blocks.WHITE_TERRACOTTA), + entry(Items.MUTTON, Blocks.RED_WOOL), + entry(Items.BEETROOT, Blocks.NETHERRACK), + entry(Items.MELON_SLICE, Blocks.MELON), + entry(Items.POISONOUS_POTATO, Blocks.SLIME_BLOCK), + entry(Items.SPIDER_EYE, Blocks.NETHERRACK), + entry(Items.GUNPOWDER, Blocks.GRAY_WOOL), + entry(Items.TURTLE_SCUTE, Blocks.LIME_WOOL), + entry(Items.ARMADILLO_SCUTE, Blocks.ANCIENT_DEBRIS), + entry(Items.FEATHER, Blocks.WHITE_WOOL), + entry(Items.FLINT, Blocks.BLACK_WOOL), + entry(Items.LEATHER, Blocks.SPRUCE_PLANKS), + entry(Items.GLOWSTONE_DUST, Blocks.GLOWSTONE), + entry(Items.PAPER, Blocks.WHITE_WOOL), + entry(Items.BRICK, Blocks.BRICKS), + entry(Items.INK_SAC, Blocks.BLACK_WOOL), + entry(Items.SNOWBALL, Blocks.SNOW_BLOCK), + entry(Items.WATER_BUCKET, Blocks.WATER), + entry(Items.LAVA_BUCKET, Blocks.LAVA), + entry(Items.MILK_BUCKET, Blocks.WHITE_WOOL), + entry(Items.CLAY_BALL, Blocks.CLAY), + entry(Items.COCOA_BEANS, Blocks.COCOA), + entry(Items.BONE, Blocks.BONE_BLOCK), + entry(Items.COD_BUCKET, Blocks.BROWN_TERRACOTTA), + entry(Items.PUFFERFISH_BUCKET, Blocks.YELLOW_TERRACOTTA), + entry(Items.SALMON_BUCKET, Blocks.PINK_TERRACOTTA), + entry(Items.TROPICAL_FISH_BUCKET, Blocks.ORANGE_TERRACOTTA), + entry(Items.SUGAR, Blocks.WHITE_WOOL), + entry(Items.BLAZE_POWDER, Blocks.GOLD_BLOCK), + entry(Items.ENDER_PEARL, Blocks.WARPED_PLANKS), + entry(Items.NETHER_STAR, Blocks.DIAMOND_BLOCK), + entry(Items.PRISMARINE_CRYSTALS, Blocks.SEA_LANTERN), + entry(Items.PRISMARINE_SHARD, Blocks.PRISMARINE), + entry(Items.RABBIT_HIDE, Blocks.OAK_PLANKS), + entry(Items.CHORUS_FRUIT, Blocks.PURPUR_BLOCK), + entry(Items.SHULKER_SHELL, Blocks.SHULKER_BOX), + entry(Items.NAUTILUS_SHELL, Blocks.BONE_BLOCK), + entry(Items.HEART_OF_THE_SEA, Blocks.CONDUIT), + entry(Items.HONEYCOMB, Blocks.HONEYCOMB_BLOCK), + entry(Items.NAME_TAG, Blocks.BONE_BLOCK), + entry(Items.TOTEM_OF_UNDYING, Blocks.YELLOW_TERRACOTTA), + entry(Items.TRIDENT, Blocks.PRISMARINE), + entry(Items.GHAST_TEAR, Blocks.WHITE_WOOL), + entry(Items.PHANTOM_MEMBRANE, Blocks.BONE_BLOCK), + entry(Items.EGG, Blocks.BONE_BLOCK), + entry(Items.COPPER_INGOT, Blocks.COPPER_BLOCK), + entry(Items.AMETHYST_SHARD, Blocks.AMETHYST_BLOCK) + ); + + @SuppressWarnings("deprecation") + @Nullable + public static TextColor guessColor(@NotNull MinecraftServer server, Item item) { + RegistryAccess registryAccess = server.registryAccess(); + TextColor direct = fromItem(item, registryAccess); + if (direct != null) { + return direct; + } + + ResourceLocation id = registryAccess.lookupOrThrow(Registries.ITEM).getKey(item); + if (id == null) { + return null; + } + + + for (Recipe recipe : getRecipesForOutput(server.getRecipeManager(), id, server.overworld())) { + for (Ingredient ingredient : recipe.placementInfo().ingredients()) { + Optional> match = ingredient.items().filter(stack -> fromItem(stack.value(), registryAccess) != null).findFirst(); + if (match.isPresent()) { + return fromItem(match.get().value(), registryAccess); + } + } + } + return null; + } + + @NotNull + public static List> getRecipesForOutput(@NotNull RecipeManager recipeManager, ResourceLocation id, Level level) { + List> results = new ArrayList<>(); + ContextMap context = SlotDisplayContext.fromLevel(level); + recipeManager.getRecipes().forEach(recipe -> { + for (RecipeDisplay recipeDisplay : recipe.value().display()) { + recipeDisplay.result().resolveForStacks(context).forEach(stack -> { + if (BuiltInRegistries.ITEM.wrapAsHolder(stack.getItem()).unwrapKey().map(ResourceKey::location).orElseThrow(IllegalStateException::new).equals(id)) { + results.add(recipe.value()); + } + }); + } + }); + return results; + } + + @Nullable + public static TextColor fromItem(Item item, RegistryAccess registryAccess) { + if (DEFAULTS.containsKey(item)) { + return TextColor.color(appropriateColor(DEFAULTS.get(item).defaultMapColor().col)); + } + if (item instanceof DyeItem dye) { + return TextColor.color(appropriateColor(dye.getDyeColor().getMapColor().col)); + } + + Block block = null; + final Registry itemRegistry = registryAccess.lookupOrThrow(Registries.ITEM); + final Registry blockRegistry = registryAccess.lookupOrThrow(Registries.BLOCK); + ResourceLocation id = itemRegistry.getKey(item); + if (item instanceof BlockItem blockItem) { + block = blockItem.getBlock(); + } else if (blockRegistry.getOptional(id).isPresent()) { + block = blockRegistry.getValue(id); + } + + if (block != null) { + if (block instanceof AbstractBannerBlock) { + return TextColor.color(appropriateColor(((AbstractBannerBlock) block).getColor().getMapColor().col)); + } else if (block instanceof BeaconBeamBlock) { + return TextColor.color(appropriateColor(((BeaconBeamBlock) block).getColor().getMapColor().col)); + } + return TextColor.color(appropriateColor(block.defaultMapColor().col)); + } + return null; + } + + public static int appropriateColor(int color) { + if (color == 0) { + return MapColor.SNOW.col; + } + int r = (color >> 16 & 255); + int g = (color >> 8 & 255); + int b = (color & 255); + if (r < 70) { + r = 70; + } + if (g < 70) { + g = 70; + } + if (b < 70) { + b = 70; + } + return (r << 16) + (g << 8) + b; + } + + public long getTotalItems() { + return counter.isEmpty() ? 0 : counter.values().longStream().sum(); + } + + public static HopperCounter getCounter(DyeColor color) { + return COUNTERS.get(color); + } + + public static void setEnabled(boolean is) { + enabled = is; + } + + public static boolean isEnabled() { + return WoolHopperCounterConfig.enabled && enabled; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/util/WoolUtils.java b/lophine-server/src/main/java/org/leavesmc/leaves/util/WoolUtils.java new file mode 100644 index 0000000..40676e8 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/util/WoolUtils.java @@ -0,0 +1,55 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.Map; + +import static java.util.Map.entry; + +public class WoolUtils { + private static final Map WOOL_BLOCK_TO_DYE = Map.ofEntries( + entry(Blocks.WHITE_WOOL, DyeColor.WHITE), + entry(Blocks.ORANGE_WOOL, DyeColor.ORANGE), + entry(Blocks.MAGENTA_WOOL, DyeColor.MAGENTA), + entry(Blocks.LIGHT_BLUE_WOOL, DyeColor.LIGHT_BLUE), + entry(Blocks.YELLOW_WOOL, DyeColor.YELLOW), + entry(Blocks.LIME_WOOL, DyeColor.LIME), + entry(Blocks.PINK_WOOL, DyeColor.PINK), + entry(Blocks.GRAY_WOOL, DyeColor.GRAY), + entry(Blocks.LIGHT_GRAY_WOOL, DyeColor.LIGHT_GRAY), + entry(Blocks.CYAN_WOOL, DyeColor.CYAN), + entry(Blocks.PURPLE_WOOL, DyeColor.PURPLE), + entry(Blocks.BLUE_WOOL, DyeColor.BLUE), + entry(Blocks.BROWN_WOOL, DyeColor.BROWN), + entry(Blocks.GREEN_WOOL, DyeColor.GREEN), + entry(Blocks.RED_WOOL, DyeColor.RED), + entry(Blocks.BLACK_WOOL, DyeColor.BLACK) + ); + + public static DyeColor getWoolColorAtPosition(Level worldIn, BlockPos pos) { + BlockState state = worldIn.getBlockState(pos); + return WOOL_BLOCK_TO_DYE.get(state.getBlock()); + } +}