From 08181f72d4a770deffed58d501a7877ab065091e Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 27 Jun 2020 10:47:31 +0100 Subject: [PATCH] Generic peripherals for any tile entities (#478) This exposes a basic peripheral for any tile entity which does not have methods already registered. We currently provide the following methods: - Inventories: size, list, getItemMeta, pushItems, pullItems. - Energy storage: getEnergy, getEnergyCapacity - Fluid tanks: tanks(), pushFluid, pullFluid. These methods are currently experimental - it must be enabled through `experimental.generic_peripherals`. While this is an initial step towards implementing #452, but is by no means complete. --- build.gradle | 3 + .../dan200/computercraft/ComputerCraft.java | 4 +- .../computercraft/core/asm/Generator.java | 94 +++++++--- .../computercraft/core/asm/GenericSource.java | 100 ++++++++++ .../computercraft/core/asm/LuaMethod.java | 6 +- .../computercraft/core/asm/NamedMethod.java | 2 +- .../core/asm/PeripheralMethod.java | 6 +- .../computercraft/core/asm/TaskCallback.java | 16 +- .../dan200/computercraft/shared/Config.java | 15 ++ .../computercraft/shared/Peripherals.java | 3 +- .../peripheral/generic/GenericPeripheral.java | 76 ++++++++ .../generic/GenericPeripheralProvider.java | 72 ++++++++ .../peripheral/generic/SaturatedMethod.java | 59 ++++++ .../peripheral/generic/meta/FluidMeta.java | 24 +++ .../peripheral/generic/meta/ItemMeta.java | 85 +++++++++ .../generic/methods/ArgumentHelpers.java | 66 +++++++ .../generic/methods/EnergyMethods.java | 39 ++++ .../generic/methods/FluidMethods.java | 173 ++++++++++++++++++ .../generic/methods/InventoryMethods.java | 161 ++++++++++++++++ 19 files changed, 960 insertions(+), 44 deletions(-) create mode 100644 src/main/java/dan200/computercraft/core/asm/GenericSource.java create mode 100644 src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java create mode 100644 src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java create mode 100644 src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java create mode 100644 src/main/java/dan200/computercraft/shared/peripheral/generic/meta/FluidMeta.java create mode 100644 src/main/java/dan200/computercraft/shared/peripheral/generic/meta/ItemMeta.java create mode 100644 src/main/java/dan200/computercraft/shared/peripheral/generic/methods/ArgumentHelpers.java create mode 100644 src/main/java/dan200/computercraft/shared/peripheral/generic/methods/EnergyMethods.java create mode 100644 src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java create mode 100644 src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java diff --git a/build.gradle b/build.gradle index debd2965b..69b2cb01f 100644 --- a/build.gradle +++ b/build.gradle @@ -105,6 +105,9 @@ accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg') runtimeOnly fg.deobf("mezz.jei:jei-1.15.2:6.0.0.3") + compileOnly 'com.google.auto.service:auto-service:1.0-rc7' + annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7' + shade 'org.squiddev:Cobalt:0.5.1-SNAPSHOT' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2' diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index f5e540126..bcac1d468 100644 --- a/src/main/java/dan200/computercraft/ComputerCraft.java +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -38,8 +38,6 @@ public final class ComputerCraft { public static final String MOD_ID = "computercraft"; - public static final int DATAFIXER_VERSION = 0; - // Configuration options public static final String[] DEFAULT_HTTP_ALLOW = new String[] { "*" }; public static final String[] DEFAULT_HTTP_DENY = new String[] { @@ -93,6 +91,8 @@ public final class ComputerCraft public static boolean turtlesCanPush = true; public static EnumSet turtleDisabledActions = EnumSet.noneOf( TurtleAction.class ); + public static boolean genericPeripheral = false; + public static final int terminalWidth_computer = 51; public static final int terminalHeight_computer = 19; diff --git a/src/main/java/dan200/computercraft/core/asm/Generator.java b/src/main/java/dan200/computercraft/core/asm/Generator.java index 801969d0c..0456cc51a 100644 --- a/src/main/java/dan200/computercraft/core/asm/Generator.java +++ b/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -34,7 +34,7 @@ import static org.objectweb.asm.Opcodes.*; -public class Generator +public final class Generator { private static final AtomicInteger METHOD_ID = new AtomicInteger(); @@ -99,26 +99,28 @@ private List> build( Class klass ) LuaFunction annotation = method.getAnnotation( LuaFunction.class ); if( annotation == null ) continue; + if( Modifier.isStatic( method.getModifiers() ) ) + { + ComputerCraft.log.warn( "LuaFunction method {}.{} should be an instance method.", method.getDeclaringClass(), method.getName() ); + continue; + } + T instance = methodCache.getUnchecked( method ).orElse( null ); if( instance == null ) continue; if( methods == null ) methods = new ArrayList<>(); + addMethod( methods, method, annotation, instance ); + } - if( annotation.mainThread() ) instance = wrap.apply( instance ); + for( GenericSource.GenericMethod method : GenericSource.GenericMethod.all() ) + { + if( !method.target.isAssignableFrom( klass ) ) continue; - String[] names = annotation.value(); - boolean isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread(); - if( names.length == 0 ) - { - methods.add( new NamedMethod<>( method.getName(), instance, isSimple ) ); - } - else - { - for( String name : names ) - { - methods.add( new NamedMethod<>( name, instance, isSimple ) ); - } - } + T instance = methodCache.getUnchecked( method.method ).orElse( null ); + if( instance == null ) continue; + + if( methods == null ) methods = new ArrayList<>(); + addMethod( methods, method.method, method.annotation, instance ); } if( methods == null ) return Collections.emptyList(); @@ -126,19 +128,40 @@ private List> build( Class klass ) return Collections.unmodifiableList( methods ); } + private void addMethod( List> methods, Method method, LuaFunction annotation, T instance ) + { + if( annotation.mainThread() ) instance = wrap.apply( instance ); + + String[] names = annotation.value(); + boolean isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread(); + if( names.length == 0 ) + { + methods.add( new NamedMethod<>( method.getName(), instance, isSimple ) ); + } + else + { + for( String name : names ) + { + methods.add( new NamedMethod<>( name, instance, isSimple ) ); + } + } + } + @Nonnull private Optional build( Method method ) { String name = method.getDeclaringClass().getName() + "." + method.getName(); int modifiers = method.getModifiers(); - if( !Modifier.isFinal( modifiers ) ) + + // Instance methods must be final - this prevents them being overridden and potentially exposed twice. + if( !Modifier.isStatic( modifiers ) && !Modifier.isFinal( modifiers ) ) { ComputerCraft.log.warn( "Lua Method {} should be final.", name ); } - if( Modifier.isStatic( modifiers ) || !Modifier.isPublic( modifiers ) ) + if( !Modifier.isPublic( modifiers ) ) { - ComputerCraft.log.error( "Lua Method {} should be a public instance method.", name ); + ComputerCraft.log.error( "Lua Method {} should be a public method.", name ); return Optional.empty(); } @@ -160,10 +183,14 @@ private Optional build( Method method ) } } + // We have some rather ugly handling of static methods in both here and the main generate function. Static methods + // only come from generic sources, so this should be safe. + Class target = Modifier.isStatic( modifiers ) ? method.getParameterTypes()[0] : method.getDeclaringClass(); + try { String className = method.getDeclaringClass().getName() + "$cc$" + method.getName() + METHOD_ID.getAndIncrement(); - byte[] bytes = generate( className, method ); + byte[] bytes = generate( className, target, method ); if( bytes == null ) return Optional.empty(); Class klass = DeclaringClassLoader.INSTANCE.define( className, bytes, method.getDeclaringClass().getProtectionDomain() ); @@ -178,7 +205,7 @@ private Optional build( Method method ) } @Nullable - private byte[] generate( String className, Method method ) + private byte[] generate( String className, Class target, Method method ) { String internalName = className.replace( ".", "/" ); @@ -200,19 +227,27 @@ private byte[] generate( String className, Method method ) { MethodVisitor mw = cw.visitMethod( ACC_PUBLIC, METHOD_NAME, methodDesc, null, EXCEPTIONS ); mw.visitCode(); - mw.visitVarInsn( ALOAD, 1 ); - mw.visitTypeInsn( CHECKCAST, Type.getInternalName( method.getDeclaringClass() ) ); + + // If we're an instance method, load the this parameter. + if( !Modifier.isStatic( method.getModifiers() ) ) + { + mw.visitVarInsn( ALOAD, 1 ); + mw.visitTypeInsn( CHECKCAST, Type.getInternalName( target ) ); + } int argIndex = 0; for( java.lang.reflect.Type genericArg : method.getGenericParameterTypes() ) { - Boolean loadedArg = loadArg( mw, method, genericArg, argIndex ); + Boolean loadedArg = loadArg( mw, target, method, genericArg, argIndex ); if( loadedArg == null ) return null; if( loadedArg ) argIndex++; } - mw.visitMethodInsn( INVOKEVIRTUAL, Type.getInternalName( method.getDeclaringClass() ), method.getName(), - Type.getMethodDescriptor( method ), false ); + mw.visitMethodInsn( + Modifier.isStatic( method.getModifiers() ) ? INVOKESTATIC : INVOKEVIRTUAL, + Type.getInternalName( method.getDeclaringClass() ), method.getName(), + Type.getMethodDescriptor( method ), false + ); // We allow a reasonable amount of flexibility on the return value's type. Alongside the obvious MethodResult, // we convert basic types into an immediate result. @@ -250,8 +285,15 @@ else if( ret == Object[].class ) return cw.toByteArray(); } - private Boolean loadArg( MethodVisitor mw, Method method, java.lang.reflect.Type genericArg, int argIndex ) + private Boolean loadArg( MethodVisitor mw, Class target, Method method, java.lang.reflect.Type genericArg, int argIndex ) { + if( genericArg == target ) + { + mw.visitVarInsn( ALOAD, 1 ); + mw.visitTypeInsn( CHECKCAST, Type.getInternalName( target ) ); + return false; + } + Class arg = Reflect.getRawType( method, genericArg, true ); if( arg == null ) return null; diff --git a/src/main/java/dan200/computercraft/core/asm/GenericSource.java b/src/main/java/dan200/computercraft/core/asm/GenericSource.java new file mode 100644 index 000000000..77c4a0ca3 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/GenericSource.java @@ -0,0 +1,100 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.shared.peripheral.generic.GenericPeripheralProvider; +import net.minecraft.util.ResourceLocation; + +import javax.annotation.Nonnull; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * A generic source of {@link LuaMethod} functions. This allows for injecting methods onto objects you do not own. + * + * Unlike conventional Lua objects, the annotated methods should be {@code static}, with their target as the first + * parameter. + * + * This is used by the generic peripheral system ({@link GenericPeripheralProvider}) to provide methods for arbitrary + * tile entities. Eventually this'll be be exposed in the public API. Until it is stabilised, it will remain in this + * package - do not use it in external mods! + */ +public interface GenericSource +{ + /** + * A unique identifier for this generic source. This may be used in the future to allow disabling specific sources. + * + * @return This source's identifier. + */ + @Nonnull + ResourceLocation id(); + + /** + * A generic method is a method belonging to a {@link GenericSource} with a known target. + */ + class GenericMethod + { + final Method method; + final LuaFunction annotation; + final Class target; + + private static List cache; + + GenericMethod( Method method, LuaFunction annotation, Class target ) + { + this.method = method; + this.annotation = annotation; + this.target = target; + } + + /** + * Find all public static methods annotated with {@link LuaFunction} which belong to a {@link GenericSource}. + * + * @return All available generic methods. + */ + static List all() + { + if( cache != null ) return cache; + return cache = StreamSupport + .stream( ServiceLoader.load( GenericSource.class, GenericSource.class.getClassLoader() ).spliterator(), false ) + .flatMap( x -> Arrays.stream( x.getClass().getDeclaredMethods() ) ) + .map( method -> { + LuaFunction annotation = method.getAnnotation( LuaFunction.class ); + if( annotation == null ) return null; + + if( !Modifier.isStatic( method.getModifiers() ) ) + { + ComputerCraft.log.error( "GenericSource method {}.{} should be static.", method.getDeclaringClass(), method.getName() ); + return null; + } + + Type[] types = method.getGenericParameterTypes(); + if( types.length == 0 ) + { + ComputerCraft.log.error( "GenericSource method {}.{} has no parameters.", method.getDeclaringClass(), method.getName() ); + return null; + } + + Class target = Reflect.getRawType( method, types[0], false ); + if( target == null ) return null; + + return new GenericMethod( method, annotation, target ); + } ) + .filter( Objects::nonNull ) + .collect( Collectors.toList() ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/LuaMethod.java b/src/main/java/dan200/computercraft/core/asm/LuaMethod.java index 35ad33b76..4bf142903 100644 --- a/src/main/java/dan200/computercraft/core/asm/LuaMethod.java +++ b/src/main/java/dan200/computercraft/core/asm/LuaMethod.java @@ -14,10 +14,8 @@ public interface LuaMethod { Generator GENERATOR = new Generator<>( LuaMethod.class, Collections.singletonList( ILuaContext.class ), - m -> ( target, context, args ) -> { - long id = context.issueMainThreadTask( () -> TaskCallback.checkUnwrap( m.apply( target, context, args ) ) ); - return new TaskCallback( id ).pull; - } ); + m -> ( target, context, args ) -> TaskCallback.make( context, () -> TaskCallback.checkUnwrap( m.apply( target, context, args ) ) ) + ); IntCache DYNAMIC = new IntCache<>( method -> ( instance, context, args ) -> ((IDynamicLuaObject) instance).callMethod( context, method, args ) diff --git a/src/main/java/dan200/computercraft/core/asm/NamedMethod.java b/src/main/java/dan200/computercraft/core/asm/NamedMethod.java index d7525fc7f..f7d0ffef4 100644 --- a/src/main/java/dan200/computercraft/core/asm/NamedMethod.java +++ b/src/main/java/dan200/computercraft/core/asm/NamedMethod.java @@ -8,7 +8,7 @@ import javax.annotation.Nonnull; -public class NamedMethod +public final class NamedMethod { private final String name; private final T method; diff --git a/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java b/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java index 637cfb806..f92a988f0 100644 --- a/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java +++ b/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java @@ -19,10 +19,8 @@ public interface PeripheralMethod { Generator GENERATOR = new Generator<>( PeripheralMethod.class, Arrays.asList( ILuaContext.class, IComputerAccess.class ), - m -> ( target, context, computer, args ) -> { - long id = context.issueMainThreadTask( () -> TaskCallback.checkUnwrap( m.apply( target, context, computer, args ) ) ); - return new TaskCallback( id ).pull; - } ); + m -> ( target, context, computer, args ) -> TaskCallback.make( context, () -> TaskCallback.checkUnwrap( m.apply( target, context, computer, args ) ) ) + ); IntCache DYNAMIC = new IntCache<>( method -> ( instance, context, computer, args ) -> ((IDynamicPeripheral) instance).callMethod( computer, context, method, args ) diff --git a/src/main/java/dan200/computercraft/core/asm/TaskCallback.java b/src/main/java/dan200/computercraft/core/asm/TaskCallback.java index 9ea4e67ad..98ca25a88 100644 --- a/src/main/java/dan200/computercraft/core/asm/TaskCallback.java +++ b/src/main/java/dan200/computercraft/core/asm/TaskCallback.java @@ -6,19 +6,17 @@ package dan200.computercraft.core.asm; -import dan200.computercraft.api.lua.ILuaCallback; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.lua.*; import javax.annotation.Nonnull; import java.util.Arrays; -class TaskCallback implements ILuaCallback +public final class TaskCallback implements ILuaCallback { - final MethodResult pull = MethodResult.pullEvent( "task_complete", this ); + private final MethodResult pull = MethodResult.pullEvent( "task_complete", this ); private final long task; - TaskCallback( long task ) + private TaskCallback( long task ) { this.task = task; } @@ -55,4 +53,10 @@ public static Object[] checkUnwrap( MethodResult result ) if( result.getCallback() != null ) throw new IllegalStateException( "Cannot return MethodResult currently" ); return result.getResult(); } + + public static MethodResult make( ILuaContext context, ILuaTask func ) throws LuaException + { + long task = context.issueMainThreadTask( func ); + return new TaskCallback( task ).pull; + } } diff --git a/src/main/java/dan200/computercraft/shared/Config.java b/src/main/java/dan200/computercraft/shared/Config.java index 7ccb2b99d..bfa177156 100644 --- a/src/main/java/dan200/computercraft/shared/Config.java +++ b/src/main/java/dan200/computercraft/shared/Config.java @@ -73,6 +73,8 @@ public final class Config private static final ConfigValue turtlesCanPush; private static final ConfigValue> turtleDisabledActions; + private static final ConfigValue genericPeripheral; + private static final ConfigValue monitorRenderer; private static final ForgeConfigSpec serverSpec; @@ -260,6 +262,17 @@ private Config() {} builder.pop(); } + { + builder.comment( "Options for various experimental features. These are not guaranteed to be stable, and may change or be removed across versions." ); + builder.push( "experimental" ); + + genericPeripheral = builder + .comment( "Attempt to make any existing block (or tile entity) a peripheral.\n" + + "This provides peripheral methods for any inventory, fluid tank or energy storage block. It will" + + "_not_ provide methods which have an existing peripheral provider." ) + .define( "generic_peripherals", false ); + } + serverSpec = builder.build(); Builder clientBuilder = new Builder(); @@ -322,6 +335,8 @@ public static void sync() ComputerCraft.turtleDisabledActions.clear(); for( String value : turtleDisabledActions.get() ) ComputerCraft.turtleDisabledActions.add( getAction( value ) ); + ComputerCraft.genericPeripheral = genericPeripheral.get(); + // Client ComputerCraft.monitorRenderer = monitorRenderer.get(); } diff --git a/src/main/java/dan200/computercraft/shared/Peripherals.java b/src/main/java/dan200/computercraft/shared/Peripherals.java index aae2b9d6d..cc61e4451 100644 --- a/src/main/java/dan200/computercraft/shared/Peripherals.java +++ b/src/main/java/dan200/computercraft/shared/Peripherals.java @@ -8,6 +8,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 dan200.computercraft.shared.util.CapabilityUtil; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.Direction; @@ -66,7 +67,7 @@ private static IPeripheral getPeripheralAt( World world, BlockPos pos, Direction } } - return null; + return CapabilityUtil.unwrap( GenericPeripheralProvider.getPeripheral( world, pos, side ), invalidate ); } } 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..9970d66cb --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java @@ -0,0 +1,76 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.api.peripheral.IDynamicPeripheral; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.ResourceLocation; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; + +class GenericPeripheral implements IDynamicPeripheral +{ + private final String type; + private final TileEntity tile; + private final List methods; + + GenericPeripheral( TileEntity tile, List methods ) + { + ResourceLocation type = tile.getType().getRegistryName(); + 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..09041c4f8 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java @@ -0,0 +1,72 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.generic; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.asm.NamedMethod; +import dan200.computercraft.core.asm.PeripheralMethod; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.energy.CapabilityEnergy; +import net.minecraftforge.fluids.capability.CapabilityFluidHandler; +import net.minecraftforge.items.CapabilityItemHandler; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +public class GenericPeripheralProvider +{ + private static final Capability[] CAPABILITIES = new Capability[] { + CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, + CapabilityEnergy.ENERGY, + CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, + }; + + @Nonnull + public static LazyOptional getPeripheral( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + if( !ComputerCraft.genericPeripheral ) return LazyOptional.empty(); + + TileEntity tile = world.getTileEntity( pos ); + if( tile == null ) return LazyOptional.empty(); + + ArrayList saturated = new ArrayList<>( 0 ); + LazyOptional peripheral = LazyOptional.of( () -> new GenericPeripheral( tile, saturated ) ); + + List> tileMethods = PeripheralMethod.GENERATOR.getMethods( tile.getClass() ); + if( !tileMethods.isEmpty() ) addSaturated( saturated, tile, tileMethods ); + + for( Capability capability : CAPABILITIES ) + { + LazyOptional wrapper = tile.getCapability( capability ); + wrapper.ifPresent( contents -> { + List> capabilityMethods = PeripheralMethod.GENERATOR.getMethods( contents.getClass() ); + if( capabilityMethods.isEmpty() ) return; + + addSaturated( saturated, contents, capabilityMethods ); + wrapper.addListener( x -> peripheral.invalidate() ); + } ); + } + + return saturated.isEmpty() ? LazyOptional.empty() : peripheral; + } + + 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..6db3f6aa4 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java @@ -0,0 +1,59 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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/meta/FluidMeta.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/meta/FluidMeta.java new file mode 100644 index 000000000..88c1b50ab --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/meta/FluidMeta.java @@ -0,0 +1,24 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.generic.meta; + +import net.minecraftforge.fluids.FluidStack; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.Objects; + +public class FluidMeta +{ + @Nonnull + public static > T fillBasicMeta( @Nonnull T data, @Nonnull FluidStack stack ) + { + data.put( "name", Objects.toString( stack.getFluid().getRegistryName() ) ); + data.put( "amount", stack.getAmount() ); + return data; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/meta/ItemMeta.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/meta/ItemMeta.java new file mode 100644 index 000000000..d61a5b21f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/meta/ItemMeta.java @@ -0,0 +1,85 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.generic.meta; + +import com.google.gson.JsonParseException; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.INBT; +import net.minecraft.nbt.ListNBT; +import net.minecraft.util.text.ITextComponent; +import net.minecraftforge.common.util.Constants; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class ItemMeta +{ + @Nonnull + public static > T fillBasicMeta( @Nonnull T data, @Nonnull ItemStack stack ) + { + data.put( "name", Objects.toString( stack.getItem().getRegistryName() ) ); + data.put( "count", stack.getCount() ); + return data; + } + + @Nonnull + public static > T fillMeta( @Nonnull T data, @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return data; + + fillBasicMeta( data, stack ); + + data.put( "displayName", stack.getDisplayName().getString() ); + data.put( "rawName", stack.getTranslationKey() ); + data.put( "maxCount", stack.getMaxStackSize() ); + + if( stack.isDamageable() ) + { + data.put( "damage", stack.getDamage() ); + data.put( "maxDamage", stack.getMaxDamage() ); + } + + if( stack.getItem().showDurabilityBar( stack ) ) + { + data.put( "durability", stack.getItem().getDurabilityForDisplay( stack ) ); + } + + CompoundNBT tag = stack.getTag(); + if( tag != null && tag.contains( "display", Constants.NBT.TAG_COMPOUND ) ) + { + CompoundNBT displayTag = tag.getCompound( "display" ); + if( displayTag.contains( "Lore", Constants.NBT.TAG_LIST ) ) + { + ListNBT loreTag = displayTag.getList( "Lore", Constants.NBT.TAG_STRING ); + data.put( "lore", loreTag.stream() + .map( ItemMeta::parseTextComponent ) + .filter( Objects::nonNull ) + .map( ITextComponent::getString ) + .collect( Collectors.toList() ) ); + } + } + + return data; + } + + @Nullable + private static ITextComponent parseTextComponent( @Nonnull INBT x ) + { + try + { + return ITextComponent.Serializer.fromJson( x.getString() ); + } + catch( JsonParseException e ) + { + return null; + } + } +} 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..d27865ee5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/ArgumentHelpers.java @@ -0,0 +1,66 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.generic.methods; + +import dan200.computercraft.api.lua.LuaException; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.ResourceLocationException; +import net.minecraftforge.registries.IForgeRegistry; +import net.minecraftforge.registries.IForgeRegistryEntry; + +import javax.annotation.Nonnull; + +/** + * 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 ) ); + } + } + + @Nonnull + public static > T getRegistryEntry( String name, String typeName, IForgeRegistry registry ) throws LuaException + { + ResourceLocation id; + try + { + id = new ResourceLocation( name ); + } + catch( ResourceLocationException e ) + { + id = null; + } + + T value; + if( id == null || !registry.containsKey( id ) || (value = registry.getValue( id )) == null ) + { + throw new LuaException( String.format( "Unknown %s '%s'", typeName, name ) ); + } + + return value; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/EnergyMethods.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/EnergyMethods.java new file mode 100644 index 000000000..1123318d3 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/EnergyMethods.java @@ -0,0 +1,39 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.api.lua.LuaFunction; +import dan200.computercraft.core.asm.GenericSource; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.energy.IEnergyStorage; +import net.minecraftforge.versions.forge.ForgeVersion; + +import javax.annotation.Nonnull; + +@AutoService( GenericSource.class ) +public class EnergyMethods implements GenericSource +{ + @Nonnull + @Override + public ResourceLocation id() + { + return new ResourceLocation( ForgeVersion.MOD_ID, "energy" ); + } + + @LuaFunction( mainThread = true ) + public static int getEnergy( IEnergyStorage energy ) + { + return energy.getEnergyStored(); + } + + @LuaFunction( mainThread = true ) + public static int getEnergyCapacity( IEnergyStorage energy ) + { + return energy.getMaxEnergyStored(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java new file mode 100644 index 000000000..a6918dbc6 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java @@ -0,0 +1,173 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.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.meta.FluidMeta; +import net.minecraft.fluid.Fluid; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.CapabilityFluidHandler; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.versions.forge.ForgeVersion; + +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.getRegistryEntry; + +@AutoService( GenericSource.class ) +public class FluidMethods implements GenericSource +{ + @Nonnull + @Override + public ResourceLocation id() + { + return new ResourceLocation( ForgeVersion.MOD_ID, "fluid" ); + } + + @LuaFunction( mainThread = true ) + public static Map> tanks( IFluidHandler fluids ) + { + Map> result = new HashMap<>(); + int size = fluids.getTanks(); + for( int i = 0; i < size; i++ ) + { + FluidStack stack = fluids.getFluidInTank( i ); + if( !stack.isEmpty() ) result.put( i + 1, FluidMeta.fillBasicMeta( new HashMap<>( 4 ), stack ) ); + } + + return result; + } + + @LuaFunction( mainThread = true ) + public static int pushFluid( + IFluidHandler from, IComputerAccess computer, + String toName, Optional limit, Optional fluidName + ) throws LuaException + { + Fluid fluid = fluidName.isPresent() + ? getRegistryEntry( fluidName.get(), "fluid", ForgeRegistries.FLUIDS ) + : null; + + // Find location to transfer to + IPeripheral location = computer.getAvailablePeripheral( toName ); + if( location == null ) throw new LuaException( "Target '" + toName + "' does not exist" ); + + IFluidHandler to = extractHandler( location.getTarget() ); + if( to == null ) throw new LuaException( "Target '" + toName + "' is not an tank" ); + + int actualLimit = limit.orElse( Integer.MAX_VALUE ); + if( actualLimit <= 0 ) throw new LuaException( "Limit must be > 0" ); + + return fluid == null + ? moveFluid( from, actualLimit, to ) + : moveFluid( from, new FluidStack( fluid, actualLimit ), to ); + } + + @LuaFunction( mainThread = true ) + public static int pullFluid( + IFluidHandler to, IComputerAccess computer, + String fromName, Optional limit, Optional fluidName + ) throws LuaException + { + Fluid fluid = fluidName.isPresent() + ? getRegistryEntry( fluidName.get(), "fluid", ForgeRegistries.FLUIDS ) + : null; + + // Find location to transfer to + IPeripheral location = computer.getAvailablePeripheral( fromName ); + if( location == null ) throw new LuaException( "Target '" + fromName + "' does not exist" ); + + IFluidHandler from = extractHandler( location.getTarget() ); + if( from == null ) throw new LuaException( "Target '" + fromName + "' is not an tank" ); + + int actualLimit = limit.orElse( Integer.MAX_VALUE ); + if( actualLimit <= 0 ) throw new LuaException( "Limit must be > 0" ); + + return fluid == null + ? moveFluid( from, actualLimit, to ) + : moveFluid( from, new FluidStack( fluid, actualLimit ), to ); + } + + @Nullable + private static IFluidHandler extractHandler( @Nullable Object object ) + { + if( object instanceof ICapabilityProvider ) + { + LazyOptional cap = ((ICapabilityProvider) object).getCapability( CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY ); + if( cap.isPresent() ) return cap.orElseThrow( NullPointerException::new ); + } + + if( object instanceof IFluidHandler ) return (IFluidHandler) object; + return null; + } + + /** + * Move fluid from one handler to another. + * + * @param from The handler to move from. + * @param limit The maximum amount of fluid to move. + * @param to The handler to move to. + * @return The amount of fluid moved. + */ + private static int moveFluid( IFluidHandler from, int limit, IFluidHandler to ) + { + return moveFluid( from, from.drain( limit, IFluidHandler.FluidAction.SIMULATE ), limit, to ); + } + + /** + * Move fluid from one handler to another. + * + * @param from The handler to move from. + * @param fluid The fluid and limit to move. + * @param to The handler to move to. + * @return The amount of fluid moved. + */ + private static int moveFluid( IFluidHandler from, FluidStack fluid, IFluidHandler to ) + { + return moveFluid( from, from.drain( fluid, IFluidHandler.FluidAction.SIMULATE ), fluid.getAmount(), to ); + } + + /** + * Move fluid from one handler to another. + * + * @param from The handler to move from. + * @param extracted The fluid which is extracted from {@code from}. + * @param limit The maximum amount of fluid to move. + * @param to The handler to move to. + * @return The amount of fluid moved. + */ + private static int moveFluid( IFluidHandler from, FluidStack extracted, int limit, IFluidHandler to ) + { + if( extracted == null || extracted.getAmount() <= 0 ) return 0; + + // Limit the amount to extract. + extracted = extracted.copy(); + extracted.setAmount( Math.min( extracted.getAmount(), limit ) ); + + int inserted = to.fill( extracted.copy(), IFluidHandler.FluidAction.EXECUTE ); + 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. + extracted.setAmount( inserted ); + from.drain( extracted, IFluidHandler.FluidAction.EXECUTE ); + return inserted; + } +} 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..cc1c37d0b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java @@ -0,0 +1,161 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.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.meta.ItemMeta; +import net.minecraft.inventory.IInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.items.CapabilityItemHandler; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.ItemHandlerHelper; +import net.minecraftforge.items.wrapper.InvWrapper; +import net.minecraftforge.versions.forge.ForgeVersion; + +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; + +@AutoService( GenericSource.class ) +public class InventoryMethods implements GenericSource +{ + @Nonnull + @Override + public ResourceLocation id() + { + return new ResourceLocation( ForgeVersion.MOD_ID, "inventory" ); + } + + @LuaFunction( mainThread = true ) + public static int size( IItemHandler inventory ) + { + return inventory.getSlots(); + } + + @LuaFunction( mainThread = true ) + public static Map> list( IItemHandler inventory ) + { + Map> result = new HashMap<>(); + int size = inventory.getSlots(); + for( int i = 0; i < size; i++ ) + { + ItemStack stack = inventory.getStackInSlot( i ); + if( !stack.isEmpty() ) result.put( i + 1, ItemMeta.fillBasicMeta( new HashMap<>( 4 ), stack ) ); + } + + return result; + } + + @LuaFunction( mainThread = true ) + public static Map getItemMeta( IItemHandler inventory, int slot ) throws LuaException + { + assertBetween( slot, 1, inventory.getSlots(), "Slot out of range (%s)" ); + + ItemStack stack = inventory.getStackInSlot( slot - 1 ); + return stack.isEmpty() ? null : ItemMeta.fillMeta( new HashMap<>(), stack ); + } + + @LuaFunction( mainThread = true ) + public static int pushItems( + IItemHandler from, IComputerAccess computer, + String toName, int fromSlot, Optional limit, Optional toSlot + ) throws LuaException + { + // Find location to transfer to + IPeripheral location = computer.getAvailablePeripheral( toName ); + if( location == null ) throw new LuaException( "Target '" + toName + "' does not exist" ); + + IItemHandler 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 ); + if( actualLimit <= 0 ) throw new LuaException( "Limit must be > 0" ); + assertBetween( fromSlot, 1, from.getSlots(), "From slot out of range (%s)" ); + if( toSlot.isPresent() ) assertBetween( toSlot.get(), 1, to.getSlots(), "To slot out of range (%s)" ); + + return moveItem( from, fromSlot - 1, to, toSlot.orElse( 0 ) - 1, actualLimit ); + } + + @LuaFunction( mainThread = true ) + public static int pullItems( + IItemHandler to, IComputerAccess computer, + String fromName, int fromSlot, Optional limit, Optional toSlot + ) throws LuaException + { + // Find location to transfer to + IPeripheral location = computer.getAvailablePeripheral( fromName ); + if( location == null ) throw new LuaException( "Source '" + fromName + "' does not exist" ); + + IItemHandler 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 ); + if( actualLimit <= 0 ) throw new LuaException( "Limit must be > 0" ); + assertBetween( fromSlot, 1, from.getSlots(), "From slot out of range (%s)" ); + if( toSlot.isPresent() ) assertBetween( toSlot.get(), 1, to.getSlots(), "To slot out of range (%s)" ); + + return moveItem( from, fromSlot - 1, to, toSlot.orElse( 0 ) - 1, actualLimit ); + } + + @Nullable + private static IItemHandler extractHandler( @Nullable Object object ) + { + if( object instanceof ICapabilityProvider ) + { + LazyOptional cap = ((ICapabilityProvider) object).getCapability( CapabilityItemHandler.ITEM_HANDLER_CAPABILITY ); + if( cap.isPresent() ) return cap.orElseThrow( NullPointerException::new ); + } + + if( object instanceof IItemHandler ) return (IItemHandler) object; + if( object instanceof IInventory ) return new InvWrapper( (IInventory) object ); + return null; + } + + /** + * 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( IItemHandler from, int fromSlot, IItemHandler to, int toSlot, final int limit ) + { + // 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 ? ItemHandlerHelper.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 ); + return inserted; + } +}