diff --git a/src/main/java/dan200/computercraft/api/peripheral/GenericPeripheral.java b/src/main/java/dan200/computercraft/api/peripheral/GenericPeripheral.java new file mode 100644 index 000000000..02e13fd7a --- /dev/null +++ b/src/main/java/dan200/computercraft/api/peripheral/GenericPeripheral.java @@ -0,0 +1,45 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.peripheral; + +import dan200.computercraft.api.lua.GenericSource; +import net.minecraft.tileentity.TileEntity; +import net.minecraftforge.items.IItemHandler; + +import javax.annotation.Nonnull; + +/** + * A {@link GenericSource} which provides methods for a peripheral. + * + * Unlike a {@link GenericSource}, all methods should target the same type, for instance a + * {@link TileEntity} subclass or a capability interface. This is not currently enforced. + */ +public interface GenericPeripheral extends GenericSource +{ + /** + * Get the type of the exposed peripheral. + * + * Unlike normal {@link IPeripheral}s, {@link GenericPeripheral} do not have to have a type. By default, the + * resulting peripheral uses the resource name of the wrapped {@link TileEntity} (for instance {@literal minecraft:chest}). + * + * However, in some cases it may be more appropriate to specify a more readable name. Overriding this method allows + * you to do so. + * + * When multiple {@link GenericPeripheral}s return a non-empty peripheral type for a single tile entity, the + * lexicographically smallest will be chosen. In order to avoid this conflict, this method should only be + * implemented when your peripheral targets a single tile entity AND it's likely that you're the + * only mod to do so. Similarly this should NOT be implemented when your methods target a + * capability or other interface (i.e. {@link IItemHandler}). + * + * @return The type of this peripheral or {@link PeripheralType#untyped()}. + * @see IPeripheral#getType() + */ + @Nonnull + default PeripheralType getType() + { + return PeripheralType.untyped(); + } +} diff --git a/src/main/java/dan200/computercraft/api/peripheral/PeripheralType.java b/src/main/java/dan200/computercraft/api/peripheral/PeripheralType.java new file mode 100644 index 000000000..2780534c2 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/peripheral/PeripheralType.java @@ -0,0 +1,62 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.peripheral; + +import com.google.common.base.Strings; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The type of a {@link GenericPeripheral}. + * + * When determining the final type of the resulting peripheral, the union of all types is taken, with the + * lexicographically smallest non-empty name being chosen. + */ +public final class PeripheralType +{ + private static final PeripheralType UNTYPED = new PeripheralType( null ); + + private final String type; + + public PeripheralType( String type ) + { + this.type = type; + } + + /** + * An empty peripheral type, used when a {@link GenericPeripheral} does not have an explicit type. + * + * @return The empty peripheral type. + */ + public static PeripheralType untyped() + { + return UNTYPED; + } + + /** + * Create a new non-empty peripheral type. + * + * @param type The name of the type. + * @return The constructed peripheral type. + */ + public static PeripheralType ofType( @Nonnull String type ) + { + if( Strings.isNullOrEmpty( type ) ) throw new IllegalArgumentException( "type cannot be null or empty" ); + return new PeripheralType( type ); + } + + /** + * Get the name of this peripheral type. This may be {@literal null}. + * + * @return The type of this peripheral. + */ + @Nullable + public String getPrimaryType() + { + return type; + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/Generator.java b/src/main/java/dan200/computercraft/core/asm/Generator.java index e68732f95..4cd2d66cf 100644 --- a/src/main/java/dan200/computercraft/core/asm/Generator.java +++ b/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -15,6 +15,7 @@ import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.PeripheralType; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; @@ -108,7 +109,7 @@ public final class Generator if( instance == null ) continue; if( methods == null ) methods = new ArrayList<>(); - addMethod( methods, method, annotation, instance ); + addMethod( methods, method, annotation, null, instance ); } for( GenericMethod method : GenericMethod.all() ) @@ -119,7 +120,7 @@ public final class Generator if( instance == null ) continue; if( methods == null ) methods = new ArrayList<>(); - addMethod( methods, method.method, method.annotation, instance ); + addMethod( methods, method.method, method.annotation, method.peripheralType, instance ); } if( methods == null ) return Collections.emptyList(); @@ -127,7 +128,7 @@ public final class Generator return Collections.unmodifiableList( methods ); } - private void addMethod( List> methods, Method method, LuaFunction annotation, T instance ) + private void addMethod( List> methods, Method method, LuaFunction annotation, PeripheralType genericType, T instance ) { if( annotation.mainThread() ) instance = wrap.apply( instance ); @@ -135,13 +136,13 @@ public final class Generator boolean isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread(); if( names.length == 0 ) { - methods.add( new NamedMethod<>( method.getName(), instance, isSimple ) ); + methods.add( new NamedMethod<>( method.getName(), instance, isSimple, genericType ) ); } else { for( String name : names ) { - methods.add( new NamedMethod<>( name, instance, isSimple ) ); + methods.add( new NamedMethod<>( name, instance, isSimple, genericType ) ); } } } diff --git a/src/main/java/dan200/computercraft/core/asm/GenericMethod.java b/src/main/java/dan200/computercraft/core/asm/GenericMethod.java index e0c916c2e..691d9b742 100644 --- a/src/main/java/dan200/computercraft/core/asm/GenericMethod.java +++ b/src/main/java/dan200/computercraft/core/asm/GenericMethod.java @@ -8,6 +8,8 @@ package dan200.computercraft.core.asm; import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.lua.GenericSource; import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.peripheral.GenericPeripheral; +import dan200.computercraft.api.peripheral.PeripheralType; import javax.annotation.Nonnull; import java.lang.reflect.Method; @@ -18,6 +20,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * A generic method is a method belonging to a {@link GenericSource} with a known target. @@ -27,15 +30,17 @@ public class GenericMethod final Method method; final LuaFunction annotation; final Class target; + final PeripheralType peripheralType; private static final List sources = new ArrayList<>(); private static List cache; - GenericMethod( Method method, LuaFunction annotation, Class target ) + GenericMethod( Method method, LuaFunction annotation, Class target, PeripheralType peripheralType ) { this.method = method; this.annotation = annotation; this.target = target; + this.peripheralType = peripheralType; } /** @@ -46,10 +51,28 @@ public class GenericMethod static List all() { if( cache != null ) return cache; - return cache = sources.stream() - .flatMap( x -> Arrays.stream( x.getClass().getDeclaredMethods() ) ) - .map( method -> - { + return cache = sources.stream().flatMap( GenericMethod::getMethods ).collect( Collectors.toList() ); + } + + public static synchronized void register( @Nonnull GenericSource source ) + { + Objects.requireNonNull( source, "Source cannot be null" ); + + if( cache != null ) + { + ComputerCraft.log.warn( "Registering a generic source {} after cache has been built. This source will be ignored.", cache ); + } + + sources.add( source ); + } + + private static Stream getMethods( GenericSource source ) + { + Class klass = source.getClass(); + PeripheralType type = source instanceof GenericPeripheral ? ((GenericPeripheral) source).getType() : null; + + return Arrays.stream( klass.getDeclaredMethods() ) + .map( method -> { LuaFunction annotation = method.getAnnotation( LuaFunction.class ); if( annotation == null ) return null; @@ -69,22 +92,8 @@ public class GenericMethod Class target = Reflect.getRawType( method, types[0], false ); if( target == null ) return null; - return new GenericMethod( method, annotation, target ); + return new GenericMethod( method, annotation, target, type ); } ) - .filter( Objects::nonNull ) - .collect( Collectors.toList() ); - } - - - public static synchronized void register( @Nonnull GenericSource source ) - { - Objects.requireNonNull( source, "Source cannot be null" ); - - if( cache != null ) - { - ComputerCraft.log.warn( "Registering a generic source {} after cache has been built. This source will be ignored.", cache ); - } - - sources.add( source ); + .filter( Objects::nonNull ); } } diff --git a/src/main/java/dan200/computercraft/core/asm/NamedMethod.java b/src/main/java/dan200/computercraft/core/asm/NamedMethod.java index ea72bb7a4..35d9c0a77 100644 --- a/src/main/java/dan200/computercraft/core/asm/NamedMethod.java +++ b/src/main/java/dan200/computercraft/core/asm/NamedMethod.java @@ -5,7 +5,10 @@ */ package dan200.computercraft.core.asm; +import dan200.computercraft.api.peripheral.PeripheralType; + import javax.annotation.Nonnull; +import javax.annotation.Nullable; public final class NamedMethod { @@ -13,11 +16,14 @@ public final class NamedMethod private final T method; private final boolean nonYielding; - NamedMethod( String name, T method, boolean nonYielding ) + private final PeripheralType genericType; + + NamedMethod( String name, T method, boolean nonYielding, PeripheralType genericType ) { this.name = name; this.method = method; this.nonYielding = nonYielding; + this.genericType = genericType; } @Nonnull @@ -36,4 +42,10 @@ public final class NamedMethod { return nonYielding; } + + @Nullable + public PeripheralType getGenericType() + { + return genericType; + } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java index ebfa94a6f..8703f687e 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java @@ -25,11 +25,11 @@ class GenericPeripheral implements IDynamicPeripheral private final TileEntity tile; private final List methods; - GenericPeripheral( TileEntity tile, List methods ) + GenericPeripheral( TileEntity tile, String name, List methods ) { ResourceLocation type = tile.getType().getRegistryName(); this.tile = tile; - this.type = type == null ? "unknown" : type.toString(); + this.type = name != null ? name : (type != null ? type.toString() : "unknown"); this.methods = 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 index 8eab46f11..d6918a1fd 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java @@ -6,6 +6,7 @@ package dan200.computercraft.shared.peripheral.generic; import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.PeripheralType; import dan200.computercraft.core.asm.NamedMethod; import dan200.computercraft.core.asm.PeripheralMethod; import net.minecraft.tileentity.TileEntity; @@ -38,10 +39,10 @@ public class GenericPeripheralProvider TileEntity tile = world.getBlockEntity( pos ); if( tile == null ) return null; - ArrayList saturated = new ArrayList<>( 0 ); + GenericPeripheralBuilder saturated = new GenericPeripheralBuilder(); List> tileMethods = PeripheralMethod.GENERATOR.getMethods( tile.getClass() ); - if( !tileMethods.isEmpty() ) addSaturated( saturated, tile, tileMethods ); + if( !tileMethods.isEmpty() ) saturated.addMethods( tile, tileMethods ); for( Capability capability : capabilities ) { @@ -50,20 +51,44 @@ public class GenericPeripheralProvider List> capabilityMethods = PeripheralMethod.GENERATOR.getMethods( contents.getClass() ); if( capabilityMethods.isEmpty() ) return; - addSaturated( saturated, contents, capabilityMethods ); + saturated.addMethods( contents, capabilityMethods ); wrapper.addListener( cast( invalidate ) ); } ); } - return saturated.isEmpty() ? null : new GenericPeripheral( tile, saturated ); + return saturated.toPeripheral( tile ); } - private static void addSaturated( ArrayList saturated, Object target, List> methods ) + private static class GenericPeripheralBuilder { - saturated.ensureCapacity( saturated.size() + methods.size() ); - for( NamedMethod method : methods ) + String name; + final ArrayList methods = new ArrayList<>( 0 ); + + IPeripheral toPeripheral( TileEntity tile ) { - saturated.add( new SaturatedMethod( target, method ) ); + if( methods.isEmpty() ) return null; + + methods.trimToSize(); + return new GenericPeripheral( tile, name, methods ); + } + + void addMethods( Object target, List> methods ) + { + ArrayList saturatedMethods = this.methods; + saturatedMethods.ensureCapacity( saturatedMethods.size() + methods.size() ); + for( NamedMethod method : methods ) + { + saturatedMethods.add( new SaturatedMethod( target, method ) ); + + // If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods + // don't change). + PeripheralType type = method.getGenericType(); + if( type != null && type.getPrimaryType() != null ) + { + String name = type.getPrimaryType(); + if( this.name == null || this.name.compareTo( name ) > 0 ) this.name = name; + } + } } }