introduce 26.1.2 changes

Fakeplayer Action Gui - Stage 1 (#161)

Still needs some tweaks to fit Leaves' changes

* pre add gui (not implemented)

* fallback one change in Actions

* pre for new structure of gui register

* Enhance GUI node structure with item support and command handling

* feat: update GUI node handling in bot actions

* feat: enhance command building logic in GUI nodes

* fix a bug

* feat: implement GUI navigation and command execution in BotAction GUI

* feat: refactor GuiNode and related classes to use Item instead of ItemStack

* feat: update command building logic to include additional action parameters

* feat: simplify command syntax in BotAction GUI and update inventory view handling

* feat: add ActionType enum and implement action confirmation in BotAction GUI

* feat: enhance ActionType handling and selection in BotAction GUI

* feat: integrate GUI data handling in bot actions and enhance navigation

* fix up

* because of tmp move to new org, rebrand

* Implement pagination for bot action GUI and enhance action stop functionality

* ci updates

* replace boarder item

* fix https://github.com/LophineCraft/Lophine/pull/161#issuecomment-5012127501 issues - 1

* fix https://github.com/LophineCraft/Lophine/pull/161#issuecomment-5012127501 issues - 2

* fix index

* replace some items

* feat: use the same item shown for stop & start action

* remove 2 todos, we wouldn't provide them

* refactor: simplify rendering methods in BotActionGuiContainer

* feat: add empty action placeholder and simplify item rendering

* fix: remove extra suggestions

* fix: update GUI icons for jump, move, and swap actions

* fix: update comment in open action gui

* set release mode to true

---------

Co-authored-by: xiaoxijun <A3167717663@hotmail.com>
This commit is contained in:
Helvetica Volubi
2026-07-23 00:23:03 +08:00
parent 04bedd5dfc
commit c9bfe55ce4
31 changed files with 1543 additions and 20 deletions
@@ -0,0 +1,759 @@
package fun.bm.lophine.bot;
import fun.bm.lophine.bot.action.gui.ActionType;
import fun.bm.lophine.bot.action.gui.GuiNode;
import fun.bm.lophine.bot.action.gui.GuiRootNode;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.leavesmc.leaves.bot.agent.actions.AbstractBotAction;
import org.leavesmc.leaves.entity.bot.CraftBot;
import org.leavesmc.leaves.entity.bot.action.BotAction;
import org.leavesmc.leaves.entity.bot.actions.CraftBotAction;
import java.util.*;
public class BotActionGuiContainer extends SimpleContainer {
private static final Map<String, GuiRootNode> GUI_ROOT_NODE_MAP = new LinkedHashMap<>();
private static final int CONTAINER_SIZE = 54;
private static final int[] BORDER_SLOTS = {
0, 1, 2, 3, 4, 5, 6, 7, 8,
9, 17,
18, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 41, 42, 43, 44,
45, 46, 47, 48, 49, 50, 51, 52, 53
};
public static final int[] CONTENT_SLOTS = {
10, 11, 12, 13, 14, 15, 16,
19, 20, 21, 22, 23, 24, 25
};
private static final int BACK_BUTTON_SLOT = 38;
private static final int COMMAND_BUILDER_SLOT = 40;
private static final int HOME_BUTTON_SLOT = 42;
private static final int PREV_PAGE_SLOT = 39;
private static final int NEXT_PAGE_SLOT = 41;
private static final ItemStack boarder = new ItemStack(Items.STAINED_GLASS_PANE.white());
private static final ItemStack emptyActionPlaceholder = createEmptyActionPlaceholder();
private final CraftBot bot;
private final CraftPlayer player;
private final Deque<GuiNode> navigationStack = new ArrayDeque<>();
private final Deque<Boolean> fromRootLevelStack = new ArrayDeque<>(); // Track if we came from root level
private GuiNode currentNode = null;
// New state for action type selection flow
private ActionType selectedActionType = null;
private boolean isSelectingActionType = false;
// Pagination state
private int currentPage = 0;
public static void registerGuiRootNode(GuiRootNode guiRootNode) {
if (!GUI_ROOT_NODE_MAP.containsKey(guiRootNode.getName())) {
GUI_ROOT_NODE_MAP.put(guiRootNode.getName(), guiRootNode);
}
}
public static void unregisterGuiRootNode(String name) {
GUI_ROOT_NODE_MAP.remove(name);
}
public static GuiRootNode getGuiRootNode(String name) {
return GUI_ROOT_NODE_MAP.get(name);
}
public static Map<String, GuiRootNode> getGuiRootNodes() {
return new LinkedHashMap<>(GUI_ROOT_NODE_MAP);
}
public BotActionGuiContainer(@NotNull CraftBot bot, CraftPlayer player) {
super(CONTAINER_SIZE, player);
this.bot = bot;
this.player = player;
this.showActionTypes();
}
public boolean isCommandBuilderSlot(int id) {
return id == COMMAND_BUILDER_SLOT;
}
private void fillBorder() {
for (int slot : BORDER_SLOTS) {
this.setItem(slot, boarder);
}
}
private void showActionTypes() {
this.clearContent();
this.fillBorder();
this.currentNode = null;
this.navigationStack.clear();
this.fromRootLevelStack.clear();
this.selectedActionType = null;
this.isSelectingActionType = true;
this.currentPage = 0;
this.renderActionTypes();
this.addNavigationButtons();
}
private void showRootNodes() {
this.clearContent();
this.fillBorder();
this.currentNode = null;
this.navigationStack.clear();
this.fromRootLevelStack.clear();
this.isSelectingActionType = false;
this.currentPage = 0;
List<GuiRootNode> allNodes = new ArrayList<>(GUI_ROOT_NODE_MAP.values());
if (allNodes.isEmpty()) {
// No root nodes, go back to action type selection
this.showActionTypes();
return;
}
this.renderRootNodes();
this.addNavigationButtons();
}
public void navigateToChild(GuiNode node) {
if (node == null) {
return;
}
// If we're at root level (currentNode is null but not selecting action type),
// we need to track that we came from root nodes level
if (this.currentNode != null) {
this.navigationStack.push(this.currentNode);
this.fromRootLevelStack.push(false);
} else if (!this.isSelectingActionType && this.selectedActionType != null) {
// We're at root nodes level, mark that we should return to root nodes
this.fromRootLevelStack.push(true);
}
this.currentNode = node;
this.currentPage = 0;
this.refreshContainer();
}
public boolean navigateBack() {
// If we're at root node level (no navigation stack), go back to action type selection
if (this.navigationStack.isEmpty() && this.fromRootLevelStack.isEmpty()) {
// If we have a selected action type, we're at the root nodes level
if (!this.isSelectingActionType && this.selectedActionType != null) {
this.showActionTypes();
return true;
}
// Already at action type selection
return false;
}
// Check if we should return to root nodes level
boolean fromRootLevel = !this.fromRootLevelStack.isEmpty() && this.fromRootLevelStack.pop();
if (fromRootLevel) {
// Return to root nodes level
this.currentNode = null;
this.showRootNodes();
} else if (!this.navigationStack.isEmpty()) {
// Pop from navigation stack and refresh
this.currentNode = this.navigationStack.pop();
this.refreshContainer();
}
return true;
}
public void navigateHome() {
this.showActionTypes();
}
private void refreshContainer() {
this.clearContent();
this.fillBorder();
Set<GuiNode> children = this.getChildrenOfCurrentNode();
if (children == null || children.isEmpty()) {
// Empty page: auto-navigate back
this.navigateBack();
return;
}
this.renderCurrentNodeChildren();
this.addNavigationButtons();
}
@Nullable
private Set<GuiNode> getChildrenOfCurrentNode() {
if (this.currentNode == null) {
return null;
}
if (this.currentNode instanceof GuiRootNode rootNode) {
return rootNode.getChildren();
}
return null;
}
private void renderActionTypes() {
List<ActionType> allTypes = List.of(ActionType.values());
this.renderPagedItems(allTypes, (actionType, slot) -> {
ItemStack item = actionType.toConfirmNode(null).getItemStack();
if (item != null && !item.isEmpty()) {
this.setItem(slot, item);
}
});
}
private void renderRootNodes() {
List<GuiRootNode> allNodes = new ArrayList<>(GUI_ROOT_NODE_MAP.values());
this.renderPagedItems(allNodes, (node, slot) -> {
ItemStack item = createNodeItemWithRunHint(node);
if (item != null && !item.isEmpty()) {
this.setItem(slot, item);
}
});
}
private void renderCurrentNodeChildren() {
Set<GuiNode> children = this.getChildrenOfCurrentNode();
if (children != null && !children.isEmpty()) {
List<GuiNode> childList = new ArrayList<>(children);
this.renderPagedItems(childList, (node, slot) -> {
ItemStack item = createNodeItemWithRunHint(node);
if (item != null && !item.isEmpty()) {
this.setItem(slot, item);
}
});
}
}
private void renderBotActions() {
int actionSize = this.bot.getActionSize();
if (actionSize > 0) {
List<Integer> actionIndices = new ArrayList<>();
for (int i = 0; i < actionSize; i++) {
actionIndices.add(i);
}
this.renderPagedItems(actionIndices, (index, slot) -> {
BotAction<?> action = this.bot.getAction(index);
ItemStack item = createActionItem(action, index);
if (!item.isEmpty()) {
this.setItem(slot, item);
}
});
} else {
// Show empty placeholder when no actions are active
this.setItem(CONTENT_SLOTS[0], emptyActionPlaceholder);
}
}
private static ItemStack createEmptyActionPlaceholder() {
ItemStack item = new ItemStack(Items.STRUCTURE_VOID);
item.set(DataComponents.CUSTOM_NAME, Component.literal("§cNo active actions"));
item.set(DataComponents.LORE, new ItemLore(List.of(
Component.literal("§cThis bot has no running actions"),
Component.literal("§cUse START to add new actions")
)));
return item;
}
@Nullable
public GuiNode getGuiNodeAtSlot(int slot) {
if (slot == BACK_BUTTON_SLOT && this.canNavigateBack()) {
return null; // back button is handled separately
}
// Check if slot is a border slot
for (int borderSlot : BORDER_SLOTS) {
if (borderSlot == slot) {
return null;
}
}
Set<GuiNode> nodes = this.getCurrentDisplayNodes();
if (nodes == null) {
return null;
}
// Find the content index for this slot
int contentIndex = -1;
for (int i = 0; i < CONTENT_SLOTS.length; i++) {
if (CONTENT_SLOTS[i] == slot) {
contentIndex = i;
break;
}
}
if (contentIndex == -1) {
return null;
}
// Apply pagination offset
int actualIndex = this.currentPage * CONTENT_SLOTS.length + contentIndex;
int index = 0;
for (GuiNode node : nodes) {
if (index == actualIndex) {
return node;
}
index++;
}
return null;
}
public boolean isBackButtonSlot(int slot) {
return slot == BACK_BUTTON_SLOT && this.canNavigateBack();
}
public boolean isHomeButtonSlot(int slot) {
return slot == HOME_BUTTON_SLOT && this.canNavigateHome();
}
private static ItemStack createBackButtonItem() {
ItemStack item = new ItemStack(Items.WOOL.red());
item.set(DataComponents.CUSTOM_NAME, Component.literal("§cBack"));
item.set(DataComponents.LORE, new ItemLore(List.of(Component.literal("Return to previous page"))));
return item;
}
private static ItemStack createHomeButtonItem() {
ItemStack item = new ItemStack(Items.BED.red());
item.set(DataComponents.CUSTOM_NAME, Component.literal("§aHome"));
item.set(DataComponents.LORE, new ItemLore(List.of(Component.literal("Return to main menu"))));
return item;
}
private ItemStack createNodeItemWithRunHint(GuiNode node) {
ItemStack itemStack = node.getItemStack();
// Check if this is a last-level node (no children)
boolean isLastLevel;
if (node instanceof GuiRootNode rootNode) {
isLastLevel = rootNode.getChildren().isEmpty();
} else {
// Non-RootNode is always considered last level
isLastLevel = true;
}
// If it's the last level and we have an action type selected, add run hint
if (isLastLevel && this.selectedActionType != null && !this.isSelectingActionType) {
// Get existing lore or create new one
ItemLore existingLore = itemStack.get(DataComponents.LORE);
List<Component> loreLines = new ArrayList<>();
if (existingLore != null) {
loreLines.addAll(existingLore.lines());
}
// Add golden run hint
loreLines.add(Component.literal("§6Click to execute"));
itemStack.set(DataComponents.LORE, new ItemLore(loreLines));
}
return itemStack;
}
/**
* Render items with pagination support.
*
* @param items the full list of items
* @param renderer callback to render each item at a specific slot
*/
private <T> void renderPagedItems(List<T> items, PagedItemRenderer<T> renderer) {
int pageSize = CONTENT_SLOTS.length;
int totalPages = Math.max(1, (items.size() + pageSize - 1) / pageSize);
// Clamp current page
if (this.currentPage >= totalPages) {
this.currentPage = totalPages - 1;
}
if (this.currentPage < 0) {
this.currentPage = 0;
}
int start = this.currentPage * pageSize;
int end = Math.min(start + pageSize, items.size());
for (int i = start; i < end; i++) {
int slotIndex = i - start;
renderer.render(items.get(i), CONTENT_SLOTS[slotIndex]);
}
}
@FunctionalInterface
private interface PagedItemRenderer<T> {
void render(T item, int slot);
}
/**
* Add navigation buttons including pagination controls.
*/
private void addNavigationButtons() {
if (this.canNavigateBack()) {
this.setItem(BACK_BUTTON_SLOT, createBackButtonItem());
}
this.setItem(COMMAND_BUILDER_SLOT, createCommandBuilderItem());
if (this.canNavigateHome()) {
this.setItem(HOME_BUTTON_SLOT, createHomeButtonItem());
}
// Pagination buttons
if (this.hasPrevPage()) {
this.setItem(PREV_PAGE_SLOT, createPrevPageItem());
}
if (this.hasNextPage()) {
this.setItem(NEXT_PAGE_SLOT, createNextPageItem());
}
}
private boolean hasPrevPage() {
return this.currentPage > 0;
}
private boolean hasNextPage() {
int totalItems = this.getCurrentTotalItemCount();
int totalPages = Math.max(1, (totalItems + CONTENT_SLOTS.length - 1) / CONTENT_SLOTS.length);
return this.currentPage < totalPages - 1;
}
private int getCurrentTotalItemCount() {
if (this.isSelectingActionType) {
return ActionType.values().length;
}
if (this.selectedActionType == ActionType.ACTION_STOP) {
return this.bot.getActionSize();
}
if (this.currentNode == null) {
return GUI_ROOT_NODE_MAP.size();
}
Set<GuiNode> children = this.getChildrenOfCurrentNode();
return children != null ? children.size() : 0;
}
public void prevPage() {
if (this.hasPrevPage()) {
this.currentPage--;
this.redisplayCurrentViewWithoutReset();
}
}
public void nextPage() {
if (this.hasNextPage()) {
this.currentPage++;
this.redisplayCurrentViewWithoutReset();
}
}
public boolean isPrevPageSlot(int slot) {
return slot == PREV_PAGE_SLOT && this.hasPrevPage();
}
public boolean isNextPageSlot(int slot) {
return slot == NEXT_PAGE_SLOT && this.hasNextPage();
}
/**
* Re-render the current view without resetting pagination state.
* Used for page navigation to preserve currentPage value.
*/
private void redisplayCurrentViewWithoutReset() {
this.clearContent();
this.fillBorder();
if (this.isSelectingActionType) {
this.renderActionTypes();
} else if (this.selectedActionType == ActionType.ACTION_STOP) {
this.renderBotActions();
} else if (this.currentNode != null) {
this.renderCurrentNodeChildren();
} else {
this.renderRootNodes();
}
this.addNavigationButtons();
}
private static ItemStack createPrevPageItem() {
ItemStack item = new ItemStack(Items.ARROW);
item.set(DataComponents.CUSTOM_NAME, Component.literal("§ePrevious Page"));
return item;
}
private static ItemStack createNextPageItem() {
ItemStack item = new ItemStack(Items.ARROW);
item.set(DataComponents.CUSTOM_NAME, Component.literal("§eNext Page"));
return item;
}
private ItemStack createCommandBuilderItem() {
ItemStack item = new ItemStack(Items.BOOK);
// Build the complete command preview
String commandPreview = buildCommandPreview();
// Check if current node is runnable (confirmable)
boolean canRun = this.currentNode != null && this.currentNode.isConfirmable();
List<Component> loreLines = new ArrayList<>();
loreLines.add(Component.literal("§eCommand:"));
loreLines.add(Component.literal("§f" + commandPreview));
// Add run hint if the node is confirmable
if (canRun) {
loreLines.add(Component.literal("§6Click to execute"));
}
item.set(DataComponents.CUSTOM_NAME, Component.literal("§6Command Builder"));
item.set(DataComponents.LORE, new ItemLore(loreLines));
return item;
}
private String buildCommandPreview() {
if (this.selectedActionType == null) {
return "Select an action type first";
}
// For STOP action type, show different preview
if (this.selectedActionType == ActionType.ACTION_STOP) {
int actionSize = this.bot.getActionSize();
if (actionSize == 0) {
return "No actions to stop";
}
return "Click an action to stop it (" + actionSize + " active)";
}
try {
String actionPrefix = this.selectedActionType.getCommandActionPrefix();
String actionSuffix = this.selectedActionType.getCommandActionSuffix();
if (!actionSuffix.isEmpty()) {
actionSuffix = actionSuffix + " ";
}
String extra = actionPrefix + " " + this.bot.getName() + " " + actionSuffix;
// If we have a current node and it's a GuiRootNode, build the command
if (this.currentNode instanceof GuiRootNode rootNode) {
String command = rootNode.buildCommand(extra);
// Apply parameter limit based on ActionType
if (this.selectedActionType.getMaxAllowedParameters() == 0) {
command = actionPrefix + " " + this.bot.getName();
}
return command;
} else if (!this.isSelectingActionType) {
// At root nodes level or selecting action type
return extra.trim() + " <command>";
} else {
return "Select a command node";
}
} catch (Exception e) {
return "Error building command";
}
}
@Nullable
private Set<GuiNode> getCurrentDisplayNodes() {
// If selecting action type, return null (handled separately)
if (this.isSelectingActionType) {
return null;
}
if (this.currentNode == null) {
return new LinkedHashSet<>(GUI_ROOT_NODE_MAP.values());
}
return this.getChildrenOfCurrentNode();
}
/**
* Called when user selects an ActionType (START/STOP)
*/
public void selectActionType(ActionType actionType) {
this.selectedActionType = actionType;
this.isSelectingActionType = false;
// If STOP action type is selected, show current bot actions instead of root nodes
if (actionType == ActionType.ACTION_STOP) {
this.showCurrentBotActions();
} else {
this.showRootNodes();
}
}
/**
* Show the current bot's scheduled actions for stopping
*/
public void showCurrentBotActions() {
this.clearContent();
this.fillBorder();
this.currentNode = null;
this.navigationStack.clear();
this.fromRootLevelStack.clear();
this.isSelectingActionType = false;
this.currentPage = 0;
this.renderBotActions();
this.addNavigationButtons();
}
/**
* Create an ItemStack representing a bot action for display in the GUI
*/
private ItemStack createActionItem(BotAction<?> action, int index) {
// Use the same item model as registered in GuiRootNode (start action display)
AbstractBotAction<?> handle = ((CraftBotAction<?, ?>) action).getHandle();
GuiRootNode guiData = handle.getGuiData();
ItemStack item = guiData != null ? guiData.getItemStack() : new ItemStack(Items.PAPER);
String actionName = action.getName();
String actionHash = action.getUUID().toString();
item.set(DataComponents.CUSTOM_NAME, Component.literal("§c" + actionName));
// Store action hash in CUSTOM_DATA for later retrieval
CompoundTag customTag = new CompoundTag();
customTag.putString("action_hash", actionHash);
item.set(DataComponents.CUSTOM_DATA, CustomData.of(customTag));
List<Component> loreLines = new ArrayList<>();
CompoundTag nbt = new CompoundTag();
((CraftBotAction<?, ?>) action).getHandle().save(nbt);
nbt.forEach((key, tag) -> loreLines.add(Component.literal("§7" + key + ": §f" + tag)));
loreLines.add(Component.literal("§7Index: §f" + index));
loreLines.add(Component.literal("§7Hash: §f" + actionHash.substring(0, 8)));
loreLines.add(Component.literal("§6Click to stop this action"));
item.set(DataComponents.LORE, new ItemLore(loreLines));
return item;
}
/**
* Get the action hash (UUID string) at a specific slot (for STOP action type)
* Returns null if not found or not in STOP mode
*/
@Nullable
public String getActionHashAtSlot(int slot) {
if (this.selectedActionType != ActionType.ACTION_STOP) {
return null;
}
// Read the action hash directly from the ItemStack's CUSTOM_DATA
ItemStack item = this.getItem(slot);
if (item.isEmpty()) {
return null;
}
CustomData customData = item.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (tag.contains("action_hash")) {
return tag.getString("action_hash").get();
}
return null;
}
/**
* Get the ActionType at a specific slot, accounting for pagination offset.
* Returns null if the slot is not a valid content slot or the index is out of bounds.
*/
@Nullable
public ActionType getActionTypeAtSlot(int slot) {
if (!this.isSelectingActionType) {
return null;
}
int contentIndex = -1;
for (int i = 0; i < CONTENT_SLOTS.length; i++) {
if (CONTENT_SLOTS[i] == slot) {
contentIndex = i;
break;
}
}
if (contentIndex == -1) {
return null;
}
ActionType[] actionTypes = ActionType.values();
int actualIndex = this.currentPage * CONTENT_SLOTS.length + contentIndex;
if (actualIndex >= 0 && actualIndex < actionTypes.length) {
return actionTypes[actualIndex];
}
return null;
}
/**
* Get the currently selected ActionType
*/
@Nullable
public ActionType getSelectedActionType() {
return this.selectedActionType;
}
/**
* Check if we're in action type selection mode
*/
public boolean isSelectingActionType() {
return this.isSelectingActionType;
}
public boolean canNavigateBack() {
return !this.navigationStack.isEmpty() || (!this.isSelectingActionType && this.selectedActionType != null);
}
public boolean canNavigateHome() {
return !this.isSelectingActionType && (this.currentNode != null || this.selectedActionType != null);
}
/**
* Check if the command builder can be executed.
* Returns true if there is a current node and it is confirmable.
*/
public boolean canExecuteCommandBuilder() {
return this.currentNode != null && this.currentNode.isConfirmable();
}
@Nullable
public GuiNode getCurrentNode() {
return this.currentNode;
}
/**
* Get the current parameter count (number of selected command nodes).
* This equals the navigation stack size plus 1 if currentNode is not null.
*/
public int getCurrentParameterCount() {
return this.navigationStack.size() + (this.currentNode != null ? 1 : 0);
}
public CraftBot getBot() {
return this.bot;
}
public CraftPlayer getPlayer() {
return this.player;
}
@Override
public boolean stillValid(@NotNull Player player) {
return this.player.getHandle() == player && this.bot.isValid();
}
}
@@ -0,0 +1,279 @@
package fun.bm.lophine.bot;
import com.mojang.logging.LogUtils;
import fun.bm.lophine.bot.action.gui.ActionType;
import fun.bm.lophine.bot.action.gui.GuiNode;
import fun.bm.lophine.bot.action.gui.GuiRootNode;
import fun.bm.lophine.carpet.config.modules.FakePlayerCompatConfig;
import fun.bm.lophine.config.modules.function.FakeplayerConfig;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ContainerInput;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.inventory.CraftInventory;
import org.bukkit.craftbukkit.inventory.CraftInventoryView;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.entity.bot.CraftBot;
import java.rmi.UnexpectedException;
public class BotActionGuiMenu extends AbstractContainerMenu {
private final BotActionGuiContainer container;
private final CraftBot bot;
private final CraftPlayer player;
private CraftInventoryView view = null;
public BotActionGuiMenu(int containerId, Inventory inventory, BotActionGuiContainer container) {
super(MenuType.GENERIC_9x6, containerId);
this.container = container;
this.bot = container.getBot();
this.player = container.getPlayer();
for (int row = 0; row < 6; row++) {
for (int col = 0; col < 9; col++) {
this.addSlot(new Slot(container, col + row * 9, 8 + col * 18, 18 + row * 18) {
@Override
public boolean mayPickup(Player player) {
return false;
}
@Override
public boolean mayPlace(ItemStack stack) {
return false;
}
});
}
}
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 9; col++) {
this.addSlot(new Slot(inventory, col + row * 9 + 9, 8 + col * 18, 140 + row * 18));
}
}
for (int col = 0; col < 9; col++) {
this.addSlot(new Slot(inventory, col, 8 + col * 18, 198));
}
}
@Override
public org.bukkit.inventory.InventoryView getBukkitView() {
if (this.view == null) {
CraftInventory inventory = new CraftInventory(this.container);
this.view = new CraftInventoryView(
this.player,
inventory,
this
);
}
return this.view;
}
@Override
public boolean stillValid(@NotNull Player player) {
return this.container.stillValid(player);
}
@Override
public void clicked(int slotIndex, int buttonNum, ContainerInput containerInput, Player player) {
if (slotIndex >= 0 && slotIndex < 54) {
if (this.container.isBackButtonSlot(slotIndex)) {
this.container.navigateBack();
this.refreshSlots();
return;
}
if (this.container.isHomeButtonSlot(slotIndex)) {
this.container.navigateHome();
this.refreshSlots();
return;
}
// Handle pagination buttons
if (this.container.isPrevPageSlot(slotIndex)) {
this.container.prevPage();
this.refreshSlots();
return;
}
if (this.container.isNextPageSlot(slotIndex)) {
this.container.nextPage();
this.refreshSlots();
return;
}
// Handle command builder click
if (this.container.isCommandBuilderSlot(slotIndex) && this.container.canExecuteCommandBuilder()) {
this.executeCommandBuilder(player);
this.refreshSlots();
return;
}
// Handle ActionType selection (with pagination offset)
if (this.container.isSelectingActionType()) {
ActionType clickedType = this.container.getActionTypeAtSlot(slotIndex);
if (clickedType != null) {
this.container.selectActionType(clickedType);
this.refreshSlots();
}
return;
}
// Handle action stop click (when STOP action type is selected)
if (this.container.getSelectedActionType() == ActionType.ACTION_STOP) {
String actionHash = this.container.getActionHashAtSlot(slotIndex);
if (actionHash != null) {
this.stopActionByHash(actionHash, player);
this.refreshSlots();
}
return;
}
GuiNode node = this.container.getGuiNodeAtSlot(slotIndex);
if (node != null) {
this.handleNodeClick(node, player);
this.refreshSlots();
}
return;
}
super.clicked(slotIndex, buttonNum, containerInput, player);
}
private void handleNodeClick(GuiNode node, Player player) {
if (node instanceof GuiRootNode rootNode) {
// Get the selected action type
ActionType actionType = this.container.getSelectedActionType();
if (actionType == null) {
return;
}
int maxAllowedParameters = actionType.getMaxAllowedParameters();
// If the node has children, check parameter limit before navigating
if (!rootNode.getChildren().isEmpty()) {
// Calculate current parameter count (navigation stack size + 1 for current node if exists)
int currentParamCount = this.container.getCurrentParameterCount();
// If adding this node would exceed the limit, execute command instead of navigating
if (currentParamCount + 1 > maxAllowedParameters) {
this.executeCommand(rootNode, actionType, player);
// Close GUI after START action execution
if (actionType == ActionType.ACTION_START && player instanceof ServerPlayer serverPlayer) {
serverPlayer.closeContainer();
}
} else {
this.container.navigateToChild(rootNode);
}
} else {
// Execute command with t`he selected action type
this.executeCommand(rootNode, actionType, player);
// Close GUI after START action execution
if (actionType == ActionType.ACTION_START && player instanceof ServerPlayer serverPlayer) {
serverPlayer.closeContainer();
}
}
}
}
private void executeCommand(GuiRootNode node, ActionType actionType, Player player) {
try {
String actionPrefix = actionType.getCommandActionPrefix();
String actionSuffix = actionType.getCommandActionSuffix();
if (!actionSuffix.isEmpty()) {
actionSuffix = actionSuffix + " ";
}
String extra = actionPrefix + " " + bot.getName() + " " + actionSuffix;
String command = node.buildCommand(extra);
// Apply parameter limit based on ActionType
if (actionType.getMaxAllowedParameters() == 0) {
// For actions like STOP that don't allow parameters, trim to just "action botName"
command = actionPrefix + " " + bot.getName();
}
if (player instanceof ServerPlayer serverPlayer) {
MinecraftServer.getServer().getCommands().performPrefixedCommand(
serverPlayer.createCommandSourceStack(),
command
);
}
} catch (Exception e) {
LogUtils.getLogger().warn("Error executing command: ", e);
}
}
/**
* Execute the command from the command builder (book item).
* Uses the current node and selected action type.
*/
private void executeCommandBuilder(Player player) {
GuiNode currentNode = this.container.getCurrentNode();
ActionType actionType = this.container.getSelectedActionType();
if (currentNode instanceof GuiRootNode rootNode && actionType != null) {
this.executeCommand(rootNode, actionType, player);
// Close GUI after START action execution
if (actionType != ActionType.ACTION_STOP && player instanceof ServerPlayer serverPlayer) {
serverPlayer.closeContainer();
}
}
}
/**
* Stop a bot action by its hash (UUID string)
*/
private void stopActionByHash(String actionHash, Player player) {
try {
if (player instanceof ServerPlayer serverPlayer) {
String command = getStopActionCommand(actionHash);
MinecraftServer.getServer().getCommands().performPrefixedCommand(
serverPlayer.createCommandSourceStack(),
command
);
}
} catch (Exception e) {
LogUtils.getLogger().warn("Error stopping action with hash {}: ", actionHash, e);
}
// Always refresh to show updated action list after stop attempt
this.container.showCurrentBotActions();
}
private String getStopActionCommand(String actionHash) throws UnexpectedException {
boolean botCommand = FakeplayerConfig.enable;
boolean playerCommand = FakePlayerCompatConfig.commandPlayer;
String command;
if (botCommand) {
command = "bot ";
} else if (playerCommand) {
command = "player ";
} else {
throw new UnexpectedException("Unable to build String from commandNode.");
}
command = command + "action " + this.bot.getName() + " stop " + actionHash;
return command;
}
private void refreshSlots() {
for (int i = 0; i < 54; i++) {
Slot slot = this.slots.get(i);
ItemStack item = this.container.getItem(i);
slot.set(item);
}
}
@Override
public ItemStack quickMoveStack(Player player, int index) {
return ItemStack.EMPTY;
}
public BotActionGuiContainer getContainer() {
return this.container;
}
}
@@ -0,0 +1,115 @@
package fun.bm.lophine.bot.action.gui;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.ItemLore;
import java.util.ArrayList;
import java.util.List;
public enum ActionType {
ACTION_START("Start", "Start a action", Items.DYE.lime(), "action", "start"),
ACTION_STOP("Stop", "Stop a action or see this bot scheduled tasks", Items.DYE.red(), "action", "stop", 0);
private final String displayName;
private final String description;
private final Item item;
private final String commandActionPrefix;
private final String commandActionSuffix;
private final int maxAllowedParameters;
ActionType(String displayName, String description, Item item, String commandActionPrefix) {
this(displayName, description, item, commandActionPrefix, "", Integer.MAX_VALUE);
}
ActionType(String displayName, String description, Item item, String commandActionPrefix, String commandActionSuffix) {
this(displayName, description, item, commandActionPrefix, commandActionSuffix, Integer.MAX_VALUE);
}
ActionType(String displayName, String description, Item item, String commandActionPrefix, String commandActionSuffix, int maxAllowedParameters) {
this.displayName = displayName;
this.description = description;
this.item = item;
this.commandActionPrefix = commandActionPrefix;
this.commandActionSuffix = commandActionSuffix;
this.maxAllowedParameters = maxAllowedParameters;
}
public String getDisplayName() {
return this.displayName;
}
public String getDescription() {
return this.description;
}
public Item getItem() {
return this.item;
}
public String getCommandActionPrefix() {
return this.commandActionPrefix;
}
public String getCommandActionSuffix() {
return commandActionSuffix;
}
public GuiNode toConfirmNode(GuiRootNode targetNode) {
return new ActionConfirmNode(this.displayName, this.description, this.item, targetNode, this.commandActionPrefix, this.maxAllowedParameters);
}
public int getMaxAllowedParameters() {
return maxAllowedParameters;
}
/**
* A special node used in the action type confirmation UI.
* It holds a reference to the target GuiRootNode and the selected action string.
*/
public static class ActionConfirmNode extends GuiNode {
private final GuiRootNode targetNode;
private final String action;
private final int maxAllowedParameters;
public ActionConfirmNode(String name, String description, Item item, GuiRootNode targetNode, String action, int maxAllowedParameters) {
super(name, description, item);
this.targetNode = targetNode;
this.action = action;
this.maxAllowedParameters = maxAllowedParameters;
}
public GuiRootNode getTargetNode() {
return this.targetNode;
}
public String getAction() {
return this.action;
}
public int getMaxAllowedParameters() {
return this.maxAllowedParameters;
}
@Override
public ItemStack getItemStack() {
ItemStack itemStack = super.getItemStack();
// Add parameter limit info to lore
List<Component> loreLines = new ArrayList<>();
if (this.description != null && !this.description.isEmpty()) {
loreLines.add(Component.literal(this.description));
}
if (!loreLines.isEmpty()) {
itemStack.set(DataComponents.LORE, new ItemLore(loreLines));
}
return itemStack;
}
}
}
@@ -0,0 +1,52 @@
package fun.bm.lophine.bot.action.gui;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.ItemLore;
import java.util.List;
public class GuiNode {
protected final String name;
protected final String description;
protected final Item item;
protected final boolean confirmable;
private static final Item defaultItem = Items.PAPER;
public GuiNode(String name, String description, Item item, boolean confirmable) {
this.name = name;
this.description = description;
this.item = item;
this.confirmable = confirmable;
}
public GuiNode(String name, String description, Item item) {
this(name, description, item, true);
}
public String getName() {
return this.name;
}
public String getDescription() {
return this.description;
}
public boolean isConfirmable() {
return this.confirmable;
}
public ItemStack getItemStack() {
ItemStack itemStack = new ItemStack(this.item == null ? defaultItem : this.item).copy();
itemStack.set(DataComponents.CUSTOM_NAME, Component.literal(this.name));
if (this.description != null && !this.description.isEmpty()) {
ItemLore lore = new ItemLore(List.of(Component.literal(this.description)));
itemStack.set(DataComponents.LORE, lore);
}
return itemStack;
}
}
@@ -0,0 +1,78 @@
package fun.bm.lophine.bot.action.gui;
import fun.bm.lophine.carpet.config.modules.FakePlayerCompatConfig;
import fun.bm.lophine.config.modules.function.FakeplayerConfig;
import net.minecraft.world.item.Item;
import java.rmi.UnexpectedException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;
public class GuiRootNode extends GuiNode {
protected final Set<GuiNode> children;
protected final String commandNode;
public GuiRootNode(String name, String description, Item item, String commandNode) {
this(name, description, item, commandNode, true);
}
public GuiRootNode(String name, String description, Item item, String commandNode, boolean confirmable) {
this(name, description, item, new LinkedHashSet<>(), commandNode, confirmable);
}
public GuiRootNode(String name, String description, Item item, Set<GuiNode> children, String commandNode) {
this(name, description, item, children, commandNode, true);
}
public GuiRootNode(String name, String description, Item item, Set<GuiNode> children, String commandNode, boolean confirmable) {
super(name, description, item, confirmable);
this.children = children;
this.commandNode = commandNode;
}
public final void child(GuiNode... node) {
this.children.addAll(List.of(node));
}
@SafeVarargs
public final void child(Supplier<? extends GuiNode>... node) {
this.children.addAll(Stream.of(node).map(Supplier::get).toList());
}
public Set<GuiNode> getChildren() {
return this.children;
}
public Set<GuiNode> getAllFurthestChildren() {
if (this.children.isEmpty()) {
Set<GuiNode> result = new LinkedHashSet<>();
result.add(this);
return result;
}
Set<GuiNode> furthestChildren = new LinkedHashSet<>();
for (GuiNode child : this.children) {
furthestChildren.addAll(((GuiRootNode) child).getAllFurthestChildren());
}
return furthestChildren;
}
public String getCommandNode() {
return this.commandNode;
}
public String buildCommand(String extra) throws UnexpectedException {
boolean botCommand = FakeplayerConfig.enable;
boolean playerCommand = FakePlayerCompatConfig.commandPlayer;
if (botCommand) {
return "bot " + extra + this.getCommandNode();
}
if (playerCommand) {
return "player " + extra + this.getCommandNode();
}
throw new UnexpectedException("Unable to build String from commandNode.");
}
}
@@ -0,0 +1,37 @@
package fun.bm.lophine.bot.action.gui;
import net.minecraft.world.item.Item;
import java.rmi.UnexpectedException;
import java.util.LinkedHashSet;
import java.util.Set;
public class GuiSubNode extends GuiRootNode {
protected final GuiNode parent;
public GuiSubNode(String name, String description, Item item, GuiNode parent, Set<GuiNode> children, String commandNode) {
this(name, description, item, parent, children, commandNode, true);
}
public GuiSubNode(String name, String description, Item item, GuiNode parent, Set<GuiNode> children, String commandNode, boolean confirmable) {
super(name, description, item, children, commandNode, confirmable);
this.parent = parent;
}
public GuiSubNode(String name, String description, Item item, GuiNode parent, String commandNode) {
this(name, description, item, parent, commandNode, true);
}
public GuiSubNode(String name, String description, Item item, GuiNode parent, String commandNode, boolean confirmable) {
this(name, description, item, parent, new LinkedHashSet<>(), commandNode, confirmable);
}
public GuiNode getParent() {
return this.parent;
}
@Override
public String buildCommand(String extra) throws UnexpectedException {
return ((GuiRootNode) parent).buildCommand(extra) + " " + getCommandNode();
}
}
@@ -40,6 +40,11 @@ public class FakeplayerConfig implements IConfigModule {
Regeneration amount for fakeplayers""")
public static double regenAmount = 0.0;
@ConfigInfo(name = "open-action-gui", comments = """
Allow opening fakeplayer action gui,
need sneak to open if you enabled inventory open gui""")
public static boolean canOpenActionGui = false;
@ConfigInfo(name = "use-action", comments = """
Allow fakeplayers to use actions""")
public static boolean canUseAction = true;