diff --git a/build.gradle b/build.gradle index 568e733c6..778c5ad55 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,9 @@ dependencies { implementation "blue.endless:jankson:${jankson_version}" implementation 'com.google.code.findbugs:jsr305:3.0.2' + compileOnly 'com.google.auto.service:auto-service:1.0-rc7' + annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7' + include "me.shedaniel.cloth:config-2:${cloth_config_version}" include "blue.endless:jankson:${jankson_version}" include 'javax.vecmath:vecmath:1.5.2' diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index 7160aef49..a5d0ea5e8 100644 --- a/src/main/java/dan200/computercraft/ComputerCraft.java +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -21,6 +21,7 @@ import dan200.computercraft.api.turtle.event.TurtleAction; import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.AddressRule; import dan200.computercraft.core.apis.http.websocket.Websocket; +import dan200.computercraft.core.asm.GenericSource; import dan200.computercraft.shared.common.ColourableRecipe; import dan200.computercraft.shared.computer.core.ClientComputerRegistry; import dan200.computercraft.shared.computer.core.ServerComputerRegistry; @@ -48,6 +49,7 @@ import dan200.computercraft.shared.turtle.upgrades.TurtleTool; import dan200.computercraft.shared.util.Config; import dan200.computercraft.shared.util.ImpostorRecipe; import dan200.computercraft.shared.util.ImpostorShapelessRecipe; +import dan200.computercraft.shared.util.ServiceUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -154,6 +156,7 @@ public final class ComputerCraft implements ModInitializer { Registry.register(Registry.LOOT_CONDITION_TYPE, new Identifier(ComputerCraft.MOD_ID, "player_creative"), PlayerCreativeLootCondition.TYPE); Registry.register(Registry.LOOT_CONDITION_TYPE, new Identifier(ComputerCraft.MOD_ID, "has_id"), HasComputerIdLootCondition.TYPE); init(); + GenericSource.setup( () -> ServiceUtil.loadServices( GenericSource.class )); } } diff --git a/src/main/java/dan200/computercraft/shared/Peripherals.java b/src/main/java/dan200/computercraft/shared/Peripherals.java index 85da4746f..9ef3bb514 100644 --- a/src/main/java/dan200/computercraft/shared/Peripherals.java +++ b/src/main/java/dan200/computercraft/shared/Peripherals.java @@ -17,6 +17,7 @@ import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheralProvider; +import dan200.computercraft.shared.peripheral.generic.GenericPeripheralProvider; import net.minecraft.block.entity.BlockEntity; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Direction; @@ -53,7 +54,7 @@ public final class Peripherals { } } - return null; + return GenericPeripheralProvider.getPeripheral(world, pos, side); } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java new file mode 100644 index 000000000..15ae9320d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java @@ -0,0 +1,78 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic; + +import dan200.computercraft.api.lua.*; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IDynamicPeripheral; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.block.entity.LockableContainerBlockEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.Nameable; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.locks.Lock; + +class GenericPeripheral implements IDynamicPeripheral +{ + private final String type; + private final BlockEntity tile; + private final List methods; + + GenericPeripheral( BlockEntity tile, List methods ) + { + Identifier type = BlockEntityType.getId(tile.getType()); + this.tile = tile; + this.type = type == null ? "unknown" : type.toString(); + this.methods = methods; + } + + @Nonnull + @Override + public String[] getMethodNames() + { + String[] names = new String[methods.size()]; + for( int i = 0; i < methods.size(); i++ ) names[i] = methods.get( i ).getName(); + return names; + } + + @Nonnull + @Override + public MethodResult callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments ) throws LuaException + { + return methods.get( method ).apply( context, computer, arguments ); + } + + @Nonnull + @Override + public String getType() + { + return type; + } + + @Nullable + @Override + public Object getTarget() + { + return tile; + } + + @Override + public boolean equals( @Nullable IPeripheral other ) + { + if( other == this ) return true; + if( !(other instanceof GenericPeripheral) ) return false; + + GenericPeripheral generic = (GenericPeripheral) other; + return tile == generic.tile && methods.equals( generic.methods ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java new file mode 100644 index 000000000..b37104fe5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java @@ -0,0 +1,46 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.asm.NamedMethod; +import dan200.computercraft.core.asm.PeripheralMethod; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +public class GenericPeripheralProvider +{ + @Nullable + public static IPeripheral getPeripheral( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + BlockEntity tile = world.getBlockEntity( pos ); + if( tile == null ) return null; + + ArrayList saturated = new ArrayList<>( 0 ); + + // This seems to add inventory methods, how??? + List> tileMethods = PeripheralMethod.GENERATOR.getMethods( tile.getClass() ); + if( !tileMethods.isEmpty() ) addSaturated( saturated, tile, tileMethods ); + + return saturated.isEmpty() ? null : new GenericPeripheral( tile, saturated ); + } + + private static void addSaturated( ArrayList saturated, Object target, List> methods ) + { + saturated.ensureCapacity( saturated.size() + methods.size() ); + for( NamedMethod method : methods ) + { + saturated.add( new SaturatedMethod( target, method ) ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java new file mode 100644 index 000000000..31a120e52 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java @@ -0,0 +1,58 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.core.asm.NamedMethod; +import dan200.computercraft.core.asm.PeripheralMethod; + +import javax.annotation.Nonnull; + +final class SaturatedMethod +{ + private final Object target; + private final String name; + private final PeripheralMethod method; + + SaturatedMethod( Object target, NamedMethod method ) + { + this.target = target; + this.name = method.getName(); + this.method = method.getMethod(); + } + + @Nonnull + MethodResult apply( @Nonnull ILuaContext context, @Nonnull IComputerAccess computer, @Nonnull IArguments args ) throws LuaException + { + return method.apply( target, context, computer, args ); + } + + @Nonnull + String getName() + { + return name; + } + + @Override + public boolean equals( Object obj ) + { + if( obj == this ) return true; + if( !(obj instanceof SaturatedMethod) ) return false; + + SaturatedMethod other = (SaturatedMethod) obj; + return method == other.method && target.equals( other.target ); + } + + @Override + public int hashCode() + { + return 31 * target.hashCode() + method.hashCode(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/data/BlockData.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/BlockData.java new file mode 100644 index 000000000..2d7038b29 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/BlockData.java @@ -0,0 +1,39 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic.data; + +import com.google.common.collect.ImmutableMap; +import net.minecraft.block.BlockState; +import net.minecraft.state.property.Property; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; + +public class BlockData +{ + @Nonnull + public static > T fill( @Nonnull T data, @Nonnull BlockState state ) + { + data.put("name", DataHelpers.getId( state.getBlock() ) ); + + Map stateTable = new HashMap<>(); + for (ImmutableMap.Entry, ? extends Comparable> entry : state.getEntries().entrySet()) { + Property property = entry.getKey(); + stateTable.put(property.getName(), getPropertyValue(property, entry.getValue())); + } + data.put("state", stateTable); + + return data; + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + private static Object getPropertyValue( Property property, Comparable value ) + { + if( value instanceof String || value instanceof Number || value instanceof Boolean ) return value; + return property.name( value ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/data/DataHelpers.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/DataHelpers.java new file mode 100644 index 000000000..3d69d2849 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/DataHelpers.java @@ -0,0 +1,54 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic.data; + +import net.minecraft.block.AbstractBlock; +import net.minecraft.block.Block; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.item.Item; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public final class DataHelpers +{ + private DataHelpers() + { } + + @Nonnull + public static Map getTags( @Nonnull Collection tags ) + { + Map result = new HashMap<>( tags.size() ); + for( Identifier location : tags ) result.put( location.toString(), true ); + return result; + } + + @Nullable + public static String getId( @Nonnull Block block ) + { + Identifier id = Registry.BLOCK.getId(block); + return id == null ? null : id.toString(); + } + + @Nullable + public static String getId( @Nonnull Item item ) + { + Identifier id = Registry.ITEM.getId(item); + return id == null ? null : id.toString(); + } + + @Nullable + public static String getId( @Nonnull Enchantment enchantment) + { + Identifier id = Registry.ENCHANTMENT.getId(enchantment); + return id == null ? null : id.toString(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/data/ItemData.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/ItemData.java new file mode 100644 index 000000000..8d7724b30 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/ItemData.java @@ -0,0 +1,155 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic.data; + +import com.google.gson.JsonParseException; +import dan200.computercraft.shared.util.NBTUtil; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.enchantment.EnchantmentHelper; +import net.minecraft.item.EnchantedBookItem; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.ListTag; +import net.minecraft.text.Text; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; + +/** + * Data providers for items. + */ +public class ItemData +{ + @Nonnull + public static > T fillBasicSafe( @Nonnull T data, @Nonnull ItemStack stack ) + { + data.put( "name", DataHelpers.getId( stack.getItem() ) ); + data.put( "count", stack.getCount() ); + + return data; + } + + @Nonnull + public static > T fillBasic( @Nonnull T data, @Nonnull ItemStack stack ) + { + fillBasicSafe( data, stack ); + String hash = NBTUtil.getNBTHash( stack.getTag() ); + if( hash != null ) data.put( "nbt", hash ); + + return data; + } + + @Nonnull + public static > T fill( @Nonnull T data, @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return data; + + fillBasic( data, stack ); + + data.put( "displayName", stack.toHoverableText().getString() ); + data.put( "maxCount", stack.getMaxCount() ); + + if( stack.isDamageable() ) + { + data.put( "damage", stack.getDamage() ); + data.put( "maxDamage", stack.getMaxDamage() ); + } + + if( stack.isDamaged() ) + { + data.put( "durability", 1.0 - ( stack.getDamage() / stack.getMaxDamage() ) ); + } + + /* + * Used to hide some data from ItemStack tooltip. + * @see https://minecraft.gamepedia.com/Tutorials/Command_NBT_tags + * @see ItemStack#getTooltip + */ + CompoundTag tag = stack.getTag(); + int hideFlags = tag != null ? tag.getInt( "HideFlags" ) : 0; + + List> enchants = getAllEnchants( stack, hideFlags ); + if( !enchants.isEmpty() ) data.put( "enchantments", enchants ); + + if( tag != null && tag.getBoolean( "Unbreakable" ) && (hideFlags & 4) == 0 ) + { + data.put( "unbreakable", true ); + } + + return data; + } + + @Nullable + private static Text parseTextComponent( @Nonnull Tag x ) + { + try + { + return Text.Serializer.fromJson( x.toString() ); + } + catch( JsonParseException e ) + { + return null; + } + } + + /** + * Retrieve all visible enchantments from given stack. Try to follow all tooltip rules : order and visibility. + * + * @param stack Stack to analyse + * @param hideFlags An int used as bit field to provide visibility rules. + * @return A filled list that contain all visible enchantments. + */ + @Nonnull + private static List> getAllEnchants( @Nonnull ItemStack stack, int hideFlags ) + { + ArrayList> enchants = new ArrayList<>( 0 ); + + if( stack.getItem() instanceof EnchantedBookItem && (hideFlags & 32) == 0 ) + { + addEnchantments( EnchantedBookItem.getEnchantmentTag( stack ), enchants ); + } + + if( stack.hasEnchantments() && (hideFlags & 1) == 0 ) + { + /* + * Mimic the EnchantmentHelper.getEnchantments(ItemStack stack) behavior without special case for Enchanted book. + * I'll do that to have the same data than ones displayed in tooltip. + * @see EnchantmentHelper.getEnchantments(ItemStack stack) + */ + addEnchantments( stack.getEnchantments(), enchants ); + } + + return enchants; + } + + /** + * Converts a Mojang enchant map to a Lua list. + * + * @param rawEnchants The raw NBT list of enchantments + * @param enchants The enchantment map to add it to. + * @see EnchantmentHelper + */ + private static void addEnchantments( @Nonnull ListTag rawEnchants, @Nonnull ArrayList> enchants ) + { + if( rawEnchants.isEmpty() ) return; + + enchants.ensureCapacity( enchants.size() + rawEnchants.size() ); + + + for( Map.Entry entry : EnchantmentHelper.fromTag( rawEnchants ).entrySet() ) + { + Enchantment enchantment = entry.getKey(); + Integer level = entry.getValue(); + HashMap enchant = new HashMap<>( 3 ); + enchant.put( "name", DataHelpers.getId( enchantment ) ); + enchant.put( "level", level ); + enchant.put( "displayName", enchantment.getName( level ).getString() ); + enchants.add( enchant ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/ArgumentHelpers.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/ArgumentHelpers.java new file mode 100644 index 000000000..119831409 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/ArgumentHelpers.java @@ -0,0 +1,37 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic.methods; + +import dan200.computercraft.api.lua.LuaException; + +/** + * A few helpers for working with arguments. + * + * This should really be moved into the public API. However, until I have settled on a suitable format, we'll keep it + * where it is used. + */ +final class ArgumentHelpers +{ + private ArgumentHelpers() + { + } + + public static void assertBetween( double value, double min, double max, String message ) throws LuaException + { + if( value < min || value > max || Double.isNaN( value ) ) + { + throw new LuaException( String.format( message, "between " + min + " and " + max ) ); + } + } + + public static void assertBetween( int value, int min, int max, String message ) throws LuaException + { + if( value < min || value > max ) + { + throw new LuaException( String.format( message, "between " + min + " and " + max ) ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java new file mode 100644 index 000000000..6642cccf2 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java @@ -0,0 +1,384 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic.methods; + +import com.google.auto.service.AutoService; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.asm.GenericSource; +import dan200.computercraft.shared.peripheral.generic.data.ItemData; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.ChestBlock; +import net.minecraft.block.InventoryProvider; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.ChestBlockEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Identifier; +import net.minecraft.util.Nameable; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static dan200.computercraft.shared.peripheral.generic.methods.ArgumentHelpers.assertBetween; + + + +/** + * Methods for interacting with inventories. + * + * @cc.module inventory + */ +@AutoService( GenericSource.class ) +public class InventoryMethods implements GenericSource +{ + @Nonnull + @Override + public Identifier id() + { + return new Identifier(ComputerCraft.MOD_ID, "inventory" ); + } + + /** + * Get the size of this inventory. + * + * @param inventory The current inventory. + * @return The number of slots in this inventory. + */ + @LuaFunction( mainThread = true ) + public static int size( Inventory inventory ) + { + // Get appropriate inventory for source peripheral + inventory = extractHandler(inventory); + + return inventory.size(); + } + + /** + * Get the name of this inventory. + * + * @param inventory The current inventory. + * @return The name of this inventory, or {@code nil} if not present. + */ + @LuaFunction( mainThread = true ) + public static String name( Nameable inventory ) + { + return inventory.hasCustomName() ? inventory.getName().asString() : null; + } + + /** + * List all items in this inventory. This returns a table, with an entry for each slot. + * + * Each item in the inventory is represented by a table containing some basic information, much like + * @link dan200.computercraft.shared.turtle.apis.TurtleAPI#getItemDetail includes. More information can be fetched + * with {@link #getItemDetail}. + * + * The table is sparse, and so empty slots will be `nil` - it is recommended to loop over using `pairs` rather than + * `ipairs`. + * + * @param inventory The current inventory. + * @return All items in this inventory. + * @cc.treturn { (table|nil)... } All items in this inventory. + */ + @LuaFunction( mainThread = true ) + public static Map> list( Inventory inventory ) + { + // Get appropriate inventory for source peripheral + inventory = extractHandler(inventory); + + Map> result = new HashMap<>(); + int size = inventory.size(); + for( int i = 0; i < size; i++ ) + { + ItemStack stack = inventory.getStack( i ); + if( !stack.isEmpty() ) result.put( i + 1, ItemData.fillBasic( new HashMap<>( 4 ), stack ) ); + } + + return result; + } + + /** + * Get detailed information about an item. + * + * @param inventory The current inventory. + * @param slot The slot to get information about. + * @return Information about the item in this slot, or {@code nil} if not present. + * @throws LuaException If the slot is out of range. + * @cc.treturn table Information about the item in this slot, or {@code nil} if not present. + */ + @Nullable + @LuaFunction( mainThread = true ) + public static Map getItemDetail( Inventory inventory, int slot ) throws LuaException + { + // Get appropriate inventory + inventory = extractHandler(inventory); + + assertBetween( slot, 1, inventory.size(), "Slot out of range (%s)" ); + + ItemStack stack = inventory.getStack( slot - 1 ); + return stack.isEmpty() ? null : ItemData.fill( new HashMap<>(), stack ); + } + + /** + * Push items from one inventory to another connected one. + * + * This allows you to push an item in an inventory to another inventory on the same wired network. Both + * inventories must attached to wired modems which are connected via a cable. + * + * @param from Inventory to move items from. + * @param computer The current computer. + * @param toName The name of the peripheral/inventory to push to. This is the string given to @{peripheral.wrap}, + * and displayed by the wired modem. + * @param fromSlot The slot in the current inventory to move items to. + * @param limit The maximum number of items to move. Defaults to the current stack limit. + * @param toSlot The slot in the target inventory to move to. If not given, the item will be inserted into any slot. + * @return The number of transferred items. + * @throws LuaException If the peripheral to transfer to doesn't exist or isn't an inventory. + * @throws LuaException If either source or destination slot is out of range. + * @cc.see peripheral.getName Allows you to get the name of a @{peripheral.wrap|wrapped} peripheral. + * @cc.usage Wrap two chests, and push an item from one to another. + *
{@code
+     * local chest_a = peripheral.wrap("minecraft:chest_0")
+     * local chest_b = peripheral.wrap("minecraft:chest_1")
+     *
+     * chest_a.pushItems(peripheral.getName(chest_b), 1)
+     * }
+ */ + @LuaFunction( mainThread = true ) + public static int pushItems( + Inventory from, IComputerAccess computer, + String toName, int fromSlot, Optional limit, Optional toSlot + ) throws LuaException + { + // Get appropriate inventory for source peripheral + from = extractHandler(from); + + // Find location to transfer to + IPeripheral location = computer.getAvailablePeripheral( toName ); + if( location == null ) throw new LuaException( "Target '" + toName + "' does not exist" ); + + Inventory to = extractHandler( location.getTarget() ); + if( to == null ) throw new LuaException( "Target '" + toName + "' is not an inventory" ); + + // Validate slots + int actualLimit = limit.orElse( Integer.MAX_VALUE ); + assertBetween( fromSlot, 1, from.size(), "From slot out of range (%s)" ); + if( toSlot.isPresent() ) assertBetween( toSlot.get(), 1, to.size(), "To slot out of range (%s)" ); + + if( actualLimit <= 0 ) return 0; + return moveItem( from, fromSlot - 1, to, toSlot.orElse( 0 ) - 1, actualLimit ); + } + + /** + * Pull items from a connected inventory into this one. + * + * This allows you to transfer items between inventories on the same wired network. Both this and the source + * inventory must attached to wired modems which are connected via a cable. + * + * @param to Inventory to move items to. + * @param computer The current computer. + * @param fromName The name of the peripheral/inventory to pull from. This is the string given to @{peripheral.wrap}, + * and displayed by the wired modem. + * @param fromSlot The slot in the source inventory to move items from. + * @param limit The maximum number of items to move. Defaults to the current stack limit. + * @param toSlot The slot in current inventory to move to. If not given, the item will be inserted into any slot. + * @return The number of transferred items. + * @throws LuaException If the peripheral to transfer to doesn't exist or isn't an inventory. + * @throws LuaException If either source or destination slot is out of range. + * @cc.see peripheral.getName Allows you to get the name of a @{peripheral.wrap|wrapped} peripheral. + * @cc.usage Wrap two chests, and push an item from one to another. + *
{@code
+     * local chest_a = peripheral.wrap("minecraft:chest_0")
+     * local chest_b = peripheral.wrap("minecraft:chest_1")
+     *
+     * chest_a.pullItems(peripheral.getName(chest_b), 1)
+     * }
+ */ + @LuaFunction( mainThread = true ) + public static int pullItems( + Inventory to, IComputerAccess computer, + String fromName, int fromSlot, Optional limit, Optional toSlot + ) throws LuaException + { + // Get appropriate inventory for source peripheral + to = extractHandler(to); + + // Find location to transfer to + IPeripheral location = computer.getAvailablePeripheral( fromName ); + if( location == null ) throw new LuaException( "Source '" + fromName + "' does not exist" ); + + Inventory from = extractHandler( location.getTarget() ); + if( from == null ) throw new LuaException( "Source '" + fromName + "' is not an inventory" ); + + // Validate slots + int actualLimit = limit.orElse( Integer.MAX_VALUE ); + assertBetween( fromSlot, 1, from.size(), "From slot out of range (%s)" ); + if( toSlot.isPresent() ) assertBetween( toSlot.get(), 1, to.size(), "To slot out of range (%s)" ); + + if( actualLimit <= 0 ) return 0; + return moveItem( from, fromSlot - 1, to, toSlot.orElse( 0 ) - 1, actualLimit ); + } + + + /** + * Extracts the most appropriate inventory from the object + * e.g., the correct inventory for a double chest or a sided inventory. + * + * @param object The handler to move from. + * @return The appropriate Inventory. + */ + @Nullable + private static Inventory extractHandler( @Nullable Object object ) + { + Inventory inventory = null; + + if (object instanceof BlockEntity ) { + BlockEntity blockEntity = (BlockEntity) object; + World world = blockEntity.getWorld(); + BlockPos blockPos = blockEntity.getPos(); + BlockState blockState = world.getBlockState(blockPos); + Block block = blockState.getBlock(); + + if (block instanceof InventoryProvider) { + inventory = ((InventoryProvider)block).getInventory(blockState, world, blockPos); + } else if (blockEntity instanceof Inventory) { + inventory = (Inventory)blockEntity; + if (inventory instanceof ChestBlockEntity && block instanceof ChestBlock) { + inventory = ChestBlock.getInventory((ChestBlock) block, blockState, world, blockPos, true); + } + } + } + + return inventory; + } + + /** + * Move an item from one handler to another. + * + * @param from The handler to move from. + * @param fromSlot The slot to move from. + * @param to The handler to move to. + * @param toSlot The slot to move to. Use any number < 0 to represent any slot. + * @param limit The max number to move. {@link Integer#MAX_VALUE} for no limit. + * @return The number of items moved. + */ + private static int moveItem( Inventory from, int fromSlot, Inventory to, int toSlot, final int limit ) + { + + /* ORIGINAL FORGE CODE + // See how much we can get out of this slot + // ItemStack extracted = from.extractItem( fromSlot, limit, true ); + if( extracted.isEmpty() ) return 0; + + // Limit the amount to extract + int extractCount = Math.min( extracted.getCount(), limit ); + extracted.setCount( extractCount ); + + // ItemStack remainder = toSlot < 0 ? bItemHandlerHelper.insertItem( to, extracted, false ) : to.insertItem( toSlot, extracted, false ); + int inserted = remainder.isEmpty() ? extractCount : extractCount - remainder.getCount(); + if( inserted <= 0 ) return 0; + + // Remove the item from the original inventory. Technically this could fail, but there's little we can do + // about that. + from.extractItem( fromSlot, inserted, false ); + */ + + // Vanilla minecraft inventory manipulation code + Boolean recurse = false; + ItemStack source = from.getStack( fromSlot ); + int count = 0; + + // If target slot was selected, only push items to that slot. + if (toSlot >= 0) { + int space = amountStackCanAddFrom(to.getStack(toSlot), source, to); + if (space == 0) return 0; + count = space; + } + // If target slot not selected, push items where they will fit, possibly + // across slots (by recurring on this method). + else if (toSlot < 0) { + recurse = true; + int[] result = getFirstValidSlotAndSpace(source, to); + toSlot = result[0]; + if(toSlot < 0) return 0; + count = result[1]; + } + + // Respect slot restrictions + if (!to.isValid(toSlot, source)) { return 0; } + + // Compare count available in target ItemStack to limit specified. + count = Math.min(count, limit); + if (count == 0) return 0; + + // Mutate destination and source ItemStack + ItemStack destination = to.getStack(toSlot); + if (destination.isEmpty()) { + ItemStack newStack = source.copy(); + newStack.setCount(count); + to.setStack(toSlot, newStack); + } else { + destination.increment(count); + } + source.decrement(count); + if (source.isEmpty()) from.setStack(fromSlot, ItemStack.EMPTY); + + to.markDirty(); + from.markDirty(); + + // Recurse if no explicit destination slot and more items exist in source slot + // and limit hasn't been reached. Else, return items moved. + if (recurse && !source.isEmpty()) return count + moveItem(from, fromSlot, to, -1, limit - count); + return count; + } + + // Maybe there is a nicer existing way to do this in the minecraft codebase. I couldn't find it. + private static int[] getFirstValidSlotAndSpace(ItemStack fromStack, Inventory inventory) { + for (int i = 0; i < inventory.size(); i++) { + ItemStack stack = inventory.getStack(i); + int space = amountStackCanAddFrom(stack, fromStack, inventory); + if (space > 0) { + return new int[]{i, space}; + } + } + return new int[]{-1, 0}; + } + + private static int amountStackCanAddFrom(ItemStack existingStack, ItemStack fromStack, Inventory inventory) { + if (fromStack.isEmpty()) { + return 0; + } + else if (existingStack.isEmpty()) { + return Math.min(Math.min(existingStack.getMaxCount(), + inventory.getMaxCountPerStack()), + fromStack.getCount()); + } + else if (InventoryMethods.areItemsEqual(existingStack, fromStack) && + existingStack.isStackable() && + existingStack.getCount() < existingStack.getMaxCount() && + existingStack.getCount() < inventory.getMaxCountPerStack()) { + int stackSpace = existingStack.getMaxCount() - existingStack.getCount(); + int invSpace = inventory.getMaxCountPerStack() - existingStack.getCount(); + return Math.min(Math.min(stackSpace, invSpace), fromStack.getCount()); + } + return 0; + } + + private static boolean areItemsEqual(ItemStack stack1, ItemStack stack2) { + return stack1.getItem() == stack2.getItem() && ItemStack.areTagsEqual(stack1, stack2); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java index f94b8023a..5f020ee57 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java @@ -181,17 +181,19 @@ public class BlockCable extends BlockGeneric implements Waterloggable { // : new ItemStack( ComputerCraftRegistry.ModItems.CABLE.get() ); // } - @Override - @Deprecated - public boolean canPlaceAt(BlockState state, @Nonnull WorldView world, @Nonnull BlockPos pos) { - Direction facing = state.get(MODEM) - .getFacing(); - if (facing == null) { - return true; - } - - return sideCoversSmallSquare(world, pos.offset(facing), facing.getOpposite()); - } + // Commenting override to allow cable modems to be placed on chests, so that chests can be generic inventory peripherals. + // TODO Perhaps there is a more selective way to achieve this? + // @Override + // @Deprecated + // public boolean canPlaceAt(BlockState state, @Nonnull WorldView world, @Nonnull BlockPos pos) { + // Direction facing = state.get(MODEM) + // .getFacing(); + // if (facing == null) { + // return true; + // } + // + // return sideCoversSmallSquare(world, pos.offset(facing), facing.getOpposite()); + // } @Nonnull @Override diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java index a961427a4..90a71d6b2 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java @@ -18,6 +18,7 @@ import dan200.computercraft.api.turtle.TurtleCommandResult; import dan200.computercraft.api.turtle.event.TurtleBlockEvent; import dan200.computercraft.api.turtle.event.TurtleEvent; +import dan200.computercraft.shared.peripheral.generic.data.BlockData; import net.minecraft.block.Block; import net.minecraft.block.BlockState; import net.minecraft.state.property.Property; @@ -49,20 +50,7 @@ public class TurtleInspectCommand implements ITurtleCommand { return TurtleCommandResult.failure("No block to inspect"); } - Block block = state.getBlock(); - String name = Registry.BLOCK.getId(block) - .toString(); - - Map table = new HashMap<>(); - table.put("name", name); - - Map stateTable = new HashMap<>(); - for (ImmutableMap.Entry, ? extends Comparable> entry : state.getEntries() - .entrySet()) { - Property property = entry.getKey(); - stateTable.put(property.getName(), getPropertyValue(property, entry.getValue())); - } - table.put("state", stateTable); + Map table = BlockData.fill( new HashMap<>(), state ); // Fire the event, exiting if it is cancelled TurtlePlayer turtlePlayer = TurtlePlaceCommand.createPlayer(turtle, oldPosition, direction); @@ -72,7 +60,6 @@ public class TurtleInspectCommand implements ITurtleCommand { } return TurtleCommandResult.success(new Object[] {table}); - } @SuppressWarnings ({ diff --git a/src/main/java/dan200/computercraft/shared/util/ServiceUtil.java b/src/main/java/dan200/computercraft/shared/util/ServiceUtil.java new file mode 100644 index 000000000..9e4772450 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/ServiceUtil.java @@ -0,0 +1,29 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.util; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +import org.objectweb.asm.Type; + +import java.util.List; +import java.util.ServiceLoader; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public final class ServiceUtil +{ + private static final Type AUTO_SERVICE = Type.getType( "Lcom/google/auto/service/AutoService;" ); + + private ServiceUtil() + { + } + + public static Stream loadServices( Class target ) + { + return StreamSupport.stream( ServiceLoader.load( target, ServiceUtil.class.getClassLoader() ).spliterator(), false ); + } +}