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.
This commit is contained in:
Jonathan Coates 2020-06-27 10:47:31 +01:00 committed by GitHub
parent 613a28a5af
commit 08181f72d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 960 additions and 44 deletions

View File

@ -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'

View File

@ -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<TurtleAction> turtleDisabledActions = EnumSet.noneOf( TurtleAction.class );
public static boolean genericPeripheral = false;
public static final int terminalWidth_computer = 51;
public static final int terminalHeight_computer = 19;

View File

@ -34,7 +34,7 @@
import static org.objectweb.asm.Opcodes.*;
public class Generator<T>
public final class Generator<T>
{
private static final AtomicInteger METHOD_ID = new AtomicInteger();
@ -99,26 +99,28 @@ private List<NamedMethod<T>> 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<NamedMethod<T>> build( Class<?> klass )
return Collections.unmodifiableList( methods );
}
private void addMethod( List<NamedMethod<T>> 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<T> 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<T> 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<T> 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;

View File

@ -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<GenericMethod> 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<GenericMethod> 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() );
}
}
}

View File

@ -14,10 +14,8 @@
public interface LuaMethod
{
Generator<LuaMethod> 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<LuaMethod> DYNAMIC = new IntCache<>(
method -> ( instance, context, args ) -> ((IDynamicLuaObject) instance).callMethod( context, method, args )

View File

@ -8,7 +8,7 @@
import javax.annotation.Nonnull;
public class NamedMethod<T>
public final class NamedMethod<T>
{
private final String name;
private final T method;

View File

@ -19,10 +19,8 @@
public interface PeripheralMethod
{
Generator<PeripheralMethod> 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<PeripheralMethod> DYNAMIC = new IntCache<>(
method -> ( instance, context, computer, args ) -> ((IDynamicPeripheral) instance).callMethod( computer, context, method, args )

View File

@ -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;
}
}

View File

@ -73,6 +73,8 @@ public final class Config
private static final ConfigValue<Boolean> turtlesCanPush;
private static final ConfigValue<List<? extends String>> turtleDisabledActions;
private static final ConfigValue<Boolean> genericPeripheral;
private static final ConfigValue<MonitorRenderer> 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();
}

View File

@ -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 );
}
}

View File

@ -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<SaturatedMethod> methods;
GenericPeripheral( TileEntity tile, List<SaturatedMethod> 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 );
}
}

View File

@ -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<IPeripheral> 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<SaturatedMethod> saturated = new ArrayList<>( 0 );
LazyOptional<IPeripheral> peripheral = LazyOptional.of( () -> new GenericPeripheral( tile, saturated ) );
List<NamedMethod<PeripheralMethod>> 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<NamedMethod<PeripheralMethod>> 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<SaturatedMethod> saturated, Object target, List<NamedMethod<PeripheralMethod>> methods )
{
saturated.ensureCapacity( saturated.size() + methods.size() );
for( NamedMethod<PeripheralMethod> method : methods )
{
saturated.add( new SaturatedMethod( target, method ) );
}
}
}

View File

@ -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<PeripheralMethod> 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();
}
}

View File

@ -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 extends Map<? super String, Object>> T fillBasicMeta( @Nonnull T data, @Nonnull FluidStack stack )
{
data.put( "name", Objects.toString( stack.getFluid().getRegistryName() ) );
data.put( "amount", stack.getAmount() );
return data;
}
}

View File

@ -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 extends Map<? super String, Object>> 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 extends Map<? super String, Object>> 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;
}
}
}

View File

@ -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 extends IForgeRegistryEntry<T>> T getRegistryEntry( String name, String typeName, IForgeRegistry<T> 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;
}
}

View File

@ -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();
}
}

View File

@ -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<Integer, Map<String, ?>> tanks( IFluidHandler fluids )
{
Map<Integer, Map<String, ?>> 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<Integer> limit, Optional<String> 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<Integer> limit, Optional<String> 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<IFluidHandler> 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;
}
}

View File

@ -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<Integer, Map<String, ?>> list( IItemHandler inventory )
{
Map<Integer, Map<String, ?>> 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<String, ?> 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<Integer> limit, Optional<Integer> 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<Integer> limit, Optional<Integer> 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<IItemHandler> 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;
}
}