[Feature] Creative-like pick block with withdrawl from AE system (#945)

Co-authored-by: Martin Robertz <[email protected]>
Co-authored-by: Kogepan229 <[email protected]>
Co-authored-by: EnderProyects <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-01-25 05:42:04 +00:00
1 parent 9746e3a489
commit 50aaed2731
11 files changed
+553 -4

No files matched your search

+1 -1
View File
@@ -33,7 +33,7 @@ dependencies {
}
compileOnly('com.github.GTNewHorizons:Baubles-Expanded:2.2.6-GTNH:dev')
compileOnly("org.jetbrains:annotations:26.0.2")
//Uncomment to add thermal expansion + foundation for testing
@@ -8,6 +8,8 @@ public enum ActionKey {
SEARCH_CONNECTED_INVENTORIES(Keyboard.KEY_NONE),
PICK_BLOCK(Keyboard.KEY_NONE),
CONTROL_OPERATION(Keyboard.KEY_LCONTROL);
private final int defaultKey;
@@ -41,6 +41,8 @@ import net.minecraftforge.common.MinecraftForge;
import org.lwjgl.opengl.GL11;
import com.gtnewhorizon.gtnhlib.event.PickBlockEvent;
import appeng.api.parts.CableRenderMode;
import appeng.api.util.AEColor;
import appeng.block.AEBaseBlock;
@@ -109,6 +111,7 @@ public class ClientHelper extends ServerHelper {
MinecraftForge.EVENT_BUS.register(new HighlighterManager());
MinecraftForge.EVENT_BUS.register(BlockRendererPreviewEvent.getInstance());
FMLCommonHandler.instance().bus().register(BlockRendererPreviewEvent.getInstance());
FMLCommonHandler.instance().bus().register(new KeyBindHandler());
for (ActionKey key : ActionKey.values()) {
final KeyBinding binding = new KeyBinding(key.getTranslationKey(), key.getDefaultKey(), KEY_CATEGORY);
@@ -448,6 +451,26 @@ public class ClientHelper extends ServerHelper {
}
}
/**
* Do not run the vanilla pick block logic if it is double bound with the AE2 pick block keybind, and the player is
* not in creative mode. The vanilla pick block event should be canceled and only the AE2 pick block logic should
* run. This is to prevent conflicts from the client-side vanilla pick-block and the server-side AE2 pick block.
*/
@SubscribeEvent
public void onPickBlockEvent(final PickBlockEvent event) {
var minecraft = Minecraft.getMinecraft();
if (!minecraft.theWorld.isRemote) {
return;
}
var isCreative = minecraft.thePlayer.capabilities.isCreativeMode;
var vanillaKeybind = minecraft.gameSettings.keyBindPickBlock.getKeyCode();
var ae2Keybind = CommonHelper.proxy.getKeybind(ActionKey.PICK_BLOCK);
if (!isCreative && vanillaKeybind == ae2Keybind) {
event.setCanceled(true);
}
}
@Override
public boolean isKeyPressed(ActionKey key) {
return this.bindings.get(key).isPressed();
@@ -0,0 +1,61 @@
package appeng.client;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.EntityClientPlayerMP;
import net.minecraft.util.MovingObjectPosition;
import appeng.core.CommonHelper;
import appeng.core.sync.network.NetworkHandler;
import appeng.core.sync.packets.PacketPickBlock;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.InputEvent;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
@SideOnly(Side.CLIENT)
public class KeyBindHandler {
@SideOnly(Side.CLIENT)
@SubscribeEvent
public void onKeyInput(InputEvent.KeyInputEvent event) {
if (CommonHelper.proxy.isKeyPressed(ActionKey.PICK_BLOCK)) {
handlePickBlock();
}
}
@SideOnly(Side.CLIENT)
@SubscribeEvent
public void onMouseInput(InputEvent.MouseInputEvent event) {
if (CommonHelper.proxy.isKeyPressed(ActionKey.PICK_BLOCK)) {
handlePickBlock();
}
}
private void handlePickBlock() {
Minecraft minecraft = Minecraft.getMinecraft();
if (minecraft.currentScreen != null) return; // Don't act if a GUI is open
EntityClientPlayerMP player = minecraft.thePlayer;
if (player == null) return;
// Use vanilla pick block when in creative
if (player.capabilities.isCreativeMode) {
return;
}
// Get the block the player is currently looking at
MovingObjectPosition movingObject = minecraft.objectMouseOver;
if (movingObject == null || movingObject.typeOfHit != MovingObjectPosition.MovingObjectType.BLOCK) {
return;
}
final var packet = new PacketPickBlock(
movingObject.blockX,
movingObject.blockY,
movingObject.blockZ,
movingObject.sideHit,
movingObject.hitVec);
NetworkHandler.instance.sendToServer(packet);
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ public final class AppEng {
+ '.' // revisionVersion
+ net.minecraftforge.common.ForgeVersion.buildVersion
+ ",);" // buildVersion
+ "required-after:gtnhlib@[0.8.34,)"; // require gtnhlib
+ "required-after:gtnhlib@[0.9.5,)"; // require gtnhlib
@Nonnull
private static final AppEng INSTANCE = new AppEng();
@@ -53,6 +53,7 @@ public enum PlayerMessages implements Localization {
MachineHighlightedNamed,
StorageInOtherDim,
StorageHighlighted,
PickBlockTerminalNotFound,
PatternRepeaterProvider,
PatternRepeaterAccessor;
@@ -49,6 +49,7 @@ import appeng.core.sync.packets.PacketPartialItem;
import appeng.core.sync.packets.PacketPatternMultiSet;
import appeng.core.sync.packets.PacketPatternSlot;
import appeng.core.sync.packets.PacketPatternValueSet;
import appeng.core.sync.packets.PacketPickBlock;
import appeng.core.sync.packets.PacketPinsUpdate;
import appeng.core.sync.packets.PacketSwapSlots;
import appeng.core.sync.packets.PacketSwitchGuis;
@@ -125,8 +126,9 @@ public class AppEngPacketHandlerBase {
PACKET_OPTIMIZE_PATTERNS(PacketOptimizePatterns.class),
PACKET_NETWORK_STATUS_SELECTED(PacketNetworkStatusSelected.class),
PACKET_PINS_UPDATE(PacketPinsUpdate.class),
PACKET_MONITORABLE_ACTION(PacketMonitorableAction.class),
PACKET_HIGHLIGHT_BLOCKS(PacketHighlightBlockStorage.class),
PACKET_PICK_BLOCK(PacketPickBlock.class),
PACKET_MONITORABLE_ACTION(PacketMonitorableAction.class),
PACKET_VIRTUAL_SLOT(PacketVirtualSlot.class),
PACKET_COLOR_SELECT(PacketColorSelect.class);
@@ -0,0 +1,278 @@
package appeng.core.sync.packets;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.block.Block;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.init.Blocks;
import net.minecraft.item.Item;
import net.minecraft.item.ItemBlock;
import net.minecraft.item.ItemStack;
import net.minecraft.network.play.server.S09PacketHeldItemChange;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import appeng.api.AEApi;
import appeng.api.config.Actionable;
import appeng.api.networking.IGridHost;
import appeng.api.networking.security.PlayerSource;
import appeng.api.networking.storage.IStorageGrid;
import appeng.api.storage.IMEInventoryHandler;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IAEStack;
import appeng.core.localization.PlayerMessages;
import appeng.core.sync.AppEngPacket;
import appeng.core.sync.network.INetworkInfo;
import appeng.util.PlayerInventoryUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class PacketPickBlock extends AppEngPacket {
private final int blockX;
private final int blockY;
private final int blockZ;
private final int blockSide;
private final Vec3 hitVec;
// Reflection
public PacketPickBlock(final ByteBuf stream) {
this.blockX = stream.readInt();
this.blockY = stream.readInt();
this.blockZ = stream.readInt();
this.blockSide = stream.readInt();
double hitVecX = stream.readDouble();
double hitVecY = stream.readDouble();
double hitVecZ = stream.readDouble();
this.hitVec = Vec3.createVectorHelper(hitVecX, hitVecY, hitVecZ);
}
// Public API
public PacketPickBlock(int blockX, int blockY, int blockZ, int blockSide, Vec3 hitVec) {
this.blockX = blockX;
this.blockY = blockY;
this.blockZ = blockZ;
this.blockSide = blockSide;
this.hitVec = hitVec;
final ByteBuf data = Unpooled.buffer();
data.writeInt(this.getPacketID());
data.writeInt(this.blockX);
data.writeInt(this.blockY);
data.writeInt(this.blockZ);
data.writeInt(this.blockSide);
data.writeDouble(this.hitVec.xCoord);
data.writeDouble(this.hitVec.yCoord);
data.writeDouble(this.hitVec.zCoord);
this.configureWrite(data);
}
@Override
public void serverPacketData(final INetworkInfo network, final AppEngPacket packet, final EntityPlayer player) {
final EntityPlayerMP sender = (EntityPlayerMP) player;
ItemStack itemToFind = getPickBlock(sender, this.blockX, this.blockY, this.blockZ);
// 1. Scan through the player's main inventory to categorize existing stacks of the target block:
// - If a full stack (stackSize >= maxStackSize) is found, record its slot and stop searching.
// This indicates the player already has the maximum possible stack, so no withdrawal is needed.
// - Otherwise, collect all partial stack slots (stacks that match the item but aren't full).
// Partial stacks will be consolidated in a later step.
int fullStackSlot = -1;
List<Integer> partialStackSlotsList = new ArrayList<>();
for (int i = 0; i < sender.inventory.mainInventory.length; i++) {
ItemStack stackInSlot = sender.inventory.mainInventory[i];
if (stackInSlot != null && stackInSlot.isItemEqual(itemToFind)
&& ItemStack.areItemStackTagsEqual(stackInSlot, itemToFind)) {
if (stackInSlot.stackSize >= stackInSlot.getMaxStackSize()) {
fullStackSlot = i;
break; // Found a full stack, no need to continue consolidating.
}
partialStackSlotsList.add(i);
}
}
// 2. If a full stack already exists, move it into the correct slot
if (fullStackSlot >= 0) {
movePickBlockItemStack(sender, fullStackSlot);
return;
}
// 3. If there are no partial stacks and the player's inventory is full,
// then return since we cannot add a retrieved stack to a full inventory
int nextEmptySlot = sender.inventory.getFirstEmptyStack();
if (partialStackSlotsList.isEmpty() && nextEmptySlot == -1) {
return;
}
// 4. Consolidate all partial stacks of target block into 1 ItemStack.
// If a full stack is obtained, set it as the active slot and return.
ItemStack consolidatedStack = null;
int consolidatedStackSlot = -1;
for (Integer partialStackSlot : partialStackSlotsList) {
if (consolidatedStack == null) {
consolidatedStack = sender.inventory.getStackInSlot(partialStackSlot);
consolidatedStackSlot = partialStackSlot;
} else {
var newStack = PlayerInventoryUtil
.consolidateItemStacks(sender, partialStackSlot, consolidatedStackSlot);
if (newStack != null) {
consolidatedStack = newStack;
}
}
// Check if we created a full stack of items
if (consolidatedStack.stackSize == consolidatedStack.getMaxStackSize()) {
movePickBlockItemStack(sender, consolidatedStackSlot);
return;
}
}
// Determine which of consolidated ItemStack or nextEmptySlot to use as our pick block ItemStack
int pickBlockSlot = consolidatedStack == null ? nextEmptySlot : consolidatedStackSlot;
ItemStack pickBlockItemStack = consolidatedStack;
// 5. Calculate withdrawal amount
int amountToWithdraw = pickBlockItemStack == null ? itemToFind.getMaxStackSize()
: itemToFind.getMaxStackSize() - pickBlockItemStack.stackSize;
if (amountToWithdraw <= 0) {
return;
}
// Ensure the player has a wireless terminal with access to an AE2 network.
ItemStack wirelessTerminal = PlayerInventoryUtil.getFirstWirelessTerminal(sender);
if (wirelessTerminal == null) {
sender.addChatMessage(PlayerMessages.PickBlockTerminalNotFound.toChat());
return;
}
var wirelessInventory = getWirelessItemInventory(sender, wirelessTerminal);
if (wirelessInventory == null) {
return;
}
// Create an IAEItemStack for the target block with the calculated amount
ItemStack targetItemStack = itemToFind.copy();
targetItemStack.stackSize = amountToWithdraw;
IAEItemStack targetAeItemStack = AEApi.instance().storage().createItemStack(targetItemStack);
if (targetAeItemStack == null) {
return;
}
// 6. Extract items from the network
PlayerSource source = new PlayerSource(player, null);
IAEStack<IAEItemStack> extractedStack = wirelessInventory
.extractItems(targetAeItemStack, Actionable.MODULATE, source);
if (extractedStack instanceof IAEItemStack extractedAeItemStack && extractedStack.getStackSize() > 0) {
ItemStack itemsToGive = extractedAeItemStack.getItemStack();
// Update the player's inventory with the withdrawn items
if (itemsToGive != null && itemsToGive.stackSize > 0) {
if (pickBlockItemStack == null) {
pickBlockItemStack = itemsToGive;
sender.inventory.setInventorySlotContents(pickBlockSlot, pickBlockItemStack);
} else {
pickBlockItemStack.stackSize += itemsToGive.stackSize;
}
}
}
// If the pick block item stack is still null, we had no consolidated stacks and did not withdraw any items.
// So, we do nothing.
if (pickBlockItemStack == null) {
return;
}
// Move the target item stack to the correct slot in the player's hotbar.
movePickBlockItemStack(sender, pickBlockSlot);
}
private IMEInventoryHandler<IAEItemStack> getWirelessItemInventory(EntityPlayer player,
ItemStack wirelessTerminal) {
if (wirelessTerminal == null) {
return null;
}
if (!AEApi.instance().registries().wireless().performCheck(wirelessTerminal, player)) {
return null;
}
var wirelessHandler = AEApi.instance().registries().wireless().getWirelessTerminalHandler(wirelessTerminal);
if (wirelessHandler == null) {
return null;
}
var encryptionKey = wirelessHandler.getEncryptionKey(wirelessTerminal);
if (encryptionKey == null) {
return null;
}
var securityTerminal = (IGridHost) AEApi.instance().registries().locatable()
.getLocatableBy(Long.parseLong(encryptionKey));
if (securityTerminal == null) {
return null;
}
var wirelessGridNode = securityTerminal.getGridNode(ForgeDirection.UNKNOWN);
if (wirelessGridNode == null) {
return null;
}
var wirelessGrid = wirelessGridNode.getGrid();
if (wirelessGrid == null) {
return null;
}
IStorageGrid wirelessGridCache = wirelessGrid.getCache(IStorageGrid.class);
if (wirelessGridCache == null) {
return null;
}
return wirelessGridCache.getItemInventory();
}
/**
* Set the position of the pick block ItemStack. If it is in the hotbar, set the active slot to the pick block slot.
* If it is not in the hotbar, move it to the first empty slot in the hotbar and set it as active slot. If there is
* no available hotbar slot, swap it with the active slot.
*
* @param player the player whose inventory to be modified
* @param pickBlockInventorySlot the inventory slot of the ItemStack to move
*/
private void movePickBlockItemStack(EntityPlayerMP player, int pickBlockInventorySlot) {
var firstEmptyHotbarSlot = PlayerInventoryUtil.getFirstEmptyHotbarSlot(player);
if (pickBlockInventorySlot <= 8) {
PlayerInventoryUtil.setSlotAsActiveSlot(player, pickBlockInventorySlot);
} else if (firstEmptyHotbarSlot >= 0) {
player.inventory.currentItem = firstEmptyHotbarSlot;
player.playerNetServerHandler.sendPacket(new S09PacketHeldItemChange(firstEmptyHotbarSlot));
PlayerInventoryUtil.setSlotAsActiveSlot(player, pickBlockInventorySlot);
} else {
PlayerInventoryUtil.setSlotAsActiveSlot(player, pickBlockInventorySlot);
}
}
/**
* Copy of the vanilla pick block function, as the vanilla version uses a client-side only function ( getItem() ).
*/
private ItemStack getPickBlock(EntityPlayerMP player, int x, int y, int z) {
// Get the target block
World world = player.worldObj;
Block targetBlock = world.getBlock(this.blockX, this.blockY, this.blockZ);
if (targetBlock == null || targetBlock == Blocks.air) {
return null;
}
Item item = Item.getItemFromBlock(targetBlock);
if (item == null) {
return null;
}
Block block = item instanceof ItemBlock ? Block.getBlockFromItem(item) : targetBlock;
return new ItemStack(item, 1, block.getDamageValue(world, x, y, z));
}
}
+1 -1
View File
@@ -177,7 +177,7 @@ public class Platform {
private static final DecimalFormat df = new DecimalFormat("#.##");
public static final boolean isAE2FCLoaded = Loader.isModLoaded("ae2fc");
public static final boolean isEIOLoaded = Loader.isModLoaded("EnderIO");
public static boolean isBaublesLoaded = Loader.isModLoaded("Baubles");
public static final boolean isBaublesLoaded = Loader.isModLoaded("Baubles|Expanded");
static {
BYTE_LIMIT = new double[10];
@@ -0,0 +1,180 @@
package appeng.util;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.network.play.server.S09PacketHeldItemChange;
import net.minecraft.network.play.server.S2FPacketSetSlot;
import appeng.api.AEApi;
import baubles.api.BaublesApi;
/**
* A collection of utility functions for manipulating player inventories.
*/
public class PlayerInventoryUtil {
/**
* Sets the specified inventory slot as the player's active (held) slot. If the target slot is in the hotbar (slots
* 0-8), that slot is set as the active slot. If the target slot is outside the hotbar, it swaps the contents of
* that slot with the current active slot.
*
* @param player The player whose active slot should be changed.
* @param slot The inventory slot index to make active (0-8 for hotbar, 9+ for main inventory).
*/
public static void setSlotAsActiveSlot(EntityPlayerMP player, int slot) {
if (slot >= 0 && slot <= 8) {
player.inventory.currentItem = slot;
player.playerNetServerHandler.sendPacket(new S09PacketHeldItemChange(slot));
} else {
swapInventorySlots(player, player.inventory.currentItem, slot);
}
player.inventory.markDirty();
}
/**
* Moves an ItemStack from a source slot to a destination slot in the player's inventory. If the destination slot is
* occupied, the items are swapped.
*
* @param player The player whose inventory is to be modified.
* @param slot1 The index of the first ItemStack to move.
* @param slot2 The index of the second ItemStack to move.
*/
public static void swapInventorySlots(EntityPlayerMP player, int slot1, int slot2) {
if (slot1 == slot2) {
return;
}
// Get the stacks from both slots
ItemStack sourceStack = player.inventory.getStackInSlot(slot1);
ItemStack destinationStack = player.inventory.getStackInSlot(slot2);
// Copy the item stacks to prevents any potential shared reference issue.
if (sourceStack != null) {
sourceStack = sourceStack.copy();
}
if (destinationStack != null) {
destinationStack = destinationStack.copy();
}
// Set the destination slot with the source stack (even if it's null)
player.inventory.setInventorySlotContents(slot2, sourceStack);
// Set the source slot with the original destination stack
player.inventory.setInventorySlotContents(slot1, destinationStack);
// Mark the inventory as dirty to ensure changes are saved and synced
player.inventory.markDirty();
player.playerNetServerHandler.sendPacket(new S2FPacketSetSlot(-2, slot2, sourceStack));
player.playerNetServerHandler.sendPacket(new S2FPacketSetSlot(-2, slot1, destinationStack));
}
/**
* Consolidate ItemStacks from a source slot to a destination slot in the player's inventory.
*
* @param player The player whose inventory is to be modified.
* @param sourceSlot The index of the slot to move the item from.
* @param destinationSlot The index of the slot to move the item to.
* @return the consolidated item stack
*/
public static ItemStack consolidateItemStacks(EntityPlayerMP player, int sourceSlot, int destinationSlot) {
ItemStack sourceStack = player.inventory.getStackInSlot(sourceSlot);
ItemStack destinationStack = player.inventory.getStackInSlot(destinationSlot);
// We can only consolidate if both the source and destination slots have item stacks.
if (sourceStack == null || destinationStack == null) {
return destinationStack;
}
if (!sourceStack.isItemEqual(destinationStack)
|| !ItemStack.areItemStackTagsEqual(sourceStack, destinationStack)) {
return destinationStack;
}
// Copy the item stacks to prevents any potential shared reference issue.
sourceStack = sourceStack.copy();
destinationStack = destinationStack.copy();
int missingQuantity = destinationStack.getMaxStackSize() - destinationStack.stackSize;
if (missingQuantity >= sourceStack.stackSize) {
destinationStack.stackSize = destinationStack.stackSize + sourceStack.stackSize;
sourceStack = null;
} else {
sourceStack.stackSize -= missingQuantity;
destinationStack.stackSize += missingQuantity;
}
// Update the inventory stacks
player.inventory.setInventorySlotContents(destinationSlot, destinationStack);
player.inventory.setInventorySlotContents(sourceSlot, sourceStack);
// Mark the inventory as dirty to ensure changes are saved and synced
player.inventory.markDirty();
player.playerNetServerHandler.sendPacket(new S2FPacketSetSlot(-2, destinationSlot, destinationStack));
player.playerNetServerHandler.sendPacket(new S2FPacketSetSlot(-2, sourceSlot, sourceStack));
return player.inventory.getStackInSlot(destinationSlot);
}
/**
* Finds the first wireless terminal in the player's inventory.
*
* @param player the player to check
* @return the wireless terminal ItemStack, or null if not found
*/
public static ItemStack getFirstWirelessTerminal(EntityPlayer player) {
// Check bauble slots
if (Platform.isBaublesLoaded) {
ItemStack terminal = getWirelessTerminalFromBaubles(player);
if (terminal != null) {
return terminal;
}
}
// Check hotbar and main inventory (slots 0-35)
for (int i = 0; i < player.inventory.mainInventory.length; i++) {
ItemStack stack = player.inventory.mainInventory[i];
if (stack != null && AEApi.instance().registries().wireless().isWirelessTerminal(stack)) {
return stack;
}
}
return null;
}
/**
* Finds the first empty hotbar slot.
*
* @param player the player whose inventory to check
* @return the slot number of the first empty hotbar slot, otherwise -1
*/
public static int getFirstEmptyHotbarSlot(EntityPlayer player) {
for (int i = 0; i <= 8; ++i) {
if (player.inventory.mainInventory[i] == null) {
return i;
}
}
return -1;
}
/**
* Finds the first wireless terminal in the player's bauble slots.
*
* @param player the player to check
* @return the wireless terminal ItemStack, or null if not found
*/
@cpw.mods.fml.common.Optional.Method(modid = "Baubles|Expanded")
public static ItemStack getWirelessTerminalFromBaubles(EntityPlayer player) {
IInventory baubles = BaublesApi.getBaubles(player);
if (baubles != null) {
for (int i = 0; i < baubles.getSizeInventory(); i++) {
ItemStack stack = baubles.getStackInSlot(i);
if (stack != null && AEApi.instance().registries().wireless().isWirelessTerminal(stack)) {
return stack;
}
}
}
return null;
}
}
@@ -137,6 +137,7 @@ chat.appliedenergistics2.MachineHighlighted=The %d is now highlighted at %d, %d,
chat.appliedenergistics2.MachineHighlightedNamed=The %d "%d" is now highlighted at %d, %d, %d
chat.appliedenergistics2.StorageHighlighted=The %d containing %d %d is now highlighted at %d, %d, %d
chat.appliedenergistics2.StorageInOtherDim=%d containing %d %d is located in dimension: %d and cant be highlighted
chat.appliedenergistics2.PickBlockTerminalNotFound=AE2 wireless terminal not found.
chat.appliedenergistics2.PatternRepeaterProvider=§5Provider§r - will provide pattern to current system from in front §5Accessor§r.
chat.appliedenergistics2.PatternRepeaterAccessor=§5Accessor§r - will provide pattern to in front §5Provider§r.
@@ -956,6 +957,7 @@ stat.ae2.TurnedCranks=Turned Cranks
# Keys
key.appliedenergistics2.category=Applied Energistics 2
key.pick_block.desc=AE2 Pick Block
key.toggle_focus.desc=Toggle search box focus
key.search_connected_inventories.desc=Search network inventories for item
key.control_operation.desc=Control operation