diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java new file mode 100644 index 000000000..194c72571 --- /dev/null +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -0,0 +1,145 @@ +/* + * 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; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import dan200.computercraft.api.turtle.event.TurtleAction; +import dan200.computercraft.core.apis.http.options.Action; +import dan200.computercraft.core.apis.http.options.AddressRule; +import dan200.computercraft.core.asm.GenericSource; +import dan200.computercraft.shared.Config; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.computer.core.ClientComputerRegistry; +import dan200.computercraft.shared.computer.core.ServerComputerRegistry; +import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; +import dan200.computercraft.shared.pocket.peripherals.PocketModem; +import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker; +import dan200.computercraft.shared.turtle.upgrades.TurtleAxe; +import dan200.computercraft.shared.turtle.upgrades.TurtleCraftingTable; +import dan200.computercraft.shared.turtle.upgrades.TurtleHoe; +import dan200.computercraft.shared.turtle.upgrades.TurtleModem; +import dan200.computercraft.shared.turtle.upgrades.TurtleShovel; +import dan200.computercraft.shared.turtle.upgrades.TurtleSpeaker; +import dan200.computercraft.shared.turtle.upgrades.TurtleSword; +import dan200.computercraft.shared.turtle.upgrades.TurtleTool; +import dan200.computercraft.shared.util.ServiceUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import net.fabricmc.api.ModInitializer; + +public final class ComputerCraft implements ModInitializer { + public static final String MOD_ID = "computercraft"; + + // Configuration options + public static final String[] DEFAULT_HTTP_ALLOW = new String[] {"*"}; + public static final String[] DEFAULT_HTTP_DENY = new String[] { + "127.0.0.0/8", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "fd00::/8", + }; + + public static int computerSpaceLimit = 1000 * 1000; + public static int floppySpaceLimit = 125 * 1000; + public static int maximumFilesOpen = 128; + public static boolean disableLua51Features = false; + public static String defaultComputerSettings = ""; + public static boolean debugEnable = true; + public static boolean logComputerErrors = true; + public static boolean commandRequireCreative = true; + + public static int computerThreads = 1; + public static long maxMainGlobalTime = TimeUnit.MILLISECONDS.toNanos(10); + public static long maxMainComputerTime = TimeUnit.MILLISECONDS.toNanos(5); + + public static boolean httpEnabled = true; + public static boolean httpWebsocketEnabled = true; + public static List httpRules = Collections.unmodifiableList(Stream.concat(Stream.of(DEFAULT_HTTP_DENY) + .map(x -> AddressRule.parse(x, Action.DENY.toPartial())) + .filter(Objects::nonNull), + Stream.of(DEFAULT_HTTP_ALLOW) + .map(x -> AddressRule.parse(x, Action.ALLOW.toPartial())) + .filter(Objects::nonNull)) + .collect(Collectors.toList())); + + public static int httpMaxRequests = 16; + public static int httpMaxWebsockets = 4; + + public static boolean enableCommandBlock = false; + public static int modemRange = 64; + public static int modemHighAltitudeRange = 384; + public static int modemRangeDuringStorm = 64; + public static int modemHighAltitudeRangeDuringStorm = 384; + public static int maxNotesPerTick = 8; + public static MonitorRenderer monitorRenderer = MonitorRenderer.BEST; + public static double monitorDistanceSq = 4096; + public static long monitorBandwidth = 1_000_000; + + public static boolean turtlesNeedFuel = true; + public static int turtleFuelLimit = 20000; + public static int advancedTurtleFuelLimit = 100000; + public static boolean turtlesObeyBlockProtection = true; + public static boolean turtlesCanPush = true; + public static EnumSet turtleDisabledActions = EnumSet.noneOf(TurtleAction.class); + + public static boolean genericPeripheral = false; + + public static int computerTermWidth = 51; + public static int computerTermHeight = 19; + + public static final int turtleTermWidth = 39; + public static final int turtleTermHeight = 13; + + public static int pocketTermWidth = 26; + public static int pocketTermHeight = 20; + + public static int monitorWidth = 8; + public static int monitorHeight = 6; + + public static final class TurtleUpgrades { + public static TurtleModem wirelessModemNormal; + public static TurtleModem wirelessModemAdvanced; + public static TurtleSpeaker speaker; + + public static TurtleCraftingTable craftingTable; + public static TurtleSword diamondSword; + public static TurtleShovel diamondShovel; + public static TurtleTool diamondPickaxe; + public static TurtleAxe diamondAxe; + public static TurtleHoe diamondHoe; + } + + public static final class PocketUpgrades { + public static PocketModem wirelessModemNormal; + public static PocketModem wirelessModemAdvanced; + public static PocketSpeaker speaker; + } + + // Registries + public static final ClientComputerRegistry clientComputerRegistry = new ClientComputerRegistry(); + public static final ServerComputerRegistry serverComputerRegistry = new ServerComputerRegistry(); + + // Logging + public static final Logger log = LogManager.getLogger(MOD_ID); + + + @Override + public void onInitialize() { + Config.setup(); + Registry.setup(); + GenericSource.setup(() -> ServiceUtil.loadServicesForge(GenericSource.class)); + } +} diff --git a/src/main/java/dan200/computercraft/ComputerCraftAPIImpl.java b/src/main/java/dan200/computercraft/ComputerCraftAPIImpl.java new file mode 100644 index 000000000..a6f1d8db3 --- /dev/null +++ b/src/main/java/dan200/computercraft/ComputerCraftAPIImpl.java @@ -0,0 +1,168 @@ +/* + * 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; + +import dan200.computercraft.api.ComputerCraftAPI.IComputerCraftAPI; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.ILuaAPIFactory; +import dan200.computercraft.api.media.IMediaProvider; +import dan200.computercraft.api.network.IPacketNetwork; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.peripheral.IPeripheralProvider; +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.redstone.IBundledRedstoneProvider; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.core.apis.ApiFactories; +import dan200.computercraft.core.filesystem.FileMount; +import dan200.computercraft.core.filesystem.ResourceMount; +import dan200.computercraft.shared.*; +import dan200.computercraft.shared.peripheral.modem.wireless.WirelessNetwork; +import dan200.computercraft.shared.util.IDAssigner; +import dan200.computercraft.shared.wired.WiredNode; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.resource.ReloadableResourceManager; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.BlockView; +import net.minecraft.world.World; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.server.ServerLifecycleHooks; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_WIRED_ELEMENT; + +public final class ComputerCraftAPIImpl implements IComputerCraftAPI +{ + public static final ComputerCraftAPIImpl INSTANCE = new ComputerCraftAPIImpl(); + + private String version; + + private ComputerCraftAPIImpl() + { + } + + public static InputStream getResourceFile( String domain, String subPath ) + { + ReloadableResourceManager manager = (ReloadableResourceManager) ServerLifecycleHooks.getCurrentServer().getDataPackRegistries().getResourceManager(); + try + { + return manager.getResource( new Identifier( domain, subPath ) ).getInputStream(); + } + catch( IOException ignored ) + { + return null; + } + } + + @Nonnull + @Override + public String getInstalledVersion() + { + if( version != null ) return version; + return version = ModList.get().getModContainerById( ComputerCraft.MOD_ID ) + .map( x -> x.getModInfo().getVersion().toString() ) + .orElse( "unknown" ); + } + + @Override + public int createUniqueNumberedSaveDir( @Nonnull World world, @Nonnull String parentSubPath ) + { + return IDAssigner.getNextId( parentSubPath ); + } + + @Override + public IWritableMount createSaveDirMount( @Nonnull World world, @Nonnull String subPath, long capacity ) + { + try + { + return new FileMount( new File( IDAssigner.getDir(), subPath ), capacity ); + } + catch( Exception e ) + { + return null; + } + } + + @Override + public IMount createResourceMount( @Nonnull String domain, @Nonnull String subPath ) + { + ReloadableResourceManager manager = (ReloadableResourceManager) ServerLifecycleHooks.getCurrentServer().getDataPackRegistries().getResourceManager(); + ResourceMount mount = ResourceMount.get( domain, subPath, manager ); + return mount.exists( "" ) ? mount : null; + } + + @Override + public void registerPeripheralProvider( @Nonnull IPeripheralProvider provider ) + { + Peripherals.register( provider ); + } + + @Override + public void registerTurtleUpgrade( @Nonnull ITurtleUpgrade upgrade ) + { + TurtleUpgrades.register( upgrade ); + } + + @Override + public void registerBundledRedstoneProvider( @Nonnull IBundledRedstoneProvider provider ) + { + BundledRedstone.register( provider ); + } + + @Override + public int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + return BundledRedstone.getDefaultOutput( world, pos, side ); + } + + @Override + public void registerMediaProvider( @Nonnull IMediaProvider provider ) + { + MediaProviders.register( provider ); + } + + @Override + public void registerPocketUpgrade( @Nonnull IPocketUpgrade upgrade ) + { + PocketUpgrades.register( upgrade ); + } + + @Nonnull + @Override + public IPacketNetwork getWirelessNetwork() + { + return WirelessNetwork.getUniversal(); + } + + @Override + public void registerAPIFactory( @Nonnull ILuaAPIFactory factory ) + { + ApiFactories.register( factory ); + } + + @Nonnull + @Override + public IWiredNode createWiredNodeForElement( @Nonnull IWiredElement element ) + { + return new WiredNode( element ); + } + + @Nonnull + @Override + public LazyOptional getWiredElementAt( @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + BlockEntity tile = world.getBlockEntity( pos ); + return tile == null ? LazyOptional.empty() : tile.getCapability( CAPABILITY_WIRED_ELEMENT, side ); + } +} diff --git a/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java b/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java new file mode 100644 index 000000000..0b3511e7b --- /dev/null +++ b/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java @@ -0,0 +1,280 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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; + +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.ILuaAPIFactory; +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.api.media.IMediaProvider; +import dan200.computercraft.api.network.IPacketNetwork; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IPeripheralProvider; +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.redstone.IBundledRedstoneProvider; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.BlockView; +import net.minecraft.world.World; +import net.minecraftforge.common.util.LazyOptional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The static entry point to the ComputerCraft API. + * + * Members in this class must be called after mod_ComputerCraft has been initialised, but may be called before it is + * fully loaded. + */ +public final class ComputerCraftAPI +{ + @Nonnull + public static String getInstalledVersion() + { + return getInstance().getInstalledVersion(); + } + + @Nonnull + @Deprecated + public static String getAPIVersion() + { + return getInstalledVersion(); + } + + /** + * Creates a numbered directory in a subfolder of the save directory for a given world, and returns that number. + * + * Use in conjunction with createSaveDirMount() to create a unique place for your peripherals or media items to store files. + * + * @param world The world for which the save dir should be created. This should be the server side world object. + * @param parentSubPath The folder path within the save directory where the new directory should be created. eg: "computercraft/disk" + * @return The numerical value of the name of the new folder, or -1 if the folder could not be created for some reason. + * + * eg: if createUniqueNumberedSaveDir( world, "computer/disk" ) was called returns 42, then "computer/disk/42" is now + * available for writing. + * @see #createSaveDirMount(World, String, long) + */ + public static int createUniqueNumberedSaveDir( @Nonnull World world, @Nonnull String parentSubPath ) + { + return getInstance().createUniqueNumberedSaveDir( world, parentSubPath ); + } + + /** + * Creates a file system mount that maps to a subfolder of the save directory for a given world, and returns it. + * + * Use in conjunction with IComputerAccess.mount() or IComputerAccess.mountWritable() to mount a folder from the + * users save directory onto a computers file system. + * + * @param world The world for which the save dir can be found. This should be the server side world object. + * @param subPath The folder path within the save directory that the mount should map to. eg: "computer/disk/42". + * Use createUniqueNumberedSaveDir() to create a new numbered folder to use. + * @param capacity The amount of data that can be stored in the directory before it fills up, in bytes. + * @return The mount, or null if it could be created for some reason. Use IComputerAccess.mount() or IComputerAccess.mountWritable() + * to mount this on a Computers' file system. + * @see #createUniqueNumberedSaveDir(World, String) + * @see IComputerAccess#mount(String, IMount) + * @see IComputerAccess#mountWritable(String, IWritableMount) + * @see IMount + * @see IWritableMount + */ + @Nullable + public static IWritableMount createSaveDirMount( @Nonnull World world, @Nonnull String subPath, long capacity ) + { + return getInstance().createSaveDirMount( world, subPath, capacity ); + } + + /** + * Creates a file system mount to a resource folder, and returns it. + * + * Use in conjunction with {@link IComputerAccess#mount} or {@link IComputerAccess#mountWritable} to mount a + * resource folder onto a computer's file system. + * + * The files in this mount will be a combination of files in all mod jar, and data packs that contain + * resources with the same domain and path. + * + * @param domain The domain under which to look for resources. eg: "mymod". + * @param subPath The subPath under which to look for resources. eg: "lua/myfiles". + * @return The mount, or {@code null} if it could be created for some reason. + * @see IComputerAccess#mount(String, IMount) + * @see IComputerAccess#mountWritable(String, IWritableMount) + * @see IMount + */ + @Nullable + public static IMount createResourceMount( @Nonnull String domain, @Nonnull String subPath ) + { + return getInstance().createResourceMount( domain, subPath ); + } + + /** + * Registers a peripheral provider to convert blocks into {@link IPeripheral} implementations. + * + * @param provider The peripheral provider to register. + * @see IPeripheral + * @see IPeripheralProvider + */ + public static void registerPeripheralProvider( @Nonnull IPeripheralProvider provider ) + { + getInstance().registerPeripheralProvider( provider ); + } + + /** + * Registers a new turtle turtle for use in ComputerCraft. After calling this, + * users should be able to craft Turtles with your new turtle. It is recommended to call + * this during the load() method of your mod. + * + * @param upgrade The turtle upgrade to register. + * @see ITurtleUpgrade + */ + public static void registerTurtleUpgrade( @Nonnull ITurtleUpgrade upgrade ) + { + getInstance().registerTurtleUpgrade( upgrade ); + } + + /** + * Registers a bundled redstone provider to provide bundled redstone output for blocks. + * + * @param provider The bundled redstone provider to register. + * @see IBundledRedstoneProvider + */ + public static void registerBundledRedstoneProvider( @Nonnull IBundledRedstoneProvider provider ) + { + getInstance().registerBundledRedstoneProvider( provider ); + } + + /** + * If there is a Computer or Turtle at a certain position in the world, get it's bundled redstone output. + * + * @param world The world this block is in. + * @param pos The position this block is at. + * @param side The side to extract the bundled redstone output from. + * @return If there is a block capable of emitting bundled redstone at the location, it's signal (0-65535) will be returned. + * If there is no block capable of emitting bundled redstone at the location, -1 will be returned. + * @see IBundledRedstoneProvider + */ + public static int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + return getInstance().getBundledRedstoneOutput( world, pos, side ); + } + + /** + * Registers a media provider to provide {@link IMedia} implementations for Items. + * + * @param provider The media provider to register. + * @see IMediaProvider + */ + public static void registerMediaProvider( @Nonnull IMediaProvider provider ) + { + getInstance().registerMediaProvider( provider ); + } + + public static void registerPocketUpgrade( @Nonnull IPocketUpgrade upgrade ) + { + getInstance().registerPocketUpgrade( upgrade ); + } + + /** + * Attempt to get the game-wide wireless network. + * + * @return The global wireless network, or {@code null} if it could not be fetched. + */ + public static IPacketNetwork getWirelessNetwork() + { + return getInstance().getWirelessNetwork(); + } + + public static void registerAPIFactory( @Nonnull ILuaAPIFactory factory ) + { + getInstance().registerAPIFactory( factory ); + } + + /** + * Construct a new wired node for a given wired element. + * + * @param element The element to construct it for + * @return The element's node + * @see IWiredElement#getNode() + */ + @Nonnull + public static IWiredNode createWiredNodeForElement( @Nonnull IWiredElement element ) + { + return getInstance().createWiredNodeForElement( element ); + } + + /** + * Get the wired network element for a block in world. + * + * @param world The world the block exists in + * @param pos The position the block exists in + * @param side The side to extract the network element from + * @return The element's node + * @see IWiredElement#getNode() + */ + @Nonnull + public static LazyOptional getWiredElementAt( @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + return getInstance().getWiredElementAt( world, pos, side ); + } + + private static IComputerCraftAPI instance; + + @Nonnull + private static IComputerCraftAPI getInstance() + { + if( instance != null ) return instance; + + try + { + return instance = (IComputerCraftAPI) Class.forName( "dan200.computercraft.ComputerCraftAPIImpl" ) + .getField( "INSTANCE" ).get( null ); + } + catch( ReflectiveOperationException e ) + { + throw new IllegalStateException( "Cannot find ComputerCraft API", e ); + } + } + + public interface IComputerCraftAPI + { + @Nonnull + String getInstalledVersion(); + + int createUniqueNumberedSaveDir( @Nonnull World world, @Nonnull String parentSubPath ); + + @Nullable + IWritableMount createSaveDirMount( @Nonnull World world, @Nonnull String subPath, long capacity ); + + @Nullable + IMount createResourceMount( @Nonnull String domain, @Nonnull String subPath ); + + void registerPeripheralProvider( @Nonnull IPeripheralProvider provider ); + + void registerTurtleUpgrade( @Nonnull ITurtleUpgrade upgrade ); + + void registerBundledRedstoneProvider( @Nonnull IBundledRedstoneProvider provider ); + + int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ); + + void registerMediaProvider( @Nonnull IMediaProvider provider ); + + void registerPocketUpgrade( @Nonnull IPocketUpgrade upgrade ); + + @Nonnull + IPacketNetwork getWirelessNetwork(); + + void registerAPIFactory( @Nonnull ILuaAPIFactory factory ); + + @Nonnull + IWiredNode createWiredNodeForElement( @Nonnull IWiredElement element ); + + @Nonnull + LazyOptional getWiredElementAt( @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction side ); + } +} diff --git a/src/main/java/dan200/computercraft/api/client/TransformedModel.java b/src/main/java/dan200/computercraft/api/client/TransformedModel.java new file mode 100644 index 000000000..b585d0f6e --- /dev/null +++ b/src/main/java/dan200/computercraft/api/client/TransformedModel.java @@ -0,0 +1,60 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.client; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.BakedModelManager; +import net.minecraft.client.util.ModelIdentifier; +import net.minecraft.client.util.math.AffineTransformation; +import net.minecraft.item.ItemStack; +import javax.annotation.Nonnull; +import java.util.Objects; + +/** + * A model to render, combined with a transformation matrix to apply. + */ +public final class TransformedModel +{ + private final BakedModel model; + private final AffineTransformation matrix; + + public TransformedModel( @Nonnull BakedModel model, @Nonnull AffineTransformation matrix ) + { + this.model = Objects.requireNonNull( model ); + this.matrix = Objects.requireNonNull( matrix ); + } + + public TransformedModel( @Nonnull BakedModel model ) + { + this.model = Objects.requireNonNull( model ); + this.matrix = AffineTransformation.identity(); + } + + public static TransformedModel of( @Nonnull ModelIdentifier location ) + { + BakedModelManager modelManager = MinecraftClient.getInstance().getBakedModelManager(); + return new TransformedModel( modelManager.getModel( location ) ); + } + + public static TransformedModel of( @Nonnull ItemStack item, @Nonnull AffineTransformation transform ) + { + BakedModel model = MinecraftClient.getInstance().getItemRenderer().getModels().getModel( item ); + return new TransformedModel( model, transform ); + } + + @Nonnull + public BakedModel getModel() + { + return model; + } + + @Nonnull + public AffineTransformation getMatrix() + { + return matrix; + } +} diff --git a/src/main/java/dan200/computercraft/api/filesystem/FileAttributes.java b/src/main/java/dan200/computercraft/api/filesystem/FileAttributes.java new file mode 100644 index 000000000..5dcf3e964 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/filesystem/FileAttributes.java @@ -0,0 +1,81 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.filesystem; + +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.time.Instant; + +/** + * A simple version of {@link BasicFileAttributes}, which provides what information a {@link IMount} already exposes. + */ +final class FileAttributes implements BasicFileAttributes +{ + private static final FileTime EPOCH = FileTime.from( Instant.EPOCH ); + + private final boolean isDirectory; + private final long size; + + FileAttributes( boolean isDirectory, long size ) + { + this.isDirectory = isDirectory; + this.size = size; + } + + @Override + public FileTime lastModifiedTime() + { + return EPOCH; + } + + @Override + public FileTime lastAccessTime() + { + return EPOCH; + } + + @Override + public FileTime creationTime() + { + return EPOCH; + } + + @Override + public boolean isRegularFile() + { + return !isDirectory; + } + + @Override + public boolean isDirectory() + { + return isDirectory; + } + + @Override + public boolean isSymbolicLink() + { + return false; + } + + @Override + public boolean isOther() + { + return false; + } + + @Override + public long size() + { + return size; + } + + @Override + public Object fileKey() + { + return null; + } +} diff --git a/src/main/java/dan200/computercraft/api/filesystem/FileOperationException.java b/src/main/java/dan200/computercraft/api/filesystem/FileOperationException.java new file mode 100644 index 000000000..70a25fe5f --- /dev/null +++ b/src/main/java/dan200/computercraft/api/filesystem/FileOperationException.java @@ -0,0 +1,41 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.filesystem; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.Objects; + +/** + * An {@link IOException} which occurred on a specific file. + * + * This may be thrown from a {@link IMount} or {@link IWritableMount} to give more information about a failure. + */ +public class FileOperationException extends IOException +{ + private static final long serialVersionUID = -8809108200853029849L; + + private final String filename; + + public FileOperationException( @Nullable String filename, @Nonnull String message ) + { + super( Objects.requireNonNull( message, "message cannot be null" ) ); + this.filename = filename; + } + + public FileOperationException( @Nonnull String message ) + { + super( Objects.requireNonNull( message, "message cannot be null" ) ); + this.filename = null; + } + + @Nullable + public String getFilename() + { + return filename; + } +} diff --git a/src/main/java/dan200/computercraft/api/filesystem/IFileSystem.java b/src/main/java/dan200/computercraft/api/filesystem/IFileSystem.java new file mode 100644 index 000000000..7ef4a1c12 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/filesystem/IFileSystem.java @@ -0,0 +1,43 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.filesystem; + +import java.io.IOException; + +/** + * Provides a mount of the entire computer's file system. + * + * This exists for use by various APIs - one should not attempt to mount it. + */ +public interface IFileSystem extends IWritableMount +{ + /** + * Combine two paths together, reducing them into a normalised form. + * + * @param path The main path. + * @param child The path to append. + * @return The combined, normalised path. + */ + String combine( String path, String child ); + + /** + * Copy files from one location to another. + * + * @param from The location to copy from. + * @param to The location to copy to. This should not exist. + * @throws IOException If the copy failed. + */ + void copy( String from, String to ) throws IOException; + + /** + * Move files from one location to another. + * + * @param from The location to move from. + * @param to The location to move to. This should not exist. + * @throws IOException If the move failed. + */ + void move( String from, String to ) throws IOException; +} diff --git a/src/main/java/dan200/computercraft/api/filesystem/IMount.java b/src/main/java/dan200/computercraft/api/filesystem/IMount.java new file mode 100644 index 000000000..258843822 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/filesystem/IMount.java @@ -0,0 +1,94 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.filesystem; + +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.peripheral.IComputerAccess; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; + +/** + * Represents a read only part of a virtual filesystem that can be mounted onto a computer using + * {@link IComputerAccess#mount(String, IMount)}. + * + * Ready made implementations of this interface can be created using + * {@link ComputerCraftAPI#createSaveDirMount(World, String, long)} or + * {@link ComputerCraftAPI#createResourceMount(String, String)}, or you're free to implement it yourselves! + * + * @see ComputerCraftAPI#createSaveDirMount(World, String, long) + * @see ComputerCraftAPI#createResourceMount(String, String) + * @see IComputerAccess#mount(String, IMount) + * @see IWritableMount + */ +public interface IMount +{ + /** + * Returns whether a file with a given path exists or not. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram" + * @return If the file exists. + * @throws IOException If an error occurs when checking the existence of the file. + */ + boolean exists( @Nonnull String path ) throws IOException; + + /** + * Returns whether a file with a given path is a directory or not. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprograms". + * @return If the file exists and is a directory + * @throws IOException If an error occurs when checking whether the file is a directory. + */ + boolean isDirectory( @Nonnull String path ) throws IOException; + + /** + * Returns the file names of all the files in a directory. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprograms". + * @param contents A list of strings. Add all the file names to this list. + * @throws IOException If the file was not a directory, or could not be listed. + */ + void list( @Nonnull String path, @Nonnull List contents ) throws IOException; + + /** + * Returns the size of a file with a given path, in bytes. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". + * @return The size of the file, in bytes. + * @throws IOException If the file does not exist, or its size could not be determined. + */ + long getSize( @Nonnull String path ) throws IOException; + + /** + * Opens a file with a given path, and returns an {@link ReadableByteChannel} representing its contents. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". + * @return A channel representing the contents of the file. If the channel implements + * {@link java.nio.channels.SeekableByteChannel}, one will be able to seek to arbitrary positions when using binary + * mode. + * @throws IOException If the file does not exist, or could not be opened. + */ + @Nonnull + ReadableByteChannel openForRead( @Nonnull String path ) throws IOException; + + /** + * Get attributes about the given file. + * + * @param path The path to query. + * @return File attributes for the given file. + * @throws IOException If the file does not exist, or attributes could not be fetched. + */ + @Nonnull + default BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException + { + if( !exists( path ) ) throw new FileOperationException( path, "No such file" ); + return new FileAttributes( isDirectory( path ), getSize( path ) ); + } +} diff --git a/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java b/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java new file mode 100644 index 000000000..5a52b5ae7 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java @@ -0,0 +1,90 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.filesystem; + +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.peripheral.IComputerAccess; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.channels.WritableByteChannel; +import java.util.OptionalLong; + +/** + * Represents a part of a virtual filesystem that can be mounted onto a computer using {@link IComputerAccess#mount(String, IMount)} + * or {@link IComputerAccess#mountWritable(String, IWritableMount)}, that can also be written to. + * + * Ready made implementations of this interface can be created using + * {@link ComputerCraftAPI#createSaveDirMount(World, String, long)}, or you're free to implement it yourselves! + * + * @see ComputerCraftAPI#createSaveDirMount(World, String, long) + * @see IComputerAccess#mount(String, IMount) + * @see IComputerAccess#mountWritable(String, IWritableMount) + * @see IMount + */ +public interface IWritableMount extends IMount +{ + /** + * Creates a directory at a given path inside the virtual file system. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/mynewprograms". + * @throws IOException If the directory already exists or could not be created. + */ + void makeDirectory( @Nonnull String path ) throws IOException; + + /** + * Deletes a directory at a given path inside the virtual file system. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myoldprograms". + * @throws IOException If the file does not exist or could not be deleted. + */ + void delete( @Nonnull String path ) throws IOException; + + /** + * Opens a file with a given path, and returns an {@link OutputStream} for writing to it. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". + * @return A stream for writing to. If the channel implements {@link java.nio.channels.SeekableByteChannel}, one + * will be able to seek to arbitrary positions when using binary mode. + * @throws IOException If the file could not be opened for writing. + */ + @Nonnull + WritableByteChannel openForWrite( @Nonnull String path ) throws IOException; + + /** + * Opens a file with a given path, and returns an {@link OutputStream} for appending to it. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". + * @return A stream for writing to. If the channel implements {@link java.nio.channels.SeekableByteChannel}, one + * will be able to seek to arbitrary positions when using binary mode. + * @throws IOException If the file could not be opened for writing. + */ + @Nonnull + WritableByteChannel openForAppend( @Nonnull String path ) throws IOException; + + /** + * Get the amount of free space on the mount, in bytes. You should decrease this value as the user writes to the + * mount, and write operations should fail once it reaches zero. + * + * @return The amount of free space, in bytes. + * @throws IOException If the remaining space could not be computed. + */ + long getRemainingSpace() throws IOException; + + /** + * Get the capacity of this mount. This should be equal to the size of all files/directories on this mount, minus + * the {@link #getRemainingSpace()}. + * + * @return The capacity of this mount, in bytes. + */ + @Nonnull + default OptionalLong getCapacity() + { + return OptionalLong.empty(); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/IArguments.java b/src/main/java/dan200/computercraft/api/lua/IArguments.java new file mode 100644 index 000000000..c19ed4526 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/IArguments.java @@ -0,0 +1,407 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Optional; + +import static dan200.computercraft.api.lua.LuaValues.checkFinite; + +/** + * The arguments passed to a function. + */ +public interface IArguments +{ + /** + * Get the number of arguments passed to this function. + * + * @return The number of passed arguments. + */ + int count(); + + /** + * Get the argument at the specific index. The returned value must obey the following conversion rules: + * + *
    + *
  • Lua values of type "string" will be represented by a {@link String}.
  • + *
  • Lua values of type "number" will be represented by a {@link Number}.
  • + *
  • Lua values of type "boolean" will be represented by a {@link Boolean}.
  • + *
  • Lua values of type "table" will be represented by a {@link Map}.
  • + *
  • Lua values of any other type will be represented by a {@code null} value.
  • + *
+ * + * @param index The argument number. + * @return The argument's value, or {@code null} if not present. + */ + @Nullable + Object get( int index ); + + /** + * Drop a number of arguments. The returned arguments instance will access arguments at position {@code i + count}, + * rather than {@code i}. However, errors will still use the given argument index. + * + * @param count The number of arguments to drop. + * @return The new {@link IArguments} instance. + */ + IArguments drop( int count ); + + default Object[] getAll() + { + Object[] result = new Object[count()]; + for( int i = 0; i < result.length; i++ ) result[i] = get( i ); + return result; + } + + /** + * Get an argument as a double. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a number. + * @see #getFiniteDouble(int) if you require this to be finite (i.e. not infinite or NaN). + */ + default double getDouble( int index ) throws LuaException + { + Object value = get( index ); + if( !(value instanceof Number) ) throw LuaValues.badArgumentOf( index, "number", value ); + return ((Number) value).doubleValue(); + } + + /** + * Get an argument as an integer. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not an integer. + */ + default int getInt( int index ) throws LuaException + { + return (int) getLong( index ); + } + + /** + * Get an argument as a long. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a long. + */ + default long getLong( int index ) throws LuaException + { + Object value = get( index ); + if( !(value instanceof Number) ) throw LuaValues.badArgumentOf( index, "number", value ); + return LuaValues.checkFiniteNum( index, (Number) value ).longValue(); + } + + /** + * Get an argument as a finite number (not infinite or NaN). + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not finite. + */ + default double getFiniteDouble( int index ) throws LuaException + { + return checkFinite( index, getDouble( index ) ); + } + + /** + * Get an argument as a boolean. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a boolean. + */ + default boolean getBoolean( int index ) throws LuaException + { + Object value = get( index ); + if( !(value instanceof Boolean) ) throw LuaValues.badArgumentOf( index, "boolean", value ); + return (Boolean) value; + } + + /** + * Get an argument as a string. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a string. + */ + @Nonnull + default String getString( int index ) throws LuaException + { + Object value = get( index ); + if( !(value instanceof String) ) throw LuaValues.badArgumentOf( index, "string", value ); + return (String) value; + } + + /** + * Get a string argument as a byte array. + * + * @param index The argument number. + * @return The argument's value. This is a read only buffer. + * @throws LuaException If the value is not a string. + */ + @Nonnull + default ByteBuffer getBytes( int index ) throws LuaException + { + return LuaValues.encode( getString( index ) ); + } + + /** + * Get a string argument as an enum value. + * + * @param index The argument number. + * @param klass The type of enum to parse. + * @param The type of enum to parse. + * @return The argument's value. + * @throws LuaException If the value is not a string or not a valid option for this enum. + */ + @Nonnull + default > T getEnum( int index, Class klass ) throws LuaException + { + return LuaValues.checkEnum( index, klass, getString( index ) ); + } + + /** + * Get an argument as a table. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a table. + */ + @Nonnull + default Map getTable( int index ) throws LuaException + { + Object value = get( index ); + if( !(value instanceof Map) ) throw LuaValues.badArgumentOf( index, "table", value ); + return (Map) value; + } + + /** + * Get an argument as a double. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a number. + */ + @Nonnull + default Optional optDouble( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof Number) ) throw LuaValues.badArgumentOf( index, "number", value ); + return Optional.of( ((Number) value).doubleValue() ); + } + + /** + * Get an argument as an int. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a number. + */ + @Nonnull + default Optional optInt( int index ) throws LuaException + { + return optLong( index ).map( Long::intValue ); + } + + /** + * Get an argument as a long. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a number. + */ + default Optional optLong( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof Number) ) throw LuaValues.badArgumentOf( index, "number", value ); + return Optional.of( LuaValues.checkFiniteNum( index, (Number) value ).longValue() ); + } + + /** + * Get an argument as a finite number (not infinite or NaN). + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not finite. + */ + default Optional optFiniteDouble( int index ) throws LuaException + { + Optional value = optDouble( index ); + if( value.isPresent() ) LuaValues.checkFiniteNum( index, value.get() ); + return value; + } + + /** + * Get an argument as a boolean. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a boolean. + */ + default Optional optBoolean( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof Boolean) ) throw LuaValues.badArgumentOf( index, "boolean", value ); + return Optional.of( (Boolean) value ); + } + + /** + * Get an argument as a string. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a string. + */ + default Optional optString( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof String) ) throw LuaValues.badArgumentOf( index, "string", value ); + return Optional.of( (String) value ); + } + + /** + * Get a string argument as a byte array. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. This is a read only buffer. + * @throws LuaException If the value is not a string. + */ + default Optional optBytes( int index ) throws LuaException + { + return optString( index ).map( LuaValues::encode ); + } + + /** + * Get a string argument as an enum value. + * + * @param index The argument number. + * @param klass The type of enum to parse. + * @param The type of enum to parse. + * @return The argument's value. + * @throws LuaException If the value is not a string or not a valid option for this enum. + */ + @Nonnull + default > Optional optEnum( int index, Class klass ) throws LuaException + { + Optional str = optString( index ); + return str.isPresent() ? Optional.of( LuaValues.checkEnum( index, klass, str.get() ) ) : Optional.empty(); + } + + /** + * Get an argument as a table. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a table. + */ + default Optional> optTable( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof Map) ) throw LuaValues.badArgumentOf( index, "map", value ); + return Optional.of( (Map) value ); + } + + /** + * Get an argument as a double. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a number. + */ + default double optDouble( int index, double def ) throws LuaException + { + return optDouble( index ).orElse( def ); + } + + /** + * Get an argument as an int. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a number. + */ + default int optInt( int index, int def ) throws LuaException + { + return optInt( index ).orElse( def ); + } + + /** + * Get an argument as a long. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a number. + */ + default long optLong( int index, long def ) throws LuaException + { + return optLong( index ).orElse( def ); + } + + /** + * Get an argument as a finite number (not infinite or NaN). + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not finite. + */ + default double optFiniteDouble( int index, double def ) throws LuaException + { + return optFiniteDouble( index ).orElse( def ); + } + + /** + * Get an argument as a boolean. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a boolean. + */ + default boolean optBoolean( int index, boolean def ) throws LuaException + { + return optBoolean( index ).orElse( def ); + } + + /** + * Get an argument as a string. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a string. + */ + default String optString( int index, String def ) throws LuaException + { + return optString( index ).orElse( def ); + } + + /** + * Get an argument as a table. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a table. + */ + default Map optTable( int index, Map def ) throws LuaException + { + return optTable( index ).orElse( def ); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/IComputerSystem.java b/src/main/java/dan200/computercraft/api/lua/IComputerSystem.java new file mode 100644 index 000000000..c0717bcb2 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/IComputerSystem.java @@ -0,0 +1,34 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import dan200.computercraft.api.filesystem.IFileSystem; +import dan200.computercraft.api.peripheral.IComputerAccess; + +import javax.annotation.Nullable; + +/** + * An interface passed to {@link ILuaAPIFactory} in order to provide additional information + * about a computer. + */ +public interface IComputerSystem extends IComputerAccess +{ + /** + * Get the file system for this computer. + * + * @return The computer's file system, or {@code null} if it is not initialised. + */ + @Nullable + IFileSystem getFileSystem(); + + /** + * Get the label for this computer. + * + * @return This computer's label, or {@code null} if it is not set. + */ + @Nullable + String getLabel(); +} diff --git a/src/main/java/dan200/computercraft/api/lua/IDynamicLuaObject.java b/src/main/java/dan200/computercraft/api/lua/IDynamicLuaObject.java new file mode 100644 index 000000000..13b2c7a8f --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/IDynamicLuaObject.java @@ -0,0 +1,45 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import dan200.computercraft.api.peripheral.IDynamicPeripheral; + +import javax.annotation.Nonnull; + +/** + * An interface for representing custom objects returned by peripherals or other Lua objects. + * + * Generally, one does not need to implement this type - it is sufficient to return an object with some methods + * annotated with {@link LuaFunction}. {@link IDynamicLuaObject} is useful when you wish your available methods to + * change at runtime. + */ +public interface IDynamicLuaObject +{ + /** + * Get the names of the methods that this object implements. This should not change over the course of the object's + * lifetime. + * + * @return The method names this object provides. + * @see IDynamicPeripheral#getMethodNames() + */ + @Nonnull + String[] getMethodNames(); + + /** + * Called when a user calls one of the methods that this object implements. + * + * @param context The context of the currently running lua thread. This can be used to wait for events + * or otherwise yield. + * @param method An integer identifying which method index from {@link #getMethodNames()} the computer wishes + * to call. + * @param arguments The arguments for this method. + * @return The result of this function. Either an immediate value ({@link MethodResult#of(Object...)} or an + * instruction to yield. + * @throws LuaException If the function threw an exception. + */ + @Nonnull + MethodResult callMethod( @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaAPI.java b/src/main/java/dan200/computercraft/api/lua/ILuaAPI.java new file mode 100644 index 000000000..ae405e3aa --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ILuaAPI.java @@ -0,0 +1,53 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import dan200.computercraft.api.ComputerCraftAPI; + +/** + * Represents a Lua object which is stored as a global variable on computer startup. This must either provide + * {@link LuaFunction} annotated functions or implement {@link IDynamicLuaObject}. + * + * Before implementing this interface, consider alternative methods of providing methods. It is generally preferred + * to use peripherals to provide functionality to users. + * + * @see ILuaAPIFactory + * @see ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory) + */ +public interface ILuaAPI +{ + /** + * Get the globals this API will be assigned to. This will override any other global, so you should + * + * @return A list of globals this API will be assigned to. + */ + String[] getNames(); + + /** + * Called when the computer is turned on. + * + * One should only interact with the file system. + */ + default void startup() + { + } + + /** + * Called every time the computer is ticked. This can be used to process various. + */ + default void update() + { + } + + /** + * Called when the computer is turned off or unloaded. + * + * This should reset the state of the object, disposing any remaining file handles, or other resources. + */ + default void shutdown() + { + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaAPIFactory.java b/src/main/java/dan200/computercraft/api/lua/ILuaAPIFactory.java new file mode 100644 index 000000000..763a8bdaf --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ILuaAPIFactory.java @@ -0,0 +1,30 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import dan200.computercraft.api.ComputerCraftAPI; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Construct an {@link ILuaAPI} for a specific computer. + * + * @see ILuaAPI + * @see ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory) + */ +@FunctionalInterface +public interface ILuaAPIFactory +{ + /** + * Create a new API instance for a given computer. + * + * @param computer The computer this API is for. + * @return The created API, or {@code null} if one should not be injected. + */ + @Nullable + ILuaAPI create( @Nonnull IComputerSystem computer ); +} diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaCallback.java b/src/main/java/dan200/computercraft/api/lua/ILuaCallback.java new file mode 100644 index 000000000..f05d82a73 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ILuaCallback.java @@ -0,0 +1,27 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import javax.annotation.Nonnull; + +/** + * A continuation which is called when this coroutine is resumed. + * + * @see MethodResult#yield(Object[], ILuaCallback) + */ +public interface ILuaCallback +{ + /** + * Resume this coroutine. + * + * @param args The result of resuming this coroutine. These will have the same form as described in + * {@link LuaFunction}. + * @return The result of this continuation. Either the result to return to the callee, or another yield. + * @throws LuaException On an error. + */ + @Nonnull + MethodResult resume( Object[] args ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaContext.java b/src/main/java/dan200/computercraft/api/lua/ILuaContext.java new file mode 100644 index 000000000..569b61e5b --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ILuaContext.java @@ -0,0 +1,30 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import javax.annotation.Nonnull; + +/** + * An interface passed to peripherals and {@link IDynamicLuaObject}s by computers or turtles, providing methods + * that allow the peripheral call to interface with the computer. + */ +public interface ILuaContext +{ + /** + * Queue a task to be executed on the main server thread at the beginning of next tick, but do not wait for it to + * complete. This should be used when you need to interact with the world in a thread-safe manner but do not care + * about the result or you wish to run asynchronously. + * + * When the task has finished, it will enqueue a {@code task_completed} event, which takes the task id, a success + * value and the return values, or an error message if it failed. + * + * @param task The task to execute on the main thread. + * @return The "id" of the task. This will be the first argument to the {@code task_completed} event. + * @throws LuaException If the task could not be queued. + * @see LuaFunction#mainThread() To run functions on the main thread and return their results synchronously. + */ + long issueMainThreadTask( @Nonnull ILuaTask task ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaFunction.java b/src/main/java/dan200/computercraft/api/lua/ILuaFunction.java new file mode 100644 index 000000000..cd75848e1 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ILuaFunction.java @@ -0,0 +1,29 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import javax.annotation.Nonnull; + +/** + * A function, which can be called from Lua. If you need to return a table of functions, it is recommended to use + * an object with {@link LuaFunction} methods, or implement {@link IDynamicLuaObject}. + * + * @see MethodResult#of(Object) + */ +@FunctionalInterface +public interface ILuaFunction +{ + /** + * Call this function with a series of arguments. Note, this will always be called on the computer thread, + * and so its implementation must be thread-safe. + * + * @param arguments The arguments for this function + * @return The result of calling this function. + * @throws LuaException Upon Lua errors. + */ + @Nonnull + MethodResult call( @Nonnull IArguments arguments ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaTask.java b/src/main/java/dan200/computercraft/api/lua/ILuaTask.java new file mode 100644 index 000000000..4ea060358 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ILuaTask.java @@ -0,0 +1,30 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import javax.annotation.Nullable; + +/** + * A task which can be executed via {@link ILuaContext#issueMainThreadTask(ILuaTask)} This will be run on the main + * thread, at the beginning of the + * next tick. + * + * @see ILuaContext#issueMainThreadTask(ILuaTask) + */ +@FunctionalInterface +public interface ILuaTask +{ + /** + * Execute this task. + * + * @return The arguments to add to the {@code task_completed} event. + * @throws LuaException If you throw any exception from this function, a lua error will be raised with the + * same message as your exception. Use this to throw appropriate errors if the wrong + * arguments are supplied to your method. + */ + @Nullable + Object[] execute() throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/api/lua/LuaException.java b/src/main/java/dan200/computercraft/api/lua/LuaException.java new file mode 100644 index 000000000..949a8eeba --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/LuaException.java @@ -0,0 +1,53 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import javax.annotation.Nullable; + +/** + * An exception representing an error in Lua, like that raised by the {@code error()} function. + */ +public class LuaException extends Exception +{ + private static final long serialVersionUID = -6136063076818512651L; + private final boolean hasLevel; + private final int level; + + public LuaException( @Nullable String message ) + { + super( message ); + this.hasLevel = false; + this.level = 1; + } + + public LuaException( @Nullable String message, int level ) + { + super( message ); + this.hasLevel = true; + this.level = level; + } + + /** + * Whether a level was explicitly specified when constructing. This is used to determine + * + * @return Whether this has an explicit level. + */ + public boolean hasLevel() + { + return hasLevel; + } + + /** + * The level this error is raised at. Level 1 is the function's caller, level 2 is that function's caller, and so + * on. + * + * @return The level to raise the error at. + */ + public int getLevel() + { + return level; + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/LuaFunction.java b/src/main/java/dan200/computercraft/api/lua/LuaFunction.java new file mode 100644 index 000000000..e17e30ac4 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/LuaFunction.java @@ -0,0 +1,58 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; + +import java.lang.annotation.*; +import java.util.Map; +import java.util.Optional; + +/** + * Used to mark a Java function which is callable from Lua. + * + * Methods annotated with {@link LuaFunction} must be public final instance methods. They can have any number of + * parameters, but they must be of the following types: + * + *
    + *
  • {@link ILuaContext} (and {@link IComputerAccess} if on a {@link IPeripheral})
  • + *
  • {@link IArguments}: The arguments supplied to this function.
  • + *
  • + * Alternatively, one may specify the desired arguments as normal parameters and the argument parsing code will + * be generated automatically. + * + * Each parameter must be one of the given types supported by {@link IArguments} (for instance, {@link int} or + * {@link Map}). Optional values are supported by accepting a parameter of type {@link Optional}. + *
  • + *
+ * + * This function may return {@link MethodResult}. However, if you simply return a value (rather than having to yield), + * you may return {@code void}, a single value (either an object or a primitive like {@code int}) or array of objects. + * These will be treated the same as {@link MethodResult#of()}, {@link MethodResult#of(Object)} and + * {@link MethodResult#of(Object...)}. + */ +@Documented +@Retention( RetentionPolicy.RUNTIME ) +@Target( ElementType.METHOD ) +public @interface LuaFunction +{ + /** + * Explicitly specify the method names of this function. If not given, it uses the name of the annotated method. + * + * @return This function's name(s). + */ + String[] value() default {}; + + /** + * Run this function on the main server thread. This should be specified for any method which interacts with + * Minecraft in a thread-unsafe manner. + * + * @return Whether this functi + * @see ILuaContext#issueMainThreadTask(ILuaTask) + */ + boolean mainThread() default false; +} diff --git a/src/main/java/dan200/computercraft/api/lua/LuaValues.java b/src/main/java/dan200/computercraft/api/lua/LuaValues.java new file mode 100644 index 000000000..f89b24a17 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/LuaValues.java @@ -0,0 +1,152 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.Map; + +/** + * Various utility functions for operating with Lua values. + * + * @see IArguments + */ +public final class LuaValues +{ + private LuaValues() + { + } + + /** + * Encode a Lua string into a read-only {@link ByteBuffer}. + * + * @param string The string to encode. + * @return The encoded string. + */ + @Nonnull + public static ByteBuffer encode( @Nonnull String string ) + { + byte[] chars = new byte[string.length()]; + for( int i = 0; i < chars.length; i++ ) + { + char c = string.charAt( i ); + chars[i] = c < 256 ? (byte) c : 63; + } + + return ByteBuffer.wrap( chars ).asReadOnlyBuffer(); + } + + /** + * Returns a more detailed representation of this number's type. If this is finite, it will just return "number", + * otherwise it returns whether it is infinite or NaN. + * + * @param value The value to extract the type for. + * @return This value's numeric type. + */ + @Nonnull + public static String getNumericType( double value ) + { + if( Double.isNaN( value ) ) return "nan"; + if( value == Double.POSITIVE_INFINITY ) return "inf"; + if( value == Double.NEGATIVE_INFINITY ) return "-inf"; + return "number"; + } + + /** + * Get a string representation of the given value's type. + * + * @param value The value whose type we are trying to compute. + * @return A string representation of the given value's type, in a similar format to that provided by Lua's + * {@code type} function. + */ + @Nonnull + public static String getType( @Nullable Object value ) + { + if( value == null ) return "nil"; + if( value instanceof String ) return "string"; + if( value instanceof Boolean ) return "boolean"; + if( value instanceof Number ) return "number"; + if( value instanceof Map ) return "table"; + return "userdata"; + } + + /** + * Construct a "bad argument" exception, from an expected type and the actual value provided. + * + * @param index The argument number, starting from 0. + * @param expected The expected type for this argument. + * @param actual The actual value provided for this argument. + * @return The constructed exception, which should be thrown immediately. + */ + @Nonnull + public static LuaException badArgumentOf( int index, @Nonnull String expected, @Nullable Object actual ) + { + return badArgument( index, expected, getType( actual ) ); + } + + /** + * Construct a "bad argument" exception, from an expected and actual type. + * + * @param index The argument number, starting from 0. + * @param expected The expected type for this argument. + * @param actual The provided type for this argument. + * @return The constructed exception, which should be thrown immediately. + */ + @Nonnull + public static LuaException badArgument( int index, @Nonnull String expected, @Nonnull String actual ) + { + return new LuaException( "bad argument #" + (index + 1) + " (" + expected + " expected, got " + actual + ")" ); + } + + /** + * Ensure a numeric argument is finite (i.e. not infinite or {@link Double#NaN}. + * + * @param index The argument index to check. + * @param value The value to check. + * @return The input {@code value}. + * @throws LuaException If this is not a finite number. + */ + public static Number checkFiniteNum( int index, Number value ) throws LuaException + { + checkFinite( index, value.doubleValue() ); + return value; + } + + /** + * Ensure a numeric argument is finite (i.e. not infinite or {@link Double#NaN}. + * + * @param index The argument index to check. + * @param value The value to check. + * @return The input {@code value}. + * @throws LuaException If this is not a finite number. + */ + public static double checkFinite( int index, double value ) throws LuaException + { + if( !Double.isFinite( value ) ) throw badArgument( index, "number", getNumericType( value ) ); + return value; + } + + /** + * Ensure a string is a valid enum value. + * + * @param index The argument index to check. + * @param klass The class of the enum instance. + * @param value The value to extract. + * @param The type of enum we are extracting. + * @return The parsed enum value. + * @throws LuaException If this is not a known enum value. + */ + public static > T checkEnum( int index, Class klass, String value ) throws LuaException + { + for( T possibility : klass.getEnumConstants() ) + { + if( possibility.name().equalsIgnoreCase( value ) ) return possibility; + } + + throw new LuaException( "bad argument #" + (index + 1) + " (unknown option " + value + ")" ); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/MethodResult.java b/src/main/java/dan200/computercraft/api/lua/MethodResult.java new file mode 100644 index 000000000..fce9f970f --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/MethodResult.java @@ -0,0 +1,170 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import dan200.computercraft.api.peripheral.IComputerAccess; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * The result of invoking a Lua method. + * + * Method results either return a value immediately ({@link #of(Object...)} or yield control to the parent coroutine. + * When the current coroutine is resumed, we invoke the provided {@link ILuaCallback#resume(Object[])} callback. + */ +public final class MethodResult +{ + private static final MethodResult empty = new MethodResult( null, null ); + + private final Object[] result; + private final ILuaCallback callback; + private final int adjust; + + private MethodResult( Object[] arguments, ILuaCallback callback ) + { + this.result = arguments; + this.callback = callback; + this.adjust = 0; + } + + private MethodResult( Object[] arguments, ILuaCallback callback, int adjust ) + { + this.result = arguments; + this.callback = callback; + this.adjust = adjust; + } + + /** + * Return no values immediately. + * + * @return A method result which returns immediately with no values. + */ + @Nonnull + public static MethodResult of() + { + return empty; + } + + /** + * Return a single value immediately. + * + * Integers, doubles, floats, strings, booleans, {@link Map}, {@link Collection}s, arrays and {@code null} will be + * converted to their corresponding Lua type. {@code byte[]} and {@link ByteBuffer} will be treated as binary + * strings. {@link ILuaFunction} will be treated as a function. + * + * In order to provide a custom object with methods, one may return a {@link IDynamicLuaObject}, or an arbitrary + * class with {@link LuaFunction} annotations. Anything else will be converted to {@code nil}. + * + * @param value The value to return to the calling Lua function. + * @return A method result which returns immediately with the given value. + */ + @Nonnull + public static MethodResult of( @Nullable Object value ) + { + return new MethodResult( new Object[] { value }, null ); + } + + /** + * Return any number of values immediately. + * + * @param values The values to return. See {@link #of(Object)} for acceptable values. + * @return A method result which returns immediately with the given values. + */ + @Nonnull + public static MethodResult of( @Nullable Object... values ) + { + return values == null || values.length == 0 ? empty : new MethodResult( values, null ); + } + + /** + * Wait for an event to occur on the computer, suspending the thread until it arises. This method is exactly + * equivalent to {@code os.pullEvent()} in lua. + * + * @param filter A specific event to wait for, or null to wait for any event. + * @param callback The callback to resume with the name of the event that occurred, and any event parameters. + * @return The method result which represents this yield. + * @see IComputerAccess#queueEvent(String, Object[]) + */ + @Nonnull + public static MethodResult pullEvent( @Nullable String filter, @Nonnull ILuaCallback callback ) + { + Objects.requireNonNull( callback, "callback cannot be null" ); + return new MethodResult( new Object[] { filter }, results -> { + if( results.length >= 1 && results[0].equals( "terminate" ) ) throw new LuaException( "Terminated", 0 ); + return callback.resume( results ); + } ); + } + + /** + * The same as {@link #pullEvent(String, ILuaCallback)}, except "terminated" events are ignored. Only use this if + * you want to prevent program termination, which is not recommended. This method is exactly equivalent to + * {@code os.pullEventRaw()} in Lua. + * + * @param filter A specific event to wait for, or null to wait for any event. + * @param callback The callback to resume with the name of the event that occurred, and any event parameters. + * @return The method result which represents this yield. + * @see #pullEvent(String, ILuaCallback) + */ + @Nonnull + public static MethodResult pullEventRaw( @Nullable String filter, @Nonnull ILuaCallback callback ) + { + Objects.requireNonNull( callback, "callback cannot be null" ); + return new MethodResult( new Object[] { filter }, callback ); + } + + /** + * Yield the current coroutine with some arguments until it is resumed. This method is exactly equivalent to + * {@code coroutine.yield()} in lua. Use {@code pullEvent()} if you wish to wait for events. + * + * @param arguments An object array containing the arguments to pass to coroutine.yield() + * @param callback The callback to resume with an array containing the return values from coroutine.yield() + * @return The method result which represents this yield. + * @see #pullEvent(String, ILuaCallback) + */ + @Nonnull + public static MethodResult yield( @Nullable Object[] arguments, @Nonnull ILuaCallback callback ) + { + Objects.requireNonNull( callback, "callback cannot be null" ); + return new MethodResult( arguments, callback ); + } + + @Nullable + public Object[] getResult() + { + return result; + } + + @Nullable + public ILuaCallback getCallback() + { + return callback; + } + + public int getErrorAdjust() + { + return adjust; + } + + /** + * Increase the Lua error by a specific amount. One should never need to use this function - it largely exists for + * some CC internal code. + * + * @param adjust The amount to increase the level by. + * @return The new {@link MethodResult} with an adjusted error. This has no effect on immediate results. + */ + @Nonnull + public MethodResult adjustError( int adjust ) + { + if( adjust < 0 ) throw new IllegalArgumentException( "cannot adjust by a negative amount" ); + if( adjust == 0 || callback == null ) return this; + return new MethodResult( result, callback, this.adjust + adjust ); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java b/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java new file mode 100644 index 000000000..4ccc50b89 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java @@ -0,0 +1,66 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.lua; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * An implementation of {@link IArguments} which wraps an array of {@link Object}. + */ +public final class ObjectArguments implements IArguments +{ + private static final IArguments EMPTY = new ObjectArguments(); + private final List args; + + @Deprecated + @SuppressWarnings( "unused" ) + public ObjectArguments( IArguments arguments ) + { + throw new IllegalStateException(); + } + + public ObjectArguments( Object... args ) + { + this.args = Arrays.asList( args ); + } + + public ObjectArguments( List args ) + { + this.args = Objects.requireNonNull( args ); + } + + @Override + public int count() + { + return args.size(); + } + + @Override + public IArguments drop( int count ) + { + if( count < 0 ) throw new IllegalStateException( "count cannot be negative" ); + if( count == 0 ) return this; + if( count >= args.size() ) return EMPTY; + + return new ObjectArguments( args.subList( count, args.size() ) ); + } + + @Nullable + @Override + public Object get( int index ) + { + return index >= args.size() ? null : args.get( index ); + } + + @Override + public Object[] getAll() + { + return args.toArray(); + } +} diff --git a/src/main/java/dan200/computercraft/api/media/IMedia.java b/src/main/java/dan200/computercraft/api/media/IMedia.java new file mode 100644 index 000000000..da532fec0 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/media/IMedia.java @@ -0,0 +1,89 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.media; + +import dan200.computercraft.api.filesystem.IMount; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.sound.SoundEvent; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Represents an item that can be placed in a disk drive and used by a Computer. + * + * Implement this interface on your {@link Item} class to allow it to be used in the drive. Alternatively, register + * a {@link IMediaProvider}. + */ +public interface IMedia +{ + /** + * Get a string representing the label of this item. Will be called via {@code disk.getLabel()} in lua. + * + * @param stack The {@link ItemStack} to inspect. + * @return The label. ie: "Dan's Programs". + */ + @Nullable + String getLabel( @Nonnull ItemStack stack ); + + /** + * Set a string representing the label of this item. Will be called vi {@code disk.setLabel()} in lua. + * + * @param stack The {@link ItemStack} to modify. + * @param label The string to set the label to. + * @return true if the label was updated, false if the label may not be modified. + */ + default boolean setLabel( @Nonnull ItemStack stack, @Nullable String label ) + { + return false; + } + + /** + * If this disk represents an item with audio (like a record), get the readable name of the audio track. ie: + * "Jonathan Coulton - Still Alive" + * + * @param stack The {@link ItemStack} to modify. + * @return The name, or null if this item does not represent an item with audio. + */ + @Nullable + default String getAudioTitle( @Nonnull ItemStack stack ) + { + return null; + } + + /** + * If this disk represents an item with audio (like a record), get the resource name of the audio track to play. + * + * @param stack The {@link ItemStack} to modify. + * @return The name, or null if this item does not represent an item with audio. + */ + @Nullable + default SoundEvent getAudio( @Nonnull ItemStack stack ) + { + return null; + } + + /** + * If this disk represents an item with data (like a floppy disk), get a mount representing it's contents. This will + * be mounted onto the filesystem of the computer while the media is in the disk drive. + * + * @param stack The {@link ItemStack} to modify. + * @param world The world in which the item and disk drive reside. + * @return The mount, or null if this item does not represent an item with data. If the mount returned also + * implements {@link dan200.computercraft.api.filesystem.IWritableMount}, it will mounted using mountWritable() + * @see IMount + * @see dan200.computercraft.api.filesystem.IWritableMount + * @see dan200.computercraft.api.ComputerCraftAPI#createSaveDirMount(World, String, long) + * @see dan200.computercraft.api.ComputerCraftAPI#createResourceMount(String, String) + */ + @Nullable + default IMount createDataMount( @Nonnull ItemStack stack, @Nonnull World world ) + { + return null; + } +} diff --git a/src/main/java/dan200/computercraft/api/media/IMediaProvider.java b/src/main/java/dan200/computercraft/api/media/IMediaProvider.java new file mode 100644 index 000000000..a3605f024 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/media/IMediaProvider.java @@ -0,0 +1,30 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.media; + +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * This interface is used to provide {@link IMedia} implementations for {@link ItemStack}. + * + * @see dan200.computercraft.api.ComputerCraftAPI#registerMediaProvider(IMediaProvider) + */ +@FunctionalInterface +public interface IMediaProvider +{ + /** + * Produce an IMedia implementation from an ItemStack. + * + * @param stack The stack from which to extract the media information. + * @return An {@link IMedia} implementation, or {@code null} if the item is not something you wish to handle + * @see dan200.computercraft.api.ComputerCraftAPI#registerMediaProvider(IMediaProvider) + */ + @Nullable + IMedia getMedia( @Nonnull ItemStack stack ); +} diff --git a/src/main/java/dan200/computercraft/api/network/IPacketNetwork.java b/src/main/java/dan200/computercraft/api/network/IPacketNetwork.java new file mode 100644 index 000000000..b85ac6e38 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/IPacketNetwork.java @@ -0,0 +1,59 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.network; + +import javax.annotation.Nonnull; + +/** + * A packet network represents a collection of devices which can send and receive packets. + * + * @see Packet + * @see IPacketReceiver + */ +public interface IPacketNetwork +{ + /** + * Add a receiver to the network. + * + * @param receiver The receiver to register to the network. + */ + void addReceiver( @Nonnull IPacketReceiver receiver ); + + /** + * Remove a receiver from the network. + * + * @param receiver The device to remove from the network. + */ + void removeReceiver( @Nonnull IPacketReceiver receiver ); + + /** + * Determine whether this network is wireless. + * + * @return Whether this network is wireless. + */ + boolean isWireless(); + + /** + * Submit a packet for transmitting across the network. This will route the packet through the network, sending it + * to all receivers within range (or any interdimensional ones). + * + * @param packet The packet to send. + * @param range The maximum distance this packet will be sent. + * @see #transmitInterdimensional(Packet) + * @see IPacketReceiver#receiveSameDimension(Packet, double) + */ + void transmitSameDimension( @Nonnull Packet packet, double range ); + + /** + * Submit a packet for transmitting across the network. This will route the packet through the network, sending it + * to all receivers across all dimensions. + * + * @param packet The packet to send. + * @see #transmitSameDimension(Packet, double) + * @see IPacketReceiver#receiveDifferentDimension(Packet) + */ + void transmitInterdimensional( @Nonnull Packet packet ); +} diff --git a/src/main/java/dan200/computercraft/api/network/IPacketReceiver.java b/src/main/java/dan200/computercraft/api/network/IPacketReceiver.java new file mode 100644 index 000000000..49b37d889 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/IPacketReceiver.java @@ -0,0 +1,84 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.network; + +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +/** + * An object on an {@link IPacketNetwork}, capable of receiving packets. + */ +public interface IPacketReceiver +{ + /** + * Get the world in which this packet receiver exists. + * + * @return The receivers's world. + */ + @Nonnull + World getWorld(); + + /** + * Get the position in the world at which this receiver exists. + * + * @return The receiver's position. + */ + @Nonnull + Vec3d getPosition(); + + /** + * Get the maximum distance this receiver can send and receive messages. + * + * When determining whether a receiver can receive a message, the largest distance of the packet and receiver is + * used - ensuring it is within range. If the packet or receiver is inter-dimensional, then the packet will always + * be received. + * + * @return The maximum distance this device can send and receive messages. + * @see #isInterdimensional() + * @see #receiveSameDimension(Packet packet, double) + * @see IPacketNetwork#transmitInterdimensional(Packet) + */ + double getRange(); + + /** + * Determine whether this receiver can receive packets from other dimensions. + * + * A device will receive an inter-dimensional packet if either it or the sending device is inter-dimensional. + * + * @return Whether this receiver receives packets from other dimensions. + * @see #getRange() + * @see #receiveDifferentDimension(Packet) + * @see IPacketNetwork#transmitInterdimensional(Packet) + */ + boolean isInterdimensional(); + + /** + * Receive a network packet from the same dimension. + * + * @param packet The packet to receive. Generally you should check that you are listening on the given channel and, + * if so, queue the appropriate modem event. + * @param distance The distance this packet has travelled from the source. + * @see Packet + * @see #getRange() + * @see IPacketNetwork#transmitSameDimension(Packet, double) + * @see IPacketNetwork#transmitInterdimensional(Packet) + */ + void receiveSameDimension( @Nonnull Packet packet, double distance ); + + /** + * Receive a network packet from a different dimension. + * + * @param packet The packet to receive. Generally you should check that you are listening on the given channel and, + * if so, queue the appropriate modem event. + * @see Packet + * @see IPacketNetwork#transmitInterdimensional(Packet) + * @see IPacketNetwork#transmitSameDimension(Packet, double) + * @see #isInterdimensional() + */ + void receiveDifferentDimension( @Nonnull Packet packet ); +} diff --git a/src/main/java/dan200/computercraft/api/network/IPacketSender.java b/src/main/java/dan200/computercraft/api/network/IPacketSender.java new file mode 100644 index 000000000..bb9dc63d1 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/IPacketSender.java @@ -0,0 +1,42 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.network; + +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +/** + * An object on a {@link IPacketNetwork}, capable of sending packets. + */ +public interface IPacketSender +{ + /** + * Get the world in which this packet sender exists. + * + * @return The sender's world. + */ + @Nonnull + World getWorld(); + + /** + * Get the position in the world at which this sender exists. + * + * @return The sender's position. + */ + @Nonnull + Vec3d getPosition(); + + /** + * Get some sort of identification string for this sender. This does not strictly need to be unique, but you + * should be able to extract some identifiable information from it. + * + * @return This device's id. + */ + @Nonnull + String getSenderID(); +} diff --git a/src/main/java/dan200/computercraft/api/network/Packet.java b/src/main/java/dan200/computercraft/api/network/Packet.java new file mode 100644 index 000000000..baaaeefa4 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/Packet.java @@ -0,0 +1,117 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.network; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * Represents a packet which may be sent across a {@link IPacketNetwork}. + * + * @see IPacketSender + * @see IPacketNetwork#transmitSameDimension(Packet, double) + * @see IPacketNetwork#transmitInterdimensional(Packet) + * @see IPacketReceiver#receiveDifferentDimension(Packet) + * @see IPacketReceiver#receiveSameDimension(Packet, double) + */ +public class Packet +{ + private final int channel; + private final int replyChannel; + private final Object payload; + + private final IPacketSender sender; + + /** + * Create a new packet, ready for transmitting across the network. + * + * @param channel The channel to send the packet along. Receiving devices should only process packets from on + * channels they are listening to. + * @param replyChannel The channel to reply on. + * @param payload The contents of this packet. This should be a "valid" Lua object, safe for queuing as an + * event or returning from a peripheral call. + * @param sender The object which sent this packet. + */ + public Packet( int channel, int replyChannel, @Nullable Object payload, @Nonnull IPacketSender sender ) + { + Objects.requireNonNull( sender, "sender cannot be null" ); + + this.channel = channel; + this.replyChannel = replyChannel; + this.payload = payload; + this.sender = sender; + } + + /** + * Get the channel this packet is sent along. Receivers should generally only process packets from on channels they + * are listening to. + * + * @return This packet's channel. + */ + public int getChannel() + { + return channel; + } + + /** + * The channel to reply on. Objects which will reply should send it along this channel. + * + * @return This channel to reply on. + */ + public int getReplyChannel() + { + return replyChannel; + } + + /** + * The actual data of this packet. This should be a "valid" Lua object, safe for queuing as an + * event or returning from a peripheral call. + * + * @return The packet's payload + */ + @Nullable + public Object getPayload() + { + return payload; + } + + /** + * The object which sent this message. + * + * @return The sending object. + */ + @Nonnull + public IPacketSender getSender() + { + return sender; + } + + @Override + public boolean equals( Object o ) + { + if( this == o ) return true; + if( o == null || getClass() != o.getClass() ) return false; + + Packet packet = (Packet) o; + + if( channel != packet.channel ) return false; + if( replyChannel != packet.replyChannel ) return false; + if( !Objects.equals( payload, packet.payload ) ) return false; + return sender.equals( packet.sender ); + } + + @Override + public int hashCode() + { + int result; + result = channel; + result = 31 * result + replyChannel; + result = 31 * result + (payload != null ? payload.hashCode() : 0); + result = 31 * result + sender.hashCode(); + return result; + } +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredElement.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredElement.java new file mode 100644 index 000000000..b60f1800c --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredElement.java @@ -0,0 +1,34 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.network.wired; + +import dan200.computercraft.api.ComputerCraftAPI; + +import javax.annotation.Nonnull; + +/** + * An object which may be part of a wired network. + * + * Elements should construct a node using {@link ComputerCraftAPI#createWiredNodeForElement(IWiredElement)}. This acts + * as a proxy for all network objects. Whilst the node may change networks, an element's node should remain constant + * for its lifespan. + * + * Elements are generally tied to a block or tile entity in world. In such as case, one should provide the + * {@link IWiredElement} capability for the appropriate sides. + */ +public interface IWiredElement extends IWiredSender +{ + /** + * Called when objects on the network change. This may occur when network nodes are added or removed, or when + * peripherals change. + * + * @param change The change which occurred. + * @see IWiredNetworkChange + */ + default void networkChanged( @Nonnull IWiredNetworkChange change ) + { + } +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredNetwork.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredNetwork.java new file mode 100644 index 000000000..d2e046e77 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredNetwork.java @@ -0,0 +1,85 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.network.wired; + +import dan200.computercraft.api.peripheral.IPeripheral; + +import javax.annotation.Nonnull; +import java.util.Map; + +/** + * A wired network is composed of one of more {@link IWiredNode}s, a set of connections between them, and a series + * of peripherals. + * + * Networks from a connected graph. This means there is some path between all nodes on the network. Further more, if + * there is some path between two nodes then they must be on the same network. {@link IWiredNetwork} will automatically + * handle the merging and splitting of networks (and thus changing of available nodes and peripherals) as connections + * change. + * + * This does mean one can not rely on the network remaining consistent between subsequent operations. Consequently, + * it is generally preferred to use the methods provided by {@link IWiredNode}. + * + * @see IWiredNode#getNetwork() + */ +public interface IWiredNetwork +{ + /** + * Create a connection between two nodes. + * + * This should only be used on the server thread. + * + * @param left The first node to connect + * @param right The second node to connect + * @return {@code true} if a connection was created or {@code false} if the connection already exists. + * @throws IllegalStateException If neither node is on the network. + * @throws IllegalArgumentException If {@code left} and {@code right} are equal. + * @see IWiredNode#connectTo(IWiredNode) + * @see IWiredNetwork#connect(IWiredNode, IWiredNode) + */ + boolean connect( @Nonnull IWiredNode left, @Nonnull IWiredNode right ); + + /** + * Destroy a connection between this node and another. + * + * This should only be used on the server thread. + * + * @param left The first node in the connection. + * @param right The second node in the connection. + * @return {@code true} if a connection was destroyed or {@code false} if no connection exists. + * @throws IllegalArgumentException If either node is not on the network. + * @throws IllegalArgumentException If {@code left} and {@code right} are equal. + * @see IWiredNode#disconnectFrom(IWiredNode) + * @see IWiredNetwork#connect(IWiredNode, IWiredNode) + */ + boolean disconnect( @Nonnull IWiredNode left, @Nonnull IWiredNode right ); + + /** + * Sever all connections this node has, removing it from this network. + * + * This should only be used on the server thread. You should only call this on nodes + * that your network element owns. + * + * @param node The node to remove + * @return Whether this node was removed from the network. One cannot remove a node from a network where it is the + * only element. + * @throws IllegalArgumentException If the node is not in the network. + * @see IWiredNode#remove() + */ + boolean remove( @Nonnull IWiredNode node ); + + /** + * Update the peripherals a node provides. + * + * This should only be used on the server thread. You should only call this on nodes + * that your network element owns. + * + * @param node The node to attach peripherals for. + * @param peripherals The new peripherals for this node. + * @throws IllegalArgumentException If the node is not in the network. + * @see IWiredNode#updatePeripherals(Map) + */ + void updatePeripherals( @Nonnull IWiredNode node, @Nonnull Map peripherals ); +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredNetworkChange.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredNetworkChange.java new file mode 100644 index 000000000..df90f279f --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredNetworkChange.java @@ -0,0 +1,37 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.network.wired; + +import dan200.computercraft.api.peripheral.IPeripheral; + +import javax.annotation.Nonnull; +import java.util.Map; + +/** + * Represents a change to the objects on a wired network. + * + * @see IWiredElement#networkChanged(IWiredNetworkChange) + */ +public interface IWiredNetworkChange +{ + /** + * A set of peripherals which have been removed. Note that there may be entries with the same name + * in the added and removed set, but with a different peripheral. + * + * @return The set of removed peripherals. + */ + @Nonnull + Map peripheralsRemoved(); + + /** + * A set of peripherals which have been added. Note that there may be entries with the same name + * in the added and removed set, but with a different peripheral. + * + * @return The set of added peripherals. + */ + @Nonnull + Map peripheralsAdded(); +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredNode.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredNode.java new file mode 100644 index 000000000..061f950f7 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredNode.java @@ -0,0 +1,108 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.network.wired; + +import dan200.computercraft.api.network.IPacketNetwork; +import dan200.computercraft.api.peripheral.IPeripheral; + +import javax.annotation.Nonnull; +import java.util.Map; + +/** + * Wired nodes act as a layer between {@link IWiredElement}s and {@link IWiredNetwork}s. + * + * Firstly, a node acts as a packet network, capable of sending and receiving modem messages to connected nodes. These + * methods may be safely used on any thread. + * + * When sending a packet, the system will attempt to find the shortest path between the two nodes based on their + * element's position. Note that packet senders and receivers can have different locations from their associated + * element: the distance between the two will be added to the total packet's distance. + * + * Wired nodes also provide several convenience methods for interacting with a wired network. These should only ever + * be used on the main server thread. + */ +public interface IWiredNode extends IPacketNetwork +{ + /** + * The associated element for this network node. + * + * @return This node's element. + */ + @Nonnull + IWiredElement getElement(); + + /** + * The network this node is currently connected to. Note that this may change + * after any network operation, so it should not be cached. + * + * This should only be used on the server thread. + * + * @return This node's network. + */ + @Nonnull + IWiredNetwork getNetwork(); + + /** + * Create a connection from this node to another. + * + * This should only be used on the server thread. + * + * @param node The other node to connect to. + * @return {@code true} if a connection was created or {@code false} if the connection already exists. + * @see IWiredNetwork#connect(IWiredNode, IWiredNode) + * @see IWiredNode#disconnectFrom(IWiredNode) + */ + default boolean connectTo( @Nonnull IWiredNode node ) + { + return getNetwork().connect( this, node ); + } + + /** + * Destroy a connection between this node and another. + * + * This should only be used on the server thread. + * + * @param node The other node to disconnect from. + * @return {@code true} if a connection was destroyed or {@code false} if no connection exists. + * @throws IllegalArgumentException If {@code node} is not on the same network. + * @see IWiredNetwork#disconnect(IWiredNode, IWiredNode) + * @see IWiredNode#connectTo(IWiredNode) + */ + default boolean disconnectFrom( @Nonnull IWiredNode node ) + { + return getNetwork().disconnect( this, node ); + } + + /** + * Sever all connections this node has, removing it from this network. + * + * This should only be used on the server thread. You should only call this on nodes + * that your network element owns. + * + * @return Whether this node was removed from the network. One cannot remove a node from a network where it is the + * only element. + * @throws IllegalArgumentException If the node is not in the network. + * @see IWiredNetwork#remove(IWiredNode) + */ + default boolean remove() + { + return getNetwork().remove( this ); + } + + /** + * Mark this node's peripherals as having changed. + * + * This should only be used on the server thread. You should only call this on nodes + * that your network element owns. + * + * @param peripherals The new peripherals for this node. + * @see IWiredNetwork#updatePeripherals(IWiredNode, Map) + */ + default void updatePeripherals( @Nonnull Map peripherals ) + { + getNetwork().updatePeripherals( this, peripherals ); + } +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredSender.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredSender.java new file mode 100644 index 000000000..ee456f538 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredSender.java @@ -0,0 +1,30 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.network.wired; + +import dan200.computercraft.api.network.IPacketSender; + +import javax.annotation.Nonnull; + +/** + * An object on a {@link IWiredNetwork} capable of sending packets. + * + * Unlike a regular {@link IPacketSender}, this must be associated with the node you are attempting to + * to send the packet from. + */ +public interface IWiredSender extends IPacketSender +{ + /** + * The node in the network representing this object. + * + * This should be used as a proxy for the main network. One should send packets + * and register receivers through this object. + * + * @return The node for this element. + */ + @Nonnull + IWiredNode getNode(); +} diff --git a/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java b/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java new file mode 100644 index 000000000..fd46dd056 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java @@ -0,0 +1,208 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.ComputerCraftAPI; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.ILuaCallback; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.ILuaTask; +import dan200.computercraft.api.lua.MethodResult; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Map; + +/** + * The interface passed to peripherals by computers or turtles, providing methods + * that they can call. This should not be implemented by your classes. Do not interact + * with computers except via this interface. + */ +public interface IComputerAccess +{ + /** + * Mount a mount onto the computer's file system in a read only mode. + * + * @param desiredLocation The location on the computer's file system where you would like the mount to be mounted. + * @param mount The mount object to mount on the computer. + * @return The location on the computer's file system where you the mount mounted, or {@code null} if there was already a + * file in the desired location. Store this value if you wish to unmount the mount later. + * @throws NotAttachedException If the peripheral has been detached. + * @see ComputerCraftAPI#createSaveDirMount(World, String, long) + * @see ComputerCraftAPI#createResourceMount(String, String) + * @see #mount(String, IMount, String) + * @see #mountWritable(String, IWritableMount) + * @see #unmount(String) + * @see IMount + */ + @Nullable + default String mount( @Nonnull String desiredLocation, @Nonnull IMount mount ) + { + return mount( desiredLocation, mount, getAttachmentName() ); + } + + /** + * Mount a mount onto the computer's file system in a read only mode. + * + * @param desiredLocation The location on the computer's file system where you would like the mount to be mounted. + * @param mount The mount object to mount on the computer. + * @param driveName A custom name to give for this mount location, as returned by {@code fs.getDrive()}. + * @return The location on the computer's file system where you the mount mounted, or {@code null} if there was already a + * file in the desired location. Store this value if you wish to unmount the mount later. + * @throws NotAttachedException If the peripheral has been detached. + * @see ComputerCraftAPI#createSaveDirMount(World, String, long) + * @see ComputerCraftAPI#createResourceMount(String, String) + * @see #mount(String, IMount) + * @see #mountWritable(String, IWritableMount) + * @see #unmount(String) + * @see IMount + */ + @Nullable + String mount( @Nonnull String desiredLocation, @Nonnull IMount mount, @Nonnull String driveName ); + + /** + * Mount a mount onto the computer's file system in a writable mode. + * + * @param desiredLocation The location on the computer's file system where you would like the mount to be mounted. + * @param mount The mount object to mount on the computer. + * @return The location on the computer's file system where you the mount mounted, or null if there was already a + * file in the desired location. Store this value if you wish to unmount the mount later. + * @throws NotAttachedException If the peripheral has been detached. + * @see ComputerCraftAPI#createSaveDirMount(World, String, long) + * @see ComputerCraftAPI#createResourceMount(String, String) + * @see #mount(String, IMount) + * @see #unmount(String) + * @see IMount + */ + @Nullable + default String mountWritable( @Nonnull String desiredLocation, @Nonnull IWritableMount mount ) + { + return mountWritable( desiredLocation, mount, getAttachmentName() ); + } + + /** + * Mount a mount onto the computer's file system in a writable mode. + * + * @param desiredLocation The location on the computer's file system where you would like the mount to be mounted. + * @param mount The mount object to mount on the computer. + * @param driveName A custom name to give for this mount location, as returned by {@code fs.getDrive()}. + * @return The location on the computer's file system where you the mount mounted, or null if there was already a + * file in the desired location. Store this value if you wish to unmount the mount later. + * @throws NotAttachedException If the peripheral has been detached. + * @see ComputerCraftAPI#createSaveDirMount(World, String, long) + * @see ComputerCraftAPI#createResourceMount(String, String) + * @see #mount(String, IMount) + * @see #unmount(String) + * @see IMount + */ + String mountWritable( @Nonnull String desiredLocation, @Nonnull IWritableMount mount, @Nonnull String driveName ); + + /** + * Unmounts a directory previously mounted onto the computers file system by {@link #mount(String, IMount)} + * or {@link #mountWritable(String, IWritableMount)}. + * + * When a directory is unmounted, it will disappear from the computers file system, and the user will no longer be + * able to access it. All directories mounted by a mount or mountWritable are automatically unmounted when the + * peripheral is attached if they have not been explicitly unmounted. + * + * Note that you cannot unmount another peripheral's mounts. + * + * @param location The desired location in the computers file system of the directory to unmount. + * This must be the location of a directory previously mounted by {@link #mount(String, IMount)} or + * {@link #mountWritable(String, IWritableMount)}, as indicated by their return value. + * @throws NotAttachedException If the peripheral has been detached. + * @throws IllegalStateException If the mount does not exist, or was mounted by another peripheral. + * @see #mount(String, IMount) + * @see #mountWritable(String, IWritableMount) + */ + void unmount( @Nullable String location ); + + /** + * Returns the numerical ID of this computer. + * + * This is the same number obtained by calling {@code os.getComputerID()} or running the "id" program from lua, + * and is guaranteed unique. This number will be positive. + * + * @return The identifier. + */ + int getID(); + + /** + * Causes an event to be raised on this computer, which the computer can respond to by calling + * {@code os.pullEvent()}. This can be used to notify the computer when things happen in the world or to + * this peripheral. + * + * @param event A string identifying the type of event that has occurred, this will be + * returned as the first value from {@code os.pullEvent()}. It is recommended that you + * you choose a name that is unique, and recognisable as originating from your + * peripheral. eg: If your peripheral type is "button", a suitable event would be + * "button_pressed". + * @param arguments In addition to a name, you may pass an array of extra arguments to the event, that will + * be supplied as extra return values to os.pullEvent(). Objects in the array will be converted + * to lua data types in the same fashion as the return values of IPeripheral.callMethod(). + * + * You may supply {@code null} to indicate that no arguments are to be supplied. + * @throws NotAttachedException If the peripheral has been detached. + * @see MethodResult#pullEvent(String, ILuaCallback) + */ + void queueEvent( @Nonnull String event, @Nullable Object... arguments ); + + /** + * Get a string, unique to the computer, by which the computer refers to this peripheral. + * For directly attached peripherals this will be "left","right","front","back",etc, but + * for peripherals attached remotely it will be different. It is good practice to supply + * this string when raising events to the computer, so that the computer knows from + * which peripheral the event came. + * + * @return A string unique to the computer, but not globally. + * @throws NotAttachedException If the peripheral has been detached. + */ + @Nonnull + String getAttachmentName(); + + /** + * Get a set of peripherals that this computer access can "see", along with their attachment name. + * + * This may include other peripherals on the wired network or peripherals on other sides of the computer. + * + * @return All reachable peripherals + * @throws NotAttachedException If the peripheral has been detached. + * @see #getAttachmentName() + * @see #getAvailablePeripheral(String) + */ + @Nonnull + Map getAvailablePeripherals(); + + /** + * Get a reachable peripheral with the given attachment name. This is a equivalent to + * {@link #getAvailablePeripherals()}{@code .get(name)}, though may be more efficient. + * + * @param name The peripheral's attached name + * @return The reachable peripheral, or {@code null} if none can be found. + * @see #getAvailablePeripherals() + */ + @Nullable + IPeripheral getAvailablePeripheral( @Nonnull String name ); + + /** + * Get a {@link IWorkMonitor} for tasks your peripheral might execute on the main (server) thread. + * + * This should be used to ensure your peripheral integrates with ComputerCraft's monitoring and limiting of how much + * server time each computer consumes. You should not need to use this if you use + * {@link ILuaContext#issueMainThreadTask(ILuaTask)} - this is intended for mods with their own system for running + * work on the main thread. + * + * Please note that the returned implementation is not thread-safe, and should only be used from the main + * thread. + * + * @return The work monitor for the main thread, or {@code null} if this computer does not have one. + * @throws NotAttachedException If the peripheral has been detached. + */ + @Nonnull + IWorkMonitor getMainThreadMonitor(); +} diff --git a/src/main/java/dan200/computercraft/api/peripheral/IDynamicPeripheral.java b/src/main/java/dan200/computercraft/api/peripheral/IDynamicPeripheral.java new file mode 100644 index 000000000..5e7e8bfcc --- /dev/null +++ b/src/main/java/dan200/computercraft/api/peripheral/IDynamicPeripheral.java @@ -0,0 +1,53 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.*; + +import javax.annotation.Nonnull; + +/** + * A peripheral whose methods are not known at runtime. + * + * This behaves similarly to {@link IDynamicLuaObject}, though also accepting the current {@link IComputerAccess}. + * Generally one may use {@link LuaFunction} instead of implementing this interface. + */ +public interface IDynamicPeripheral extends IPeripheral +{ + /** + * Should return an array of strings that identify the methods that this peripheral exposes to Lua. This will be + * called once before each attachment, and should not change when called multiple times. + * + * @return An array of strings representing method names. + * @see #callMethod + */ + @Nonnull + String[] getMethodNames(); + + /** + * This is called when a lua program on an attached computer calls {@code peripheral.call()} with + * one of the methods exposed by {@link #getMethodNames()}. + * + * Be aware that this will be called from the ComputerCraft Lua thread, and must be thread-safe when interacting + * with Minecraft objects. + * + * @param computer The interface to the computer that is making the call. Remember that multiple + * computers can be attached to a peripheral at once. + * @param context The context of the currently running lua thread. This can be used to wait for events + * or otherwise yield. + * @param method An integer identifying which of the methods from getMethodNames() the computercraft + * wishes to call. The integer indicates the index into the getMethodNames() table + * that corresponds to the string passed into peripheral.call() + * @param arguments The arguments for this method. + * @return A {@link MethodResult} containing the values to return or the action to perform. + * @throws LuaException If you throw any exception from this function, a lua error will be raised with the + * same message as your exception. Use this to throw appropriate errors if the wrong + * arguments are supplied to your method. + * @see #getMethodNames() + */ + @Nonnull + MethodResult callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java b/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java new file mode 100644 index 000000000..88c8154a4 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java @@ -0,0 +1,99 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.LuaFunction; +import net.minecraftforge.common.capabilities.Capability; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The interface that defines a peripheral. + * + * In order to expose a peripheral for your block or tile entity, you may either attach a {@link Capability}, or + * register a {@link IPeripheralProvider}. This cannot be implemented {@link IPeripheral} directly on the tile. + * + * Peripherals should provide a series of methods to the user, either using {@link LuaFunction} or by implementing + * {@link IDynamicPeripheral}. + */ +public interface IPeripheral +{ + /** + * Should return a string that uniquely identifies this type of peripheral. + * This can be queried from lua by calling {@code peripheral.getType()} + * + * @return A string identifying the type of peripheral. + */ + @Nonnull + String getType(); + + /** + * Is called when when a computer is attaching to the peripheral. + * + * This will occur when a peripheral is placed next to an active computer, when a computer is turned on next to a + * peripheral, when a turtle travels into a square next to a peripheral, or when a wired modem adjacent to this + * peripheral is does any of the above. + * + * Between calls to attach and {@link #detach}, the attached computer can make method calls on the peripheral using + * {@code peripheral.call()}. This method can be used to keep track of which computers are attached to the + * peripheral, or to take action when attachment occurs. + * + * Be aware that will be called from both the server thread and ComputerCraft Lua thread, and so must be thread-safe + * and reentrant. + * + * @param computer The interface to the computer that is being attached. Remember that multiple computers can be + * attached to a peripheral at once. + * @see #detach + */ + default void attach( @Nonnull IComputerAccess computer ) + { + } + + /** + * Called when a computer is detaching from the peripheral. + * + * This will occur when a computer shuts down, when the peripheral is removed while attached to computers, when a + * turtle moves away from a block attached to a peripheral, or when a wired modem adjacent to this peripheral is + * detached. + * + * This method can be used to keep track of which computers are attached to the peripheral, or to take action when + * detachment occurs. + * + * Be aware that this will be called from both the server and ComputerCraft Lua thread, and must be thread-safe + * and reentrant. + * + * @param computer The interface to the computer that is being detached. Remember that multiple computers can be + * attached to a peripheral at once. + * @see #attach + */ + default void detach( @Nonnull IComputerAccess computer ) + { + } + + /** + * Get the object that this peripheral provides methods for. This will generally be the tile entity + * or block, but may be an inventory, entity, etc... + * + * @return The object this peripheral targets + */ + @Nullable + default Object getTarget() + { + return null; + } + + /** + * Determine whether this peripheral is equivalent to another one. + * + * The minimal example should at least check whether they are the same object. However, you may wish to check if + * they point to the same block or tile entity. + * + * @param other The peripheral to compare against. This may be {@code null}. + * @return Whether these peripherals are equivalent. + */ + boolean equals( @Nullable IPeripheral other ); +} diff --git a/src/main/java/dan200/computercraft/api/peripheral/IPeripheralProvider.java b/src/main/java/dan200/computercraft/api/peripheral/IPeripheralProvider.java new file mode 100644 index 000000000..ed768f1a3 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/peripheral/IPeripheralProvider.java @@ -0,0 +1,37 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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 net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.common.util.LazyOptional; + +import javax.annotation.Nonnull; + +/** + * This interface is used to create peripheral implementations for blocks. + * + * If you have a {@link TileEntity} which acts as a peripheral, you may alternatively expose the {@link IPeripheral} + * capability. + * + * @see dan200.computercraft.api.ComputerCraftAPI#registerPeripheralProvider(IPeripheralProvider) + */ +@FunctionalInterface +public interface IPeripheralProvider +{ + /** + * Produce an peripheral implementation from a block location. + * + * @param world The world the block is in. + * @param pos The position the block is at. + * @param side The side to get the peripheral from. + * @return A peripheral, or {@link LazyOptional#empty()} if there is not a peripheral here you'd like to handle. + * @see dan200.computercraft.api.ComputerCraftAPI#registerPeripheralProvider(IPeripheralProvider) + */ + @Nonnull + LazyOptional getPeripheral( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ); +} diff --git a/src/main/java/dan200/computercraft/api/peripheral/IWorkMonitor.java b/src/main/java/dan200/computercraft/api/peripheral/IWorkMonitor.java new file mode 100644 index 000000000..a68fd58e0 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/peripheral/IWorkMonitor.java @@ -0,0 +1,77 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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 javax.annotation.Nonnull; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * Monitors "work" associated with a computer, keeping track of how much a computer has done, and ensuring every + * computer receives a fair share of any processing time. + * + * This is primarily intended for work done by peripherals on the main thread (such as on a tile entity's tick), but + * could be used for other purposes (such as complex computations done on another thread). + * + * Before running a task, one should call {@link #canWork()} to determine if the computer is currently allowed to + * execute work. If that returns true, you should execute the task and use {@link #trackWork(long, TimeUnit)} to inform + * the monitor how long that task took. + * + * Alternatively, use {@link #runWork(Runnable)} to run and keep track of work. + * + * @see IComputerAccess#getMainThreadMonitor() + */ +public interface IWorkMonitor +{ + /** + * If the owning computer is currently allowed to execute work. + * + * @return If we can execute work right now. + */ + boolean canWork(); + + /** + * If the owning computer is currently allowed to execute work, and has ample time to do so. + * + * This is effectively a more restrictive form of {@link #canWork()}. One should use that in order to determine if + * you may do an initial piece of work, and shouldWork to determine if any additional task may be performed. + * + * @return If we should execute work right now. + */ + boolean shouldWork(); + + /** + * Inform the monitor how long some piece of work took to execute. + * + * @param time The time some task took to run + * @param unit The unit that {@code time} was measured in. + */ + void trackWork( long time, @Nonnull TimeUnit unit ); + + /** + * Run a task if possible, and inform the monitor of how long it took. + * + * @param runnable The task to run. + * @return If the task was actually run (namely, {@link #canWork()} returned {@code true}). + */ + default boolean runWork( @Nonnull Runnable runnable ) + { + Objects.requireNonNull( runnable, "runnable should not be null" ); + if( !canWork() ) return false; + + long start = System.nanoTime(); + try + { + runnable.run(); + } + finally + { + trackWork( System.nanoTime() - start, TimeUnit.NANOSECONDS ); + } + + return true; + } +} diff --git a/src/main/java/dan200/computercraft/api/peripheral/NotAttachedException.java b/src/main/java/dan200/computercraft/api/peripheral/NotAttachedException.java new file mode 100644 index 000000000..a01efa5a6 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/peripheral/NotAttachedException.java @@ -0,0 +1,25 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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; + +/** + * Thrown when performing operations on {@link IComputerAccess} when the current peripheral is no longer attached to + * the computer. + */ +public class NotAttachedException extends IllegalStateException +{ + private static final long serialVersionUID = 1221244785535553536L; + + public NotAttachedException() + { + super( "You are not attached to this computer" ); + } + + public NotAttachedException( String s ) + { + super( s ); + } +} diff --git a/src/main/java/dan200/computercraft/api/pocket/AbstractPocketUpgrade.java b/src/main/java/dan200/computercraft/api/pocket/AbstractPocketUpgrade.java new file mode 100644 index 000000000..ad2e1ee67 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/pocket/AbstractPocketUpgrade.java @@ -0,0 +1,117 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.pocket; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import net.minecraftforge.common.util.NonNullSupplier; + +import javax.annotation.Nonnull; +import java.util.function.Supplier; + +/** + * A base class for {@link IPocketUpgrade}s. + * + * One does not have to use this, but it does provide a convenient template. + */ +public abstract class AbstractPocketUpgrade implements IPocketUpgrade +{ + private final Identifier id; + private final String adjective; + private final NonNullSupplier stack; + + protected AbstractPocketUpgrade( Identifier id, String adjective, NonNullSupplier stack ) + { + this.id = id; + this.adjective = adjective; + this.stack = stack; + } + + protected AbstractPocketUpgrade( Identifier id, NonNullSupplier item ) + { + this( id, Util.createTranslationKey( "upgrade", id ) + ".adjective", item ); + } + + protected AbstractPocketUpgrade( Identifier id, String adjective, ItemStack stack ) + { + this( id, adjective, () -> stack ); + } + + protected AbstractPocketUpgrade( Identifier id, ItemStack stack ) + { + this( id, () -> stack ); + } + + protected AbstractPocketUpgrade( Identifier id, String adjective, ItemConvertible item ) + { + this( id, adjective, new CachedStack( () -> item ) ); + } + + protected AbstractPocketUpgrade( Identifier id, ItemConvertible item ) + { + this( id, new CachedStack( () -> item ) ); + } + + protected AbstractPocketUpgrade( Identifier id, String adjective, Supplier item ) + { + this( id, adjective, new CachedStack( item ) ); + } + + protected AbstractPocketUpgrade( Identifier id, Supplier item ) + { + this( id, new CachedStack( item ) ); + } + + @Nonnull + @Override + public final Identifier getUpgradeID() + { + return id; + } + + @Nonnull + @Override + public final String getUnlocalisedAdjective() + { + return adjective; + } + + @Nonnull + @Override + public final ItemStack getCraftingItem() + { + return stack.get(); + } + + /** + * Caches the construction of an item stack. + * + * @see dan200.computercraft.api.turtle.AbstractTurtleUpgrade For explanation of this class. + */ + private static final class CachedStack implements NonNullSupplier + { + private final Supplier provider; + private Item item; + private ItemStack stack; + + CachedStack( Supplier provider ) + { + this.provider = provider; + } + + @Nonnull + @Override + public ItemStack get() + { + Item item = provider.get().asItem(); + if( item == this.item && stack != null ) return stack; + return stack = new ItemStack( this.item = item ); + } + } +} diff --git a/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java b/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java new file mode 100644 index 000000000..6370e382b --- /dev/null +++ b/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java @@ -0,0 +1,97 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.pocket; + +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.entity.Entity; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.Identifier; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Map; + +/** + * Wrapper class for pocket computers. + */ +public interface IPocketAccess +{ + /** + * Gets the entity holding this item. + * + * This must be called on the server thread. + * + * @return The holding entity, or {@code null} if none exists. + */ + @Nullable + Entity getEntity(); + + /** + * Get the colour of this pocket computer as a RGB number. + * + * @return The colour this pocket computer is. This will be a RGB colour between {@code 0x000000} and + * {@code 0xFFFFFF} or -1 if it has no colour. + * @see #setColour(int) + */ + int getColour(); + + /** + * Set the colour of the pocket computer to a RGB number. + * + * @param colour The colour this pocket computer should be changed to. This should be a RGB colour between + * {@code 0x000000} and {@code 0xFFFFFF} or -1 to reset to the default colour. + * @see #getColour() + */ + void setColour( int colour ); + + /** + * Get the colour of this pocket computer's light as a RGB number. + * + * @return The colour this light is. This will be a RGB colour between {@code 0x000000} and {@code 0xFFFFFF} or + * -1 if it has no colour. + * @see #setLight(int) + */ + int getLight(); + + /** + * Set the colour of the pocket computer's light to a RGB number. + * + * @param colour The colour this modem's light will be changed to. This should be a RGB colour between + * {@code 0x000000} and {@code 0xFFFFFF} or -1 to reset to the default colour. + * @see #getLight() + */ + void setLight( int colour ); + + /** + * Get the upgrade-specific NBT. + * + * This is persisted between computer reboots and chunk loads. + * + * @return The upgrade's NBT. + * @see #updateUpgradeNBTData() + */ + @Nonnull + CompoundTag getUpgradeNBTData(); + + /** + * Mark the upgrade-specific NBT as dirty. + * + * @see #getUpgradeNBTData() + */ + void updateUpgradeNBTData(); + + /** + * Remove the current peripheral and create a new one. You may wish to do this if the methods available change. + */ + void invalidatePeripheral(); + + /** + * Get a list of all upgrades for the pocket computer. + * + * @return A collection of all upgrade names. + */ + @Nonnull + Map getUpgrades(); +} diff --git a/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java b/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java new file mode 100644 index 000000000..16b224a83 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java @@ -0,0 +1,104 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.pocket; + +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Additional peripherals for pocket computers. + * + * This is similar to {@link ITurtleUpgrade}. + */ +public interface IPocketUpgrade +{ + + /** + * Gets a unique identifier representing this type of turtle upgrade. eg: "computercraft:wireless_modem" or + * "my_mod:my_upgrade". + * + * You should use a unique resource domain to ensure this upgrade is uniquely identified. The upgrade will fail + * registration if an already used ID is specified. + * + * @return The upgrade's id. + * @see IPocketUpgrade#getUpgradeID() + * @see ComputerCraftAPI#registerPocketUpgrade(IPocketUpgrade) + */ + @Nonnull + Identifier getUpgradeID(); + + /** + * Return an unlocalised string to describe the type of pocket computer this upgrade provides. + * + * An example of a built-in adjectives is "Wireless" - this is converted to "Wireless Pocket Computer". + * + * @return The unlocalised adjective. + * @see ITurtleUpgrade#getUnlocalisedAdjective() + */ + @Nonnull + String getUnlocalisedAdjective(); + + /** + * Return an item stack representing the type of item that a pocket computer must be crafted with to create a + * pocket computer which holds this upgrade. This item stack is also used to determine the upgrade given by + * {@code pocket.equip()}/{@code pocket.unequip()}. + * + * Ideally this should be constant over a session. It is recommended that you cache + * the item too, in order to prevent constructing it every time the method is called. + * + * @return The item stack used for crafting. This can be {@link ItemStack#EMPTY} if crafting is disabled. + */ + @Nonnull + ItemStack getCraftingItem(); + + /** + * Creates a peripheral for the pocket computer. + * + * The peripheral created will be stored for the lifetime of the upgrade, will be passed an argument to + * {@link #update(IPocketAccess, IPeripheral)} and will be attached, detached and have methods called in the same + * manner as an ordinary peripheral. + * + * @param access The access object for the pocket item stack. + * @return The newly created peripheral. + * @see #update(IPocketAccess, IPeripheral) + */ + @Nullable + IPeripheral createPeripheral( @Nonnull IPocketAccess access ); + + /** + * Called when the pocket computer item stack updates. + * + * @param access The access object for the pocket item stack. + * @param peripheral The peripheral for this upgrade. + * @see #createPeripheral(IPocketAccess) + */ + default void update( @Nonnull IPocketAccess access, @Nullable IPeripheral peripheral ) + { + } + + /** + * Called when the pocket computer is right clicked. + * + * @param world The world the computer is in. + * @param access The access object for the pocket item stack. + * @param peripheral The peripheral for this upgrade. + * @return {@code true} to stop the GUI from opening, otherwise false. You should always provide some code path + * which returns {@code false}, such as requiring the player to be sneaking - otherwise they will be unable to + * access the GUI. + * @see #createPeripheral(IPocketAccess) + */ + default boolean onRightClick( @Nonnull World world, @Nonnull IPocketAccess access, @Nullable IPeripheral peripheral ) + { + return false; + } +} diff --git a/src/main/java/dan200/computercraft/api/redstone/IBundledRedstoneProvider.java b/src/main/java/dan200/computercraft/api/redstone/IBundledRedstoneProvider.java new file mode 100644 index 000000000..9e9f8df47 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/redstone/IBundledRedstoneProvider.java @@ -0,0 +1,33 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.redstone; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +/** + * This interface is used to provide bundled redstone output for blocks. + * + * @see dan200.computercraft.api.ComputerCraftAPI#registerBundledRedstoneProvider(IBundledRedstoneProvider) + */ +@FunctionalInterface +public interface IBundledRedstoneProvider +{ + /** + * Produce an bundled redstone output from a block location. + * + * @param world The world this block is in. + * @param pos The position this block is at. + * @param side The side to extract the bundled redstone output from. + * @return A number in the range 0-65535 to indicate this block is providing output, or -1 if you do not wish to + * handle this block. + * @see dan200.computercraft.api.ComputerCraftAPI#registerBundledRedstoneProvider(IBundledRedstoneProvider) + */ + int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ); +} diff --git a/src/main/java/dan200/computercraft/api/turtle/AbstractTurtleUpgrade.java b/src/main/java/dan200/computercraft/api/turtle/AbstractTurtleUpgrade.java new file mode 100644 index 000000000..e0dd3c142 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/AbstractTurtleUpgrade.java @@ -0,0 +1,126 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import net.minecraftforge.common.util.NonNullSupplier; + +import javax.annotation.Nonnull; +import java.util.function.Supplier; + +/** + * A base class for {@link ITurtleUpgrade}s. + * + * One does not have to use this, but it does provide a convenient template. + */ +public abstract class AbstractTurtleUpgrade implements ITurtleUpgrade +{ + private final Identifier id; + private final TurtleUpgradeType type; + private final String adjective; + private final NonNullSupplier stack; + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, String adjective, NonNullSupplier stack ) + { + this.id = id; + this.type = type; + this.adjective = adjective; + this.stack = stack; + } + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, NonNullSupplier stack ) + { + this( id, type, Util.createTranslationKey( "upgrade", id ) + ".adjective", stack ); + } + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, String adjective, ItemStack stack ) + { + this( id, type, adjective, () -> stack ); + } + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, ItemStack stack ) + { + this( id, type, () -> stack ); + } + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, String adjective, ItemConvertible item ) + { + this( id, type, adjective, new CachedStack( () -> item ) ); + } + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, ItemConvertible item ) + { + this( id, type, new CachedStack( () -> item ) ); + } + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, String adjective, Supplier item ) + { + this( id, type, adjective, new CachedStack( item ) ); + } + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, Supplier item ) + { + this( id, type, new CachedStack( item ) ); + } + + @Nonnull + @Override + public final Identifier getUpgradeID() + { + return id; + } + + @Nonnull + @Override + public final String getUnlocalisedAdjective() + { + return adjective; + } + + @Nonnull + @Override + public final TurtleUpgradeType getType() + { + return type; + } + + @Nonnull + @Override + public final ItemStack getCraftingItem() + { + return stack.get(); + } + + /** + * A supplier which converts an item into an item stack. + * + * Constructing item stacks is somewhat expensive due to attaching capabilities. We cache it if given a consistent item. + */ + private static final class CachedStack implements NonNullSupplier + { + private final Supplier provider; + private Item item; + private ItemStack stack; + + CachedStack( Supplier provider ) + { + this.provider = provider; + } + + @Nonnull + @Override + public ItemStack get() + { + Item item = provider.get().asItem(); + if( item == this.item && stack != null ) return stack; + return stack = new ItemStack( this.item = item ); + } + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java b/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java new file mode 100644 index 000000000..5129ab318 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java @@ -0,0 +1,300 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle; + +import com.mojang.authlib.GameProfile; +import dan200.computercraft.api.lua.ILuaCallback; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.inventory.Inventory; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.items.IItemHandlerModifiable; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The interface passed to turtle by turtles, providing methods that they can call. + * + * This should not be implemented by your classes. Do not interact with turtles except via this interface and + * {@link ITurtleUpgrade}. + */ +public interface ITurtleAccess +{ + /** + * Returns the world in which the turtle resides. + * + * @return the world in which the turtle resides. + */ + @Nonnull + World getWorld(); + + /** + * Returns a vector containing the integer co-ordinates at which the turtle resides. + * + * @return a vector containing the integer co-ordinates at which the turtle resides. + */ + @Nonnull + BlockPos getPosition(); + + /** + * Attempt to move this turtle to a new position. + * + * This will preserve the turtle's internal state, such as it's inventory, computer and upgrades. It should + * be used before playing a movement animation using {@link #playAnimation(TurtleAnimation)}. + * + * @param world The new world to move it to + * @param pos The new position to move it to. + * @return Whether the movement was successful. It may fail if the block was not loaded or the block placement + * was cancelled. + * @throws UnsupportedOperationException When attempting to teleport on the client side. + */ + boolean teleportTo( @Nonnull World world, @Nonnull BlockPos pos ); + + /** + * Returns a vector containing the floating point co-ordinates at which the turtle is rendered. + * This will shift when the turtle is moving. + * + * @param f The subframe fraction. + * @return A vector containing the floating point co-ordinates at which the turtle resides. + * @see #getVisualYaw(float) + */ + @Nonnull + Vec3d getVisualPosition( float f ); + + /** + * Returns the yaw the turtle is facing when it is rendered. + * + * @param f The subframe fraction. + * @return The yaw the turtle is facing. + * @see #getVisualPosition(float) + */ + float getVisualYaw( float f ); + + /** + * Returns the world direction the turtle is currently facing. + * + * @return The world direction the turtle is currently facing. + * @see #setDirection(Direction) + */ + @Nonnull + Direction getDirection(); + + /** + * Set the direction the turtle is facing. Note that this will not play a rotation animation, you will also need to + * call {@link #playAnimation(TurtleAnimation)} to do so. + * + * @param dir The new direction to set. This should be on either the x or z axis (so north, south, east or west). + * @see #getDirection() + */ + void setDirection( @Nonnull Direction dir ); + + /** + * Get the currently selected slot in the turtle's inventory. + * + * @return An integer representing the current slot. + * @see #getInventory() + * @see #setSelectedSlot(int) + */ + int getSelectedSlot(); + + /** + * Set the currently selected slot in the turtle's inventory. + * + * @param slot The slot to set. This must be greater or equal to 0 and less than the inventory size. Otherwise no + * action will be taken. + * @throws UnsupportedOperationException When attempting to change the slot on the client side. + * @see #getInventory() + * @see #getSelectedSlot() + */ + void setSelectedSlot( int slot ); + + /** + * Set the colour of the turtle to a RGB number. + * + * @param colour The colour this turtle should be changed to. This should be a RGB colour between {@code 0x000000} + * and {@code 0xFFFFFF} or -1 to reset to the default colour. + * @see #getColour() + */ + void setColour( int colour ); + + /** + * Get the colour of this turtle as a RGB number. + * + * @return The colour this turtle is. This will be a RGB colour between {@code 0x000000} and {@code 0xFFFFFF} or + * -1 if it has no colour. + * @see #setColour(int) + */ + int getColour(); + + /** + * Get the player who owns this turtle, namely whoever placed it. + * + * @return This turtle's owner. + */ + @Nonnull + GameProfile getOwningPlayer(); + + /** + * Get the inventory of this turtle. + * + * Note: this inventory should only be accessed and modified on the server thread. + * + * @return This turtle's inventory + * @see #getItemHandler() + */ + @Nonnull + Inventory getInventory(); + + /** + * Get the inventory of this turtle as an {@link IItemHandlerModifiable}. + * + * Note: this inventory should only be accessed and modified on the server thread. + * + * @return This turtle's inventory + * @see #getInventory() + * @see IItemHandlerModifiable + * @see net.minecraftforge.items.CapabilityItemHandler#ITEM_HANDLER_CAPABILITY + */ + @Nonnull + IItemHandlerModifiable getItemHandler(); + + /** + * Determine whether this turtle will require fuel when performing actions. + * + * @return Whether this turtle needs fuel. + * @see #getFuelLevel() + * @see #setFuelLevel(int) + */ + boolean isFuelNeeded(); + + /** + * Get the current fuel level of this turtle. + * + * @return The turtle's current fuel level. + * @see #isFuelNeeded() + * @see #setFuelLevel(int) + */ + int getFuelLevel(); + + /** + * Set the fuel level to a new value. It is generally preferred to use {@link #consumeFuel(int)}} or {@link #addFuel(int)} + * instead. + * + * @param fuel The new amount of fuel. This must be between 0 and the fuel limit. + * @see #getFuelLevel() + * @see #getFuelLimit() + * @see #addFuel(int) + * @see #consumeFuel(int) + */ + void setFuelLevel( int fuel ); + + /** + * Get the maximum amount of fuel a turtle can hold. + * + * @return The turtle's fuel limit. + */ + int getFuelLimit(); + + /** + * Removes some fuel from the turtles fuel supply. Negative numbers can be passed in to INCREASE the fuel level of the turtle. + * + * @param fuel The amount of fuel to consume. + * @return Whether the turtle was able to consume the amount of fuel specified. Will return false if you supply a number + * greater than the current fuel level of the turtle. No fuel will be consumed if {@code false} is returned. + * @throws UnsupportedOperationException When attempting to consume fuel on the client side. + */ + boolean consumeFuel( int fuel ); + + /** + * Increase the turtle's fuel level by the given amount. + * + * @param fuel The amount to refuel with. + * @throws UnsupportedOperationException When attempting to refuel on the client side. + */ + void addFuel( int fuel ); + + /** + * Adds a custom command to the turtles command queue. Unlike peripheral methods, these custom commands will be executed + * on the main thread, so are guaranteed to be able to access Minecraft objects safely, and will be queued up + * with the turtles standard movement and tool commands. An issued command will return an unique integer, which will + * be supplied as a parameter to a "turtle_response" event issued to the turtle after the command has completed. Look at the + * lua source code for "rom/apis/turtle" for how to build a lua wrapper around this functionality. + * + * @param command An object which will execute the custom command when its point in the queue is reached + * @return The objects the command returned when executed. you should probably return these to the player + * unchanged if called from a peripheral method. + * @throws UnsupportedOperationException When attempting to execute a command on the client side. + * @see ITurtleCommand + * @see MethodResult#pullEvent(String, ILuaCallback) + */ + @Nonnull + MethodResult executeCommand( @Nonnull ITurtleCommand command ); + + /** + * Start playing a specific animation. This will prevent other turtle commands from executing until + * it is finished. + * + * @param animation The animation to play. + * @throws UnsupportedOperationException When attempting to execute play an animation on the client side. + * @see TurtleAnimation + */ + void playAnimation( @Nonnull TurtleAnimation animation ); + + /** + * Returns the turtle on the specified side of the turtle, if there is one. + * + * @param side The side to get the upgrade from. + * @return The upgrade on the specified side of the turtle, if there is one. + * @see #setUpgrade(TurtleSide, ITurtleUpgrade) + */ + @Nullable + ITurtleUpgrade getUpgrade( @Nonnull TurtleSide side ); + + /** + * Set the upgrade for a given side, resetting peripherals and clearing upgrade specific data. + * + * @param side The side to set the upgrade on. + * @param upgrade The upgrade to set, may be {@code null} to clear. + * @see #getUpgrade(TurtleSide) + */ + void setUpgrade( @Nonnull TurtleSide side, @Nullable ITurtleUpgrade upgrade ); + + /** + * Returns the peripheral created by the upgrade on the specified side of the turtle, if there is one. + * + * @param side The side to get the peripheral from. + * @return The peripheral created by the upgrade on the specified side of the turtle, {@code null} if none exists. + */ + @Nullable + IPeripheral getPeripheral( @Nonnull TurtleSide side ); + + /** + * Get an upgrade-specific NBT compound, which can be used to store arbitrary data. + * + * This will be persisted across turtle restarts and chunk loads, as well as being synced to the client. You must + * call {@link #updateUpgradeNBTData(TurtleSide)} after modifying it. + * + * @param side The side to get the upgrade data for. + * @return The upgrade-specific data. + * @see #updateUpgradeNBTData(TurtleSide) + */ + @Nonnull + CompoundTag getUpgradeNBTData( @Nullable TurtleSide side ); + + /** + * Mark the upgrade-specific data as dirty on a specific side. This is required for the data to be synced to the + * client and persisted. + * + * @param side The side to mark dirty. + * @see #updateUpgradeNBTData(TurtleSide) + */ + void updateUpgradeNBTData( @Nonnull TurtleSide side ); +} diff --git a/src/main/java/dan200/computercraft/api/turtle/ITurtleCommand.java b/src/main/java/dan200/computercraft/api/turtle/ITurtleCommand.java new file mode 100644 index 000000000..00c17ab54 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/ITurtleCommand.java @@ -0,0 +1,33 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle; + +import javax.annotation.Nonnull; + +/** + * An interface for objects executing custom turtle commands, used with {@link ITurtleAccess#executeCommand(ITurtleCommand)}. + * + * @see ITurtleAccess#executeCommand(ITurtleCommand) + */ +@FunctionalInterface +public interface ITurtleCommand +{ + /** + * Will be called by the turtle on the main thread when it is time to execute the custom command. + * + * The handler should either perform the work of the command, and return success, or return + * failure with an error message to indicate the command cannot be executed at this time. + * + * @param turtle Access to the turtle for whom the command was issued. + * @return A result, indicating whether this action succeeded or not. + * @see ITurtleAccess#executeCommand(ITurtleCommand) + * @see TurtleCommandResult#success() + * @see TurtleCommandResult#failure(String) + * @see TurtleCommandResult + */ + @Nonnull + TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ); +} diff --git a/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java b/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java new file mode 100644 index 000000000..a11ac4a87 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java @@ -0,0 +1,143 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle; + +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.client.TransformedModel; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.turtle.event.TurtleAttackEvent; +import dan200.computercraft.api.turtle.event.TurtleBlockEvent; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Direction; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.event.entity.player.AttackEntityEvent; +import net.minecraftforge.event.world.BlockEvent; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The primary interface for defining an update for Turtles. A turtle update + * can either be a new tool, or a new peripheral. + * + * @see ComputerCraftAPI#registerTurtleUpgrade(ITurtleUpgrade) + */ +public interface ITurtleUpgrade +{ + /** + * Gets a unique identifier representing this type of turtle upgrade. eg: "computercraft:wireless_modem" or "my_mod:my_upgrade". + * You should use a unique resource domain to ensure this upgrade is uniquely identified. + * The turtle will fail registration if an already used ID is specified. + * + * @return The unique ID for this upgrade. + * @see ComputerCraftAPI#registerTurtleUpgrade(ITurtleUpgrade) + */ + @Nonnull + Identifier getUpgradeID(); + + /** + * Return an unlocalised string to describe this type of turtle in turtle item names. + * + * Examples of built-in adjectives are "Wireless", "Mining" and "Crafty". + * + * @return The localisation key for this upgrade's adjective. + */ + @Nonnull + String getUnlocalisedAdjective(); + + /** + * Return whether this turtle adds a tool or a peripheral to the turtle. + * + * @return The type of upgrade this is. + * @see TurtleUpgradeType for the differences between them. + */ + @Nonnull + TurtleUpgradeType getType(); + + /** + * Return an item stack representing the type of item that a turtle must be crafted + * with to create a turtle which holds this upgrade. This item stack is also used + * to determine the upgrade given by {@code turtle.equip()} + * + * Ideally this should be constant over a session. It is recommended that you cache + * the item too, in order to prevent constructing it every time the method is called. + * + * @return The item stack to craft with, or {@link ItemStack#EMPTY} if it cannot be crafted. + */ + @Nonnull + ItemStack getCraftingItem(); + + /** + * Will only be called for peripheral upgrades. Creates a peripheral for a turtle being placed using this upgrade. + * + * The peripheral created will be stored for the lifetime of the upgrade and will be passed as an argument to + * {@link #update(ITurtleAccess, TurtleSide)}. It will be attached, detached and have methods called in the same + * manner as a Computer peripheral. + * + * @param turtle Access to the turtle that the peripheral is being created for. + * @param side Which side of the turtle (left or right) that the upgrade resides on. + * @return The newly created peripheral. You may return {@code null} if this upgrade is a Tool + * and this method is not expected to be called. + */ + @Nullable + default IPeripheral createPeripheral( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + return null; + } + + /** + * Will only be called for Tool turtle. Called when turtle.dig() or turtle.attack() is called + * by the turtle, and the tool is required to do some work. + * + * Conforming implementations should fire {@link BlockEvent.BreakEvent} and {@link TurtleBlockEvent.Dig} for + * digging, {@link AttackEntityEvent} and {@link TurtleAttackEvent} for attacking. + * + * @param turtle Access to the turtle that the tool resides on. + * @param side Which side of the turtle (left or right) the tool resides on. + * @param verb Which action (dig or attack) the turtle is being called on to perform. + * @param direction Which world direction the action should be performed in, relative to the turtles + * position. This will either be up, down, or the direction the turtle is facing, depending on + * whether dig, digUp or digDown was called. + * @return Whether the turtle was able to perform the action, and hence whether the {@code turtle.dig()} + * or {@code turtle.attack()} lua method should return true. If true is returned, the tool will perform + * a swinging animation. You may return {@code null} if this turtle is a Peripheral and this method is not expected + * to be called. + */ + @Nonnull + default TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull Direction direction ) + { + return TurtleCommandResult.failure(); + } + + /** + * Called to obtain the model to be used when rendering a turtle peripheral. + * + * This can be obtained from {@link net.minecraft.client.renderer.ItemModelMesher#getItemModel(ItemStack)}, + * {@link net.minecraft.client.renderer.model.ModelManager#getModel(ModelResourceLocation)} or any other + * source. + * + * @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models! + * @param side Which side of the turtle (left or right) the upgrade resides on. + * @return The model that you wish to be used to render your upgrade. + */ + @Nonnull + @Environment(EnvType.CLIENT) + TransformedModel getModel( @Nullable ITurtleAccess turtle, @Nonnull TurtleSide side ); + + /** + * Called once per tick for each turtle which has the upgrade equipped. + * + * @param turtle Access to the turtle that the upgrade resides on. + * @param side Which side of the turtle (left or right) the upgrade resides on. + */ + default void update( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/TurtleAnimation.java b/src/main/java/dan200/computercraft/api/turtle/TurtleAnimation.java new file mode 100644 index 000000000..1e988b91c --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/TurtleAnimation.java @@ -0,0 +1,86 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle; + +/** + * An animation a turtle will play between executing commands. + * + * Each animation takes 8 ticks to complete unless otherwise specified. + * + * @see ITurtleAccess#playAnimation(TurtleAnimation) + */ +public enum TurtleAnimation +{ + /** + * An animation which does nothing. This takes no time to complete. + * + * @see #WAIT + * @see #SHORT_WAIT + */ + NONE, + + /** + * Make the turtle move forward. Note that the animation starts from the block behind it, and + * moves into this one. + */ + MOVE_FORWARD, + + /** + * Make the turtle move backwards. Note that the animation starts from the block in front it, and + * moves into this one. + */ + MOVE_BACK, + + /** + * Make the turtle move backwards. Note that the animation starts from the block above it, and + * moves into this one. + */ + MOVE_UP, + + /** + * Make the turtle move backwards. Note that the animation starts from the block below it, and + * moves into this one. + */ + MOVE_DOWN, + + /** + * Turn the turtle to the left. Note that the animation starts with the turtle facing right, and + * the turtle turns to face in the current direction. + */ + TURN_LEFT, + + /** + * Turn the turtle to the left. Note that the animation starts with the turtle facing right, and + * the turtle turns to face in the current direction. + */ + TURN_RIGHT, + + /** + * Swing the tool on the left. + */ + SWING_LEFT_TOOL, + + /** + * Swing the tool on the right. + */ + SWING_RIGHT_TOOL, + + /** + * Wait until the animation has finished, performing no movement. + * + * @see #SHORT_WAIT + * @see #NONE + */ + WAIT, + + /** + * Wait until the animation has finished, performing no movement. This takes 4 ticks to complete. + * + * @see #WAIT + * @see #NONE + */ + SHORT_WAIT, +} diff --git a/src/main/java/dan200/computercraft/api/turtle/TurtleCommandResult.java b/src/main/java/dan200/computercraft/api/turtle/TurtleCommandResult.java new file mode 100644 index 000000000..b8bfd0c40 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/TurtleCommandResult.java @@ -0,0 +1,112 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Used to indicate the result of executing a turtle command. + * + * @see ITurtleCommand#execute(ITurtleAccess) + * @see ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, Direction) + */ +public final class TurtleCommandResult +{ + private static final TurtleCommandResult EMPTY_SUCCESS = new TurtleCommandResult( true, null, null ); + private static final TurtleCommandResult EMPTY_FAILURE = new TurtleCommandResult( false, null, null ); + + /** + * Create a successful command result with no result. + * + * @return A successful command result with no values. + */ + @Nonnull + public static TurtleCommandResult success() + { + return EMPTY_SUCCESS; + } + + /** + * Create a successful command result with the given result values. + * + * @param results The results of executing this command. + * @return A successful command result with the given values. + */ + @Nonnull + public static TurtleCommandResult success( @Nullable Object[] results ) + { + if( results == null || results.length == 0 ) return EMPTY_SUCCESS; + return new TurtleCommandResult( true, null, results ); + } + + /** + * Create a failed command result with no error message. + * + * @return A failed command result with no message. + */ + @Nonnull + public static TurtleCommandResult failure() + { + return EMPTY_FAILURE; + } + + /** + * Create a failed command result with an error message. + * + * @param errorMessage The error message to provide. + * @return A failed command result with a message. + */ + @Nonnull + public static TurtleCommandResult failure( @Nullable String errorMessage ) + { + if( errorMessage == null ) return EMPTY_FAILURE; + return new TurtleCommandResult( false, errorMessage, null ); + } + + private final boolean success; + private final String errorMessage; + private final Object[] results; + + private TurtleCommandResult( boolean success, String errorMessage, Object[] results ) + { + this.success = success; + this.errorMessage = errorMessage; + this.results = results; + } + + /** + * Determine whether the command executed successfully. + * + * @return If the command was successful. + */ + public boolean isSuccess() + { + return success; + } + + /** + * Get the error message of this command result. + * + * @return The command's error message, or {@code null} if it was a success. + */ + @Nullable + public String getErrorMessage() + { + return errorMessage; + } + + /** + * Get the resulting values of this command result. + * + * @return The command's result, or {@code null} if it was a failure. + */ + @Nullable + public Object[] getResults() + { + return results; + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/TurtleSide.java b/src/main/java/dan200/computercraft/api/turtle/TurtleSide.java new file mode 100644 index 000000000..a32f149f4 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/TurtleSide.java @@ -0,0 +1,22 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle; + +/** + * An enum representing the two sides of the turtle that a turtle turtle might reside. + */ +public enum TurtleSide +{ + /** + * The turtle's left side (where the pickaxe usually is on a Wireless Mining Turtle). + */ + LEFT, + + /** + * The turtle's right side (where the modem usually is on a Wireless Mining Turtle). + */ + RIGHT, +} diff --git a/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeType.java b/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeType.java new file mode 100644 index 000000000..8991551aa --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeType.java @@ -0,0 +1,43 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle; + +/** + * An enum representing the different types of turtle that an {@link ITurtleUpgrade} implementation can add to a turtle. + * + * @see ITurtleUpgrade#getType() + */ +public enum TurtleUpgradeType +{ + /** + * A tool is rendered as an item on the side of the turtle, and responds to the {@code turtle.dig()} + * and {@code turtle.attack()} methods (Such as pickaxe or sword on Mining and Melee turtles). + */ + TOOL, + + /** + * A peripheral adds a special peripheral which is attached to the side of the turtle, + * and can be interacted with the {@code peripheral} API (Such as the modem on Wireless Turtles). + */ + PERIPHERAL, + + /** + * An upgrade which provides both a tool and a peripheral. This can be used when you wish + * your upgrade to also provide methods. For example, a pickaxe could provide methods + * determining whether it can break the given block or not. + */ + BOTH; + + public boolean isTool() + { + return this == TOOL || this == BOTH; + } + + public boolean isPeripheral() + { + return this == PERIPHERAL || this == BOTH; + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/TurtleVerb.java b/src/main/java/dan200/computercraft/api/turtle/TurtleVerb.java new file mode 100644 index 000000000..27b0578f7 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/TurtleVerb.java @@ -0,0 +1,26 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle; + +/** + * An enum representing the different actions that an {@link ITurtleUpgrade} of type Tool may be called on to perform by + * a turtle. + * + * @see ITurtleUpgrade#getType() + * @see ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, Direction) + */ +public enum TurtleVerb +{ + /** + * The turtle called {@code turtle.dig()}, {@code turtle.digUp()} or {@code turtle.digDown()}. + */ + DIG, + + /** + * The turtle called {@code turtle.attack()}, {@code turtle.attackUp()} or {@code turtle.attackDown()}. + */ + ATTACK, +} diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleAction.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleAction.java new file mode 100644 index 000000000..f5e347d5a --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleAction.java @@ -0,0 +1,83 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle.event; + +/** + * A basic action that a turtle may perform, as accessed by the {@code turtle} API. + * + * @see TurtleActionEvent + */ +public enum TurtleAction +{ + /** + * A turtle moves to a new position. + * + * @see TurtleBlockEvent.Move + */ + MOVE, + + /** + * A turtle turns in a specific direction. + */ + TURN, + + /** + * A turtle attempts to dig a block. + * + * @see TurtleBlockEvent.Dig + */ + DIG, + + /** + * A turtle attempts to place a block or item in the world. + * + * @see TurtleBlockEvent.Place + */ + PLACE, + + /** + * A turtle attempts to attack an entity. + * + * @see TurtleActionEvent + */ + ATTACK, + + /** + * Drop an item into an inventory/the world. + * + * @see TurtleInventoryEvent.Drop + */ + DROP, + + /** + * Suck an item from an inventory or the world. + * + * @see TurtleInventoryEvent.Suck + */ + SUCK, + + /** + * Refuel the turtle's fuel levels. + */ + REFUEL, + + /** + * Equip or unequip an item. + */ + EQUIP, + + /** + * Inspect a block in world. + * + * @see TurtleBlockEvent.Inspect + */ + INSPECT, + + /** + * Gather metdata about an item in the turtle's inventory. + */ + INSPECT_ITEM, +} diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleActionEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleActionEvent.java new file mode 100644 index 000000000..2f95cbb87 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleActionEvent.java @@ -0,0 +1,81 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle.event; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import net.minecraftforge.eventbus.api.Cancelable; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * An event fired when a turtle is performing a known action. + */ +@Cancelable +public class TurtleActionEvent extends TurtleEvent +{ + private final TurtleAction action; + private String failureMessage; + + public TurtleActionEvent( @Nonnull ITurtleAccess turtle, @Nonnull TurtleAction action ) + { + super( turtle ); + + Objects.requireNonNull( action, "action cannot be null" ); + this.action = action; + } + + public TurtleAction getAction() + { + return action; + } + + /** + * Sets the cancellation state of this action. + * + * If {@code cancel} is {@code true}, this action will not be carried out. + * + * @param cancel The new canceled value. + * @see TurtleCommandResult#failure() + * @deprecated Use {@link #setCanceled(boolean, String)} instead. + */ + @Override + @Deprecated + public void setCanceled( boolean cancel ) + { + setCanceled( cancel, null ); + } + + /** + * Set the cancellation state of this action, setting a failure message if required. + * + * If {@code cancel} is {@code true}, this action will not be carried out. + * + * @param cancel The new canceled value. + * @param failureMessage The message to return to the user explaining the failure. + * @see TurtleCommandResult#failure(String) + */ + public void setCanceled( boolean cancel, @Nullable String failureMessage ) + { + super.setCanceled( cancel ); + this.failureMessage = cancel ? failureMessage : null; + } + + /** + * Get the message with which this will fail. + * + * @return The failure message. + * @see TurtleCommandResult#failure() + * @see #setCanceled(boolean, String) + */ + @Nullable + public String getFailureMessage() + { + return failureMessage; + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleAttackEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleAttackEvent.java new file mode 100644 index 000000000..a7a79fcbe --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleAttackEvent.java @@ -0,0 +1,78 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle.event; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.turtle.TurtleVerb; +import net.minecraft.entity.Entity; +import net.minecraftforge.common.util.FakePlayer; +import net.minecraftforge.event.entity.player.AttackEntityEvent; + +import javax.annotation.Nonnull; +import java.util.Objects; + +/** + * Fired when a turtle attempts to attack an entity. + * + * This must be fired by {@link ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, Direction)}, + * as the base {@code turtle.attack()} command does not fire it. + * + * Note that such commands should also fire {@link AttackEntityEvent}, so you do not need to listen to both. + * + * @see TurtleAction#ATTACK + */ +public class TurtleAttackEvent extends TurtlePlayerEvent +{ + private final Entity target; + private final ITurtleUpgrade upgrade; + private final TurtleSide side; + + public TurtleAttackEvent( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull Entity target, @Nonnull ITurtleUpgrade upgrade, @Nonnull TurtleSide side ) + { + super( turtle, TurtleAction.ATTACK, player ); + Objects.requireNonNull( target, "target cannot be null" ); + Objects.requireNonNull( upgrade, "upgrade cannot be null" ); + Objects.requireNonNull( side, "side cannot be null" ); + this.target = target; + this.upgrade = upgrade; + this.side = side; + } + + /** + * Get the entity being attacked by this turtle. + * + * @return The entity being attacked. + */ + @Nonnull + public Entity getTarget() + { + return target; + } + + /** + * Get the upgrade responsible for attacking. + * + * @return The upgrade responsible for attacking. + */ + @Nonnull + public ITurtleUpgrade getUpgrade() + { + return upgrade; + } + + /** + * Get the side the attacking upgrade is on. + * + * @return The upgrade's side. + */ + @Nonnull + public TurtleSide getSide() + { + return side; + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java new file mode 100644 index 000000000..1fd67187d --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java @@ -0,0 +1,232 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle.event; + +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.turtle.TurtleVerb; +import net.minecraft.block.BlockState; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.common.util.FakePlayer; +import net.minecraftforge.event.world.BlockEvent; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.Objects; + +/** + * A general event for when a turtle interacts with a block or region. + * + * You should generally listen to one of the sub-events instead, cancelling them where + * appropriate. + * + * Note that you are not guaranteed to receive this event, if it has been cancelled by other + * mechanisms, such as block protection systems. + * + * Be aware that some events (such as {@link TurtleInventoryEvent}) do not necessarily interact + * with a block, simply objects within that block space. + */ +public abstract class TurtleBlockEvent extends TurtlePlayerEvent +{ + private final World world; + private final BlockPos pos; + + protected TurtleBlockEvent( @Nonnull ITurtleAccess turtle, @Nonnull TurtleAction action, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos ) + { + super( turtle, action, player ); + + Objects.requireNonNull( world, "world cannot be null" ); + Objects.requireNonNull( pos, "pos cannot be null" ); + this.world = world; + this.pos = pos; + } + + /** + * Get the world the turtle is interacting in. + * + * @return The world the turtle is interacting in. + */ + public World getWorld() + { + return world; + } + + /** + * Get the position the turtle is interacting with. Note that this is different + * to {@link ITurtleAccess#getPosition()}. + * + * @return The position the turtle is interacting with. + */ + public BlockPos getPos() + { + return pos; + } + + /** + * Fired when a turtle attempts to dig a block. + * + * This must be fired by {@link ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, Direction)}, + * as the base {@code turtle.dig()} command does not fire it. + * + * Note that such commands should also fire {@link BlockEvent.BreakEvent}, so you do not need to listen to both. + * + * @see TurtleAction#DIG + */ + public static class Dig extends TurtleBlockEvent + { + private final BlockState block; + private final ITurtleUpgrade upgrade; + private final TurtleSide side; + + public Dig( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState block, @Nonnull ITurtleUpgrade upgrade, @Nonnull TurtleSide side ) + { + super( turtle, TurtleAction.DIG, player, world, pos ); + + Objects.requireNonNull( block, "block cannot be null" ); + Objects.requireNonNull( upgrade, "upgrade cannot be null" ); + Objects.requireNonNull( side, "side cannot be null" ); + this.block = block; + this.upgrade = upgrade; + this.side = side; + } + + /** + * Get the block which is about to be broken. + * + * @return The block which is going to be broken. + */ + @Nonnull + public BlockState getBlock() + { + return block; + } + + /** + * Get the upgrade doing the digging. + * + * @return The upgrade doing the digging. + */ + @Nonnull + public ITurtleUpgrade getUpgrade() + { + return upgrade; + } + + /** + * Get the side the upgrade doing the digging is on. + * + * @return The upgrade's side. + */ + @Nonnull + public TurtleSide getSide() + { + return side; + } + } + + /** + * Fired when a turtle attempts to move into a block. + * + * @see TurtleAction#MOVE + */ + public static class Move extends TurtleBlockEvent + { + public Move( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos ) + { + super( turtle, TurtleAction.MOVE, player, world, pos ); + } + } + + /** + * Fired when a turtle attempts to place a block in the world. + * + * @see TurtleAction#PLACE + */ + public static class Place extends TurtleBlockEvent + { + private final ItemStack stack; + + public Place( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nonnull ItemStack stack ) + { + super( turtle, TurtleAction.PLACE, player, world, pos ); + + Objects.requireNonNull( stack, "stack cannot be null" ); + this.stack = stack; + } + + /** + * Get the item stack that will be placed. This should not be modified. + * + * @return The item stack to be placed. + */ + @Nonnull + public ItemStack getStack() + { + return stack; + } + } + + /** + * Fired when a turtle gathers data on a block in world. + * + * You may prevent blocks being inspected, or add additional information to the result. + * + * @see TurtleAction#INSPECT + */ + public static class Inspect extends TurtleBlockEvent + { + private final BlockState state; + private final Map data; + + public Inspect( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState state, @Nonnull Map data ) + { + super( turtle, TurtleAction.INSPECT, player, world, pos ); + + Objects.requireNonNull( state, "state cannot be null" ); + Objects.requireNonNull( data, "data cannot be null" ); + this.data = data; + this.state = state; + } + + /** + * Get the block state which is being inspected. + * + * @return The inspected block state. + */ + @Nonnull + public BlockState getState() + { + return state; + } + + /** + * Get the "inspection data" from this block, which will be returned to the user. + * + * @return This block's inspection data. + */ + @Nonnull + public Map getData() + { + return data; + } + + /** + * Add new information to the inspection result. Note this will override fields with the same name. + * + * @param newData The data to add. Note all values should be convertible to Lua (see + * {@link MethodResult#of(Object)}). + */ + public void addData( @Nonnull Map newData ) + { + Objects.requireNonNull( newData, "newData cannot be null" ); + data.putAll( newData ); + } + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleEvent.java new file mode 100644 index 000000000..1a980db85 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleEvent.java @@ -0,0 +1,42 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle.event; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import net.minecraftforge.eventbus.api.Event; + +import javax.annotation.Nonnull; +import java.util.Objects; + +/** + * A base class for all events concerning a turtle. This will only ever constructed and fired on the server side, + * so sever specific methods on {@link ITurtleAccess} are safe to use. + * + * You should generally not need to subscribe to this event, preferring one of the more specific classes. + * + * @see TurtleActionEvent + */ +public abstract class TurtleEvent extends Event +{ + private final ITurtleAccess turtle; + + protected TurtleEvent( @Nonnull ITurtleAccess turtle ) + { + Objects.requireNonNull( turtle, "turtle cannot be null" ); + this.turtle = turtle; + } + + /** + * Get the turtle which is performing this action. + * + * @return The access for this turtle. + */ + @Nonnull + public ITurtleAccess getTurtle() + { + return turtle; + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleInspectItemEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleInspectItemEvent.java new file mode 100644 index 000000000..e4eb517e3 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleInspectItemEvent.java @@ -0,0 +1,91 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle.event; + +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.turtle.ITurtleAccess; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.Objects; + +/** + * Fired when a turtle gathers data on an item in its inventory. + * + * You may prevent items being inspected, or add additional information to the result. Be aware that this may be fired + * on the computer thread, and so any operations on it must be thread safe. + * + * @see TurtleAction#INSPECT_ITEM + */ +public class TurtleInspectItemEvent extends TurtleActionEvent +{ + private final ItemStack stack; + private final Map data; + private final boolean mainThread; + + @Deprecated + public TurtleInspectItemEvent( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack stack, @Nonnull Map data ) + { + this( turtle, stack, data, false ); + } + + public TurtleInspectItemEvent( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack stack, @Nonnull Map data, boolean mainThread ) + { + super( turtle, TurtleAction.INSPECT_ITEM ); + + Objects.requireNonNull( stack, "stack cannot be null" ); + Objects.requireNonNull( data, "data cannot be null" ); + this.stack = stack; + this.data = data; + this.mainThread = mainThread; + } + + /** + * The item which is currently being inspected. + * + * @return The item stack which is being inspected. This should not be modified. + */ + @Nonnull + public ItemStack getStack() + { + return stack; + } + + /** + * Get the "inspection data" from this item, which will be returned to the user. + * + * @return This items's inspection data. + */ + @Nonnull + public Map getData() + { + return data; + } + + /** + * If this event is being fired on the server thread. When true, information which relies on server state may be + * exposed. + * + * @return If this is run on the main thread. + */ + public boolean onMainThread() + { + return mainThread; + } + + /** + * Add new information to the inspection result. Note this will override fields with the same name. + * + * @param newData The data to add. Note all values should be convertible to Lua (see + * {@link MethodResult#of(Object)}). + */ + public void addData( @Nonnull Map newData ) + { + Objects.requireNonNull( newData, "newData cannot be null" ); + data.putAll( newData ); + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleInventoryEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleInventoryEvent.java new file mode 100644 index 000000000..f01436f6b --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleInventoryEvent.java @@ -0,0 +1,84 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle.event; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.common.util.FakePlayer; +import net.minecraftforge.items.IItemHandler; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * Fired when a turtle attempts to interact with an inventory. + */ +public abstract class TurtleInventoryEvent extends TurtleBlockEvent +{ + private final IItemHandler handler; + + protected TurtleInventoryEvent( @Nonnull ITurtleAccess turtle, @Nonnull TurtleAction action, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nullable IItemHandler handler ) + { + super( turtle, action, player, world, pos ); + this.handler = handler; + } + + /** + * Get the inventory being interacted with. + * + * @return The inventory being interacted with, {@code null} if the item will be dropped to/sucked from the world. + */ + @Nullable + public IItemHandler getItemHandler() + { + return handler; + } + + /** + * Fired when a turtle attempts to suck from an inventory. + * + * @see TurtleAction#SUCK + */ + public static class Suck extends TurtleInventoryEvent + { + public Suck( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nullable IItemHandler handler ) + { + super( turtle, TurtleAction.SUCK, player, world, pos, handler ); + } + } + + /** + * Fired when a turtle attempts to drop an item into an inventory. + * + * @see TurtleAction#DROP + */ + public static class Drop extends TurtleInventoryEvent + { + private final ItemStack stack; + + public Drop( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nullable IItemHandler handler, @Nonnull ItemStack stack ) + { + super( turtle, TurtleAction.DROP, player, world, pos, handler ); + + Objects.requireNonNull( stack, "stack cannot be null" ); + this.stack = stack; + } + + /** + * The item which will be inserted into the inventory/dropped on the ground. + * + * @return The item stack which will be dropped. This should not be modified. + */ + @Nonnull + public ItemStack getStack() + { + return stack; + } + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtlePlayerEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtlePlayerEvent.java new file mode 100644 index 000000000..476221171 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtlePlayerEvent.java @@ -0,0 +1,43 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle.event; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import net.minecraftforge.common.util.FakePlayer; + +import javax.annotation.Nonnull; +import java.util.Objects; + +/** + * An action done by a turtle which is normally done by a player. + * + * {@link #getPlayer()} may be used to modify the player's attributes or perform permission checks. + */ +public abstract class TurtlePlayerEvent extends TurtleActionEvent +{ + private final FakePlayer player; + + protected TurtlePlayerEvent( @Nonnull ITurtleAccess turtle, @Nonnull TurtleAction action, @Nonnull FakePlayer player ) + { + super( turtle, action ); + + Objects.requireNonNull( player, "player cannot be null" ); + this.player = player; + } + + /** + * A fake player, representing this turtle. + * + * This may be used for triggering permission checks. + * + * @return A {@link FakePlayer} representing this turtle. + */ + @Nonnull + public FakePlayer getPlayer() + { + return player; + } +} diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleRefuelEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleRefuelEvent.java new file mode 100644 index 000000000..e9fc0c92e --- /dev/null +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleRefuelEvent.java @@ -0,0 +1,90 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. 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.turtle.event; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * Fired when a turtle attempts to refuel from an item. + * + * One may use {@link #setCanceled(boolean, String)} to prevent refueling from this specific item. Additionally, you + * may use {@link #setHandler(Handler)} to register a custom fuel provider. + */ +public class TurtleRefuelEvent extends TurtleActionEvent +{ + private final ItemStack stack; + private Handler handler; + + public TurtleRefuelEvent( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack stack ) + { + super( turtle, TurtleAction.REFUEL ); + + Objects.requireNonNull( turtle, "turtle cannot be null" ); + this.stack = stack; + } + + /** + * Get the stack we are attempting to refuel from. + * + * Do not modify the returned stack - all modifications should be done within the {@link Handler}. + * + * @return The stack to refuel from. + */ + public ItemStack getStack() + { + return stack; + } + + /** + * Get the refuel handler for this stack. + * + * @return The refuel handler, or {@code null} if none has currently been set. + * @see #setHandler(Handler) + */ + @Nullable + public Handler getHandler() + { + return handler; + } + + /** + * Set the refuel handler for this stack. + * + * You should call this if you can actually refuel from this item, and ideally only if there are no existing + * handlers. + * + * @param handler The new refuel handler. + * @see #getHandler() + */ + public void setHandler( @Nullable Handler handler ) + { + this.handler = handler; + } + + /** + * Handles refuelling a turtle from a specific item. + */ + @FunctionalInterface + public interface Handler + { + /** + * Refuel a turtle using an item. + * + * @param turtle The turtle to refuel. + * @param stack The stack to refuel with. + * @param slot The slot the stack resides within. This may be used to modify the inventory afterwards. + * @param limit The maximum number of refuel operations to perform. This will often correspond to the number of + * items to consume. + * @return The amount of fuel gained. + */ + int refuel( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack stack, int slot, int limit ); + } +} diff --git a/src/main/java/dan200/computercraft/client/ClientRegistry.java b/src/main/java/dan200/computercraft/client/ClientRegistry.java new file mode 100644 index 000000000..31f17679a --- /dev/null +++ b/src/main/java/dan200/computercraft/client/ClientRegistry.java @@ -0,0 +1,151 @@ +/* + * 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.client; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.render.TurtleModelLoader; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.common.IColouredItem; +import dan200.computercraft.shared.media.items.ItemDisk; +import dan200.computercraft.shared.media.items.ItemTreasureDisk; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import dan200.computercraft.shared.util.Colour; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.UnbakedModel; +import net.minecraft.client.util.ModelIdentifier; +import net.minecraft.screen.PlayerScreenHandler; +import net.minecraft.util.Identifier; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.ColorHandlerEvent; +import net.minecraftforge.client.event.ModelBakeEvent; +import net.minecraftforge.client.event.ModelRegistryEvent; +import net.minecraftforge.client.event.TextureStitchEvent; +import net.minecraftforge.client.model.ModelLoader; +import net.minecraftforge.client.model.ModelLoaderRegistry; +import net.minecraftforge.client.model.SimpleModelTransform; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.HashSet; +import java.util.Map; + +/** + * Registers textures and models for items. + */ +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD ) +public final class ClientRegistry +{ + private static final String[] EXTRA_MODELS = new String[] { + "turtle_modem_normal_off_left", + "turtle_modem_normal_on_left", + "turtle_modem_normal_off_right", + "turtle_modem_normal_on_right", + + "turtle_modem_advanced_off_left", + "turtle_modem_advanced_on_left", + "turtle_modem_advanced_off_right", + "turtle_modem_advanced_on_right", + "turtle_crafting_table_left", + "turtle_crafting_table_right", + + "turtle_speaker_upgrade_left", + "turtle_speaker_upgrade_right", + + "turtle_colour", + "turtle_elf_overlay", + }; + + private static final String[] EXTRA_TEXTURES = new String[] { + // TODO: Gather these automatically from the model. Sadly the model loader isn't available + // when stitching textures. + "block/turtle_colour", + "block/turtle_elf_overlay", + "block/turtle_crafty_face", + "block/turtle_speaker_face", + }; + + private ClientRegistry() {} + + @SubscribeEvent + public static void registerModels( ModelRegistryEvent event ) + { + ModelLoaderRegistry.registerLoader( new Identifier( ComputerCraft.MOD_ID, "turtle" ), TurtleModelLoader.INSTANCE ); + } + + @SubscribeEvent + public static void onTextureStitchEvent( TextureStitchEvent.Pre event ) + { + if( !event.getMap().getId().equals( PlayerScreenHandler.BLOCK_ATLAS_TEXTURE ) ) return; + + for( String extra : EXTRA_TEXTURES ) + { + event.addSprite( new Identifier( ComputerCraft.MOD_ID, extra ) ); + } + } + + @SubscribeEvent + public static void onModelBakeEvent( ModelBakeEvent event ) + { + // Load all extra models + ModelLoader loader = event.getModelLoader(); + Map registry = event.getModelRegistry(); + + for( String modelName : EXTRA_MODELS ) + { + Identifier location = new Identifier( ComputerCraft.MOD_ID, "item/" + modelName ); + UnbakedModel model = loader.getOrLoadModel( location ); + model.getTextureDependencies( loader::getOrLoadModel, new HashSet<>() ); + + BakedModel baked = model.bake( loader, ModelLoader.defaultTextureGetter(), SimpleModelTransform.IDENTITY, location ); + if( baked != null ) + { + registry.put( new ModelIdentifier( new Identifier( ComputerCraft.MOD_ID, modelName ), "inventory" ), baked ); + } + } + } + + @SubscribeEvent + public static void onItemColours( ColorHandlerEvent.Item event ) + { + if( Registry.ModItems.DISK == null || Registry.ModBlocks.TURTLE_NORMAL == null ) + { + ComputerCraft.log.warn( "Block/item registration has failed. Skipping registration of item colours." ); + return; + } + + event.getItemColors().register( + ( stack, layer ) -> layer == 1 ? ((ItemDisk) stack.getItem()).getColour( stack ) : 0xFFFFFF, + Registry.ModItems.DISK.get() + ); + + event.getItemColors().register( + ( stack, layer ) -> layer == 1 ? ItemTreasureDisk.getColour( stack ) : 0xFFFFFF, + Registry.ModItems.TREASURE_DISK.get() + ); + + event.getItemColors().register( ( stack, layer ) -> { + switch( layer ) + { + case 0: + default: + return 0xFFFFFF; + case 1: // Frame colour + return IColouredItem.getColourBasic( stack ); + case 2: // Light colour + { + int light = ItemPocketComputer.getLightState( stack ); + return light == -1 ? Colour.BLACK.getHex() : light; + } + } + }, Registry.ModItems.POCKET_COMPUTER_NORMAL.get(), Registry.ModItems.POCKET_COMPUTER_ADVANCED.get() ); + + // Setup turtle colours + event.getItemColors().register( + ( stack, tintIndex ) -> tintIndex == 0 ? ((IColouredItem) stack.getItem()).getColour( stack ) : 0xFFFFFF, + Registry.ModBlocks.TURTLE_NORMAL.get(), Registry.ModBlocks.TURTLE_ADVANCED.get() + ); + } +} diff --git a/src/main/java/dan200/computercraft/client/ClientTableFormatter.java b/src/main/java/dan200/computercraft/client/ClientTableFormatter.java new file mode 100644 index 000000000..c5b208fdf --- /dev/null +++ b/src/main/java/dan200/computercraft/client/ClientTableFormatter.java @@ -0,0 +1,87 @@ +/* + * 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.client; + +import dan200.computercraft.shared.command.text.ChatHelpers; +import dan200.computercraft.shared.command.text.TableBuilder; +import dan200.computercraft.shared.command.text.TableFormatter; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.hud.ChatHud; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.MathHelper; +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Nullable; + +public class ClientTableFormatter implements TableFormatter +{ + public static final ClientTableFormatter INSTANCE = new ClientTableFormatter(); + + private static Int2IntOpenHashMap lastHeights = new Int2IntOpenHashMap(); + + private static TextRenderer renderer() + { + return MinecraftClient.getInstance().textRenderer; + } + + @Override + @Nullable + public Text getPadding( Text component, int width ) + { + int extraWidth = width - getWidth( component ); + if( extraWidth <= 0 ) return null; + + TextRenderer renderer = renderer(); + + float spaceWidth = renderer.getWidth( " " ); + int spaces = MathHelper.floor( extraWidth / spaceWidth ); + int extra = extraWidth - (int) (spaces * spaceWidth); + + return ChatHelpers.coloured( StringUtils.repeat( ' ', spaces ) + StringUtils.repeat( (char) 712, extra ), Formatting.GRAY ); + } + + @Override + public int getColumnPadding() + { + return 3; + } + + @Override + public int getWidth( Text component ) + { + return renderer().getWidth( component ); + } + + @Override + public void writeLine( int id, Text component ) + { + MinecraftClient mc = MinecraftClient.getInstance(); + ChatHud chat = mc.inGameHud.getChatHud(); + + // TODO: Trim the text if it goes over the allowed length + // int maxWidth = MathHelper.floor( chat.getChatWidth() / chat.getScale() ); + // List list = RenderComponentsUtil.func_238505_a_( component, maxWidth, mc.fontRenderer ); + // if( !list.isEmpty() ) chat.printChatMessageWithOptionalDeletion( list.get( 0 ), id ); + chat.addMessage( component, id ); + } + + @Override + public int display( TableBuilder table ) + { + ChatHud chat = MinecraftClient.getInstance().inGameHud.getChatHud(); + + int lastHeight = lastHeights.get( table.getId() ); + + int height = TableFormatter.super.display( table ); + lastHeights.put( table.getId(), height ); + + for( int i = height; i < lastHeight; i++ ) chat.removeMessage( i + table.getId() ); + return height; + } +} diff --git a/src/main/java/dan200/computercraft/client/FrameInfo.java b/src/main/java/dan200/computercraft/client/FrameInfo.java new file mode 100644 index 000000000..00e72b67a --- /dev/null +++ b/src/main/java/dan200/computercraft/client/FrameInfo.java @@ -0,0 +1,45 @@ +/* + * 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.client; + +import dan200.computercraft.ComputerCraft; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT ) +public final class FrameInfo +{ + private static int tick; + private static long renderFrame; + + private FrameInfo() + { + } + + public static boolean getGlobalCursorBlink() + { + return (tick / 8) % 2 == 0; + } + + public static long getRenderFrame() + { + return renderFrame; + } + + @SubscribeEvent + public static void onTick( TickEvent.ClientTickEvent event ) + { + if( event.phase == TickEvent.Phase.START ) tick++; + } + + @SubscribeEvent + public static void onRenderTick( TickEvent.RenderTickEvent event ) + { + if( event.phase == TickEvent.Phase.START ) renderFrame++; + } +} diff --git a/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java b/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java new file mode 100644 index 000000000..0753426bd --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java @@ -0,0 +1,349 @@ +/* + * 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.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.client.FrameInfo; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.terminal.TextBuffer; +import dan200.computercraft.shared.util.Colour; +import dan200.computercraft.shared.util.Palette; +import org.lwjgl.opengl.GL11; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderPhase; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.VertexFormat; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.util.math.AffineTransformation; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Matrix4f; + +public final class FixedWidthFontRenderer +{ + private static final Matrix4f IDENTITY = AffineTransformation.identity().getMatrix(); + + private static final Identifier FONT = new Identifier( "computercraft", "textures/gui/term_font.png" ); + + public static final int FONT_HEIGHT = 9; + public static final int FONT_WIDTH = 6; + public static final float WIDTH = 256.0f; + + public static final float BACKGROUND_START = (WIDTH - 6.0f) / WIDTH; + public static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH; + + public static final RenderLayer TYPE = Type.MAIN; + + private FixedWidthFontRenderer() + { + } + + public static float toGreyscale( double[] rgb ) + { + return (float) ((rgb[0] + rgb[1] + rgb[2]) / 3); + } + + public static int getColour( char c, Colour def ) + { + return 15 - Terminal.getColour( c, def ); + } + + private static void drawChar( Matrix4f transform, VertexConsumer buffer, float x, float y, int index, float r, float g, float b ) + { + // Short circuit to avoid the common case - the texture should be blank here after all. + if( index == '\0' || index == ' ' ) return; + + int column = index % 16; + int row = index / 16; + + int xStart = 1 + column * (FONT_WIDTH + 2); + int yStart = 1 + row * (FONT_HEIGHT + 2); + + buffer.vertex( transform, x, y, 0f ).color( r, g, b, 1.0f ).texture( xStart / WIDTH, yStart / WIDTH ).next(); + buffer.vertex( transform, x, y + FONT_HEIGHT, 0f ).color( r, g, b, 1.0f ).texture( xStart / WIDTH, (yStart + FONT_HEIGHT) / WIDTH ).next(); + buffer.vertex( transform, x + FONT_WIDTH, y, 0f ).color( r, g, b, 1.0f ).texture( (xStart + FONT_WIDTH) / WIDTH, yStart / WIDTH ).next(); + buffer.vertex( transform, x + FONT_WIDTH, y, 0f ).color( r, g, b, 1.0f ).texture( (xStart + FONT_WIDTH) / WIDTH, yStart / WIDTH ).next(); + buffer.vertex( transform, x, y + FONT_HEIGHT, 0f ).color( r, g, b, 1.0f ).texture( xStart / WIDTH, (yStart + FONT_HEIGHT) / WIDTH ).next(); + buffer.vertex( transform, x + FONT_WIDTH, y + FONT_HEIGHT, 0f ).color( r, g, b, 1.0f ).texture( (xStart + FONT_WIDTH) / WIDTH, (yStart + FONT_HEIGHT) / WIDTH ).next(); + } + + private static void drawQuad( Matrix4f transform, VertexConsumer buffer, float x, float y, float width, float height, float r, float g, float b ) + { + buffer.vertex( transform, x, y, 0 ).color( r, g, b, 1.0f ).texture( BACKGROUND_START, BACKGROUND_START ).next(); + buffer.vertex( transform, x, y + height, 0 ).color( r, g, b, 1.0f ).texture( BACKGROUND_START, BACKGROUND_END ).next(); + buffer.vertex( transform, x + width, y, 0 ).color( r, g, b, 1.0f ).texture( BACKGROUND_END, BACKGROUND_START ).next(); + buffer.vertex( transform, x + width, y, 0 ).color( r, g, b, 1.0f ).texture( BACKGROUND_END, BACKGROUND_START ).next(); + buffer.vertex( transform, x, y + height, 0 ).color( r, g, b, 1.0f ).texture( BACKGROUND_START, BACKGROUND_END ).next(); + buffer.vertex( transform, x + width, y + height, 0 ).color( r, g, b, 1.0f ).texture( BACKGROUND_END, BACKGROUND_END ).next(); + } + + private static void drawQuad( Matrix4f transform, VertexConsumer buffer, float x, float y, float width, float height, Palette palette, boolean greyscale, char colourIndex ) + { + double[] colour = palette.getColour( getColour( colourIndex, Colour.BLACK ) ); + float r, g, b; + if( greyscale ) + { + r = g = b = toGreyscale( colour ); + } + else + { + r = (float) colour[0]; + g = (float) colour[1]; + b = (float) colour[2]; + } + + drawQuad( transform, buffer, x, y, width, height, r, g, b ); + } + + private static void drawBackground( + @Nonnull Matrix4f transform, @Nonnull VertexConsumer renderer, float x, float y, + @Nonnull TextBuffer backgroundColour, @Nonnull Palette palette, boolean greyscale, + float leftMarginSize, float rightMarginSize, float height + ) + { + if( leftMarginSize > 0 ) + { + drawQuad( transform, renderer, x - leftMarginSize, y, leftMarginSize, height, palette, greyscale, backgroundColour.charAt( 0 ) ); + } + + if( rightMarginSize > 0 ) + { + drawQuad( transform, renderer, x + backgroundColour.length() * FONT_WIDTH, y, rightMarginSize, height, palette, greyscale, backgroundColour.charAt( backgroundColour.length() - 1 ) ); + } + + // Batch together runs of identical background cells. + int blockStart = 0; + char blockColour = '\0'; + for( int i = 0; i < backgroundColour.length(); i++ ) + { + char colourIndex = backgroundColour.charAt( i ); + if( colourIndex == blockColour ) continue; + + if( blockColour != '\0' ) + { + drawQuad( transform, renderer, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (i - blockStart), height, palette, greyscale, blockColour ); + } + + blockColour = colourIndex; + blockStart = i; + } + + if( blockColour != '\0' ) + { + drawQuad( transform, renderer, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (backgroundColour.length() - blockStart), height, palette, greyscale, blockColour ); + } + } + + public static void drawString( + @Nonnull Matrix4f transform, @Nonnull VertexConsumer renderer, float x, float y, + @Nonnull TextBuffer text, @Nonnull TextBuffer textColour, @Nullable TextBuffer backgroundColour, + @Nonnull Palette palette, boolean greyscale, float leftMarginSize, float rightMarginSize + ) + { + if( backgroundColour != null ) + { + drawBackground( transform, renderer, x, y, backgroundColour, palette, greyscale, leftMarginSize, rightMarginSize, FONT_HEIGHT ); + } + + for( int i = 0; i < text.length(); i++ ) + { + double[] colour = palette.getColour( getColour( textColour.charAt( i ), Colour.BLACK ) ); + float r, g, b; + if( greyscale ) + { + r = g = b = toGreyscale( colour ); + } + else + { + r = (float) colour[0]; + g = (float) colour[1]; + b = (float) colour[2]; + } + + // Draw char + int index = text.charAt( i ); + if( index > 255 ) index = '?'; + drawChar( transform, renderer, x + i * FONT_WIDTH, y, index, r, g, b ); + } + + } + + public static void drawString( + float x, float y, @Nonnull TextBuffer text, @Nonnull TextBuffer textColour, @Nullable TextBuffer backgroundColour, + @Nonnull Palette palette, boolean greyscale, float leftMarginSize, float rightMarginSize + ) + { + bindFont(); + + VertexConsumerProvider.Immediate renderer = MinecraftClient.getInstance().getBufferBuilders().getEntityVertexConsumers(); + drawString( IDENTITY, ((VertexConsumerProvider) renderer).getBuffer( TYPE ), x, y, text, textColour, backgroundColour, palette, greyscale, leftMarginSize, rightMarginSize ); + renderer.draw(); + } + + public static void drawTerminalWithoutCursor( + @Nonnull Matrix4f transform, @Nonnull VertexConsumer buffer, float x, float y, + @Nonnull Terminal terminal, boolean greyscale, + float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize + ) + { + Palette palette = terminal.getPalette(); + int height = terminal.getHeight(); + + // Top and bottom margins + drawBackground( + transform, buffer, x, y - topMarginSize, + terminal.getBackgroundColourLine( 0 ), palette, greyscale, + leftMarginSize, rightMarginSize, topMarginSize + ); + + drawBackground( + transform, buffer, x, y + height * FONT_HEIGHT, + terminal.getBackgroundColourLine( height - 1 ), palette, greyscale, + leftMarginSize, rightMarginSize, bottomMarginSize + ); + + // The main text + for( int i = 0; i < height; i++ ) + { + drawString( + transform, buffer, x, y + FixedWidthFontRenderer.FONT_HEIGHT * i, + terminal.getLine( i ), terminal.getTextColourLine( i ), terminal.getBackgroundColourLine( i ), + palette, greyscale, leftMarginSize, rightMarginSize + ); + } + } + + public static void drawCursor( + @Nonnull Matrix4f transform, @Nonnull VertexConsumer buffer, float x, float y, + @Nonnull Terminal terminal, boolean greyscale + ) + { + Palette palette = terminal.getPalette(); + int width = terminal.getWidth(); + int height = terminal.getHeight(); + + int cursorX = terminal.getCursorX(); + int cursorY = terminal.getCursorY(); + if( terminal.getCursorBlink() && cursorX >= 0 && cursorX < width && cursorY >= 0 && cursorY < height && FrameInfo.getGlobalCursorBlink() ) + { + double[] colour = palette.getColour( 15 - terminal.getTextColour() ); + float r, g, b; + if( greyscale ) + { + r = g = b = toGreyscale( colour ); + } + else + { + r = (float) colour[0]; + g = (float) colour[1]; + b = (float) colour[2]; + } + + drawChar( transform, buffer, x + cursorX * FONT_WIDTH, y + cursorY * FONT_HEIGHT, '_', r, g, b ); + } + } + + public static void drawTerminal( + @Nonnull Matrix4f transform, @Nonnull VertexConsumer buffer, float x, float y, + @Nonnull Terminal terminal, boolean greyscale, + float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize + ) + { + drawTerminalWithoutCursor( transform, buffer, x, y, terminal, greyscale, topMarginSize, bottomMarginSize, leftMarginSize, rightMarginSize ); + drawCursor( transform, buffer, x, y, terminal, greyscale ); + } + + public static void drawTerminal( + @Nonnull Matrix4f transform, float x, float y, @Nonnull Terminal terminal, boolean greyscale, + float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize + ) + { + bindFont(); + + VertexConsumerProvider.Immediate renderer = MinecraftClient.getInstance().getBufferBuilders().getEntityVertexConsumers(); + VertexConsumer buffer = renderer.getBuffer( TYPE ); + drawTerminal( transform, buffer, x, y, terminal, greyscale, topMarginSize, bottomMarginSize, leftMarginSize, rightMarginSize ); + renderer.draw( TYPE ); + } + + public static void drawTerminal( + float x, float y, @Nonnull Terminal terminal, boolean greyscale, + float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize + ) + { + drawTerminal( IDENTITY, x, y, terminal, greyscale, topMarginSize, bottomMarginSize, leftMarginSize, rightMarginSize ); + } + + public static void drawEmptyTerminal( @Nonnull Matrix4f transform, @Nonnull VertexConsumerProvider renderer, float x, float y, float width, float height ) + { + Colour colour = Colour.BLACK; + drawQuad( transform, renderer.getBuffer( TYPE ), x, y, width, height, colour.getR(), colour.getG(), colour.getB() ); + } + + public static void drawEmptyTerminal( @Nonnull Matrix4f transform, float x, float y, float width, float height ) + { + bindFont(); + + VertexConsumerProvider.Immediate renderer = MinecraftClient.getInstance().getBufferBuilders().getEntityVertexConsumers(); + drawEmptyTerminal( transform, renderer, x, y, width, height ); + renderer.draw(); + } + + public static void drawEmptyTerminal( float x, float y, float width, float height ) + { + drawEmptyTerminal( IDENTITY, x, y, width, height ); + } + + public static void drawBlocker( @Nonnull Matrix4f transform, @Nonnull VertexConsumerProvider renderer, float x, float y, float width, float height ) + { + Colour colour = Colour.BLACK; + drawQuad( transform, renderer.getBuffer( Type.BLOCKER ), x, y, width, height, colour.getR(), colour.getG(), colour.getB() ); + } + + private static void bindFont() + { + MinecraftClient.getInstance().getTextureManager().bindTexture( FONT ); + RenderSystem.texParameter( GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP ); + } + + private static final class Type extends RenderPhase + { + private static final int GL_MODE = GL11.GL_TRIANGLES; + + private static final VertexFormat FORMAT = VertexFormats.POSITION_COLOR_TEXTURE; + + static final RenderLayer MAIN = RenderLayer.of( + "terminal_font", FORMAT, GL_MODE, 1024, + false, false, // useDelegate, needsSorting + RenderLayer.MultiPhaseParameters.builder() + .texture( new RenderPhase.Texture( FONT, false, false ) ) // blur, minimap + .alpha( ONE_TENTH_ALPHA ) + .lightmap( DISABLE_LIGHTMAP ) + .writeMaskState( COLOR_MASK ) + .build( false ) + ); + + static final RenderLayer BLOCKER = RenderLayer.of( + "terminal_blocker", FORMAT, GL_MODE, 256, + false, false, // useDelegate, needsSorting + RenderLayer.MultiPhaseParameters.builder() + .texture( new RenderPhase.Texture( FONT, false, false ) ) // blur, minimap + .alpha( ONE_TENTH_ALPHA ) + .writeMaskState( DEPTH_MASK ) + .lightmap( DISABLE_LIGHTMAP ) + .build( false ) + ); + + private Type( String name, Runnable setup, Runnable destroy ) + { + super( name, setup, destroy ); + } + } +} diff --git a/src/main/java/dan200/computercraft/client/gui/GuiComputer.java b/src/main/java/dan200/computercraft/client/gui/GuiComputer.java new file mode 100644 index 000000000..0ee1293a9 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/GuiComputer.java @@ -0,0 +1,159 @@ +/* + * 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.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.gui.widgets.WidgetTerminal; +import dan200.computercraft.client.gui.widgets.WidgetWrapper; +import dan200.computercraft.client.render.ComputerBorderRenderer; +import dan200.computercraft.shared.computer.core.ClientComputer; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.inventory.ContainerComputer; +import dan200.computercraft.shared.computer.inventory.ContainerComputerBase; +import dan200.computercraft.shared.computer.inventory.ContainerViewComputer; +import dan200.computercraft.shared.pocket.inventory.ContainerPocketComputer; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.text.Text; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nonnull; + +import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER; +import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN; + +public final class GuiComputer extends HandledScreen +{ + private final ComputerFamily family; + private final ClientComputer computer; + private final int termWidth; + private final int termHeight; + + private WidgetTerminal terminal; + private WidgetWrapper terminalWrapper; + + private GuiComputer( + T container, PlayerInventory player, Text title, int termWidth, int termHeight + ) + { + super( container, player, title ); + family = container.getFamily(); + computer = (ClientComputer) container.getComputer(); + this.termWidth = termWidth; + this.termHeight = termHeight; + terminal = null; + } + + public static GuiComputer create( ContainerComputer container, PlayerInventory inventory, Text component ) + { + return new GuiComputer<>( + container, inventory, component, + ComputerCraft.computerTermWidth, ComputerCraft.computerTermHeight + ); + } + + public static GuiComputer createPocket( ContainerPocketComputer container, PlayerInventory inventory, Text component ) + { + return new GuiComputer<>( + container, inventory, component, + ComputerCraft.pocketTermWidth, ComputerCraft.pocketTermHeight + ); + } + + public static GuiComputer createView( ContainerViewComputer container, PlayerInventory inventory, Text component ) + { + return new GuiComputer<>( + container, inventory, component, + container.getWidth(), container.getHeight() + ); + } + + + @Override + protected void init() + { + client.keyboard.setRepeatEvents( true ); + + int termPxWidth = termWidth * FixedWidthFontRenderer.FONT_WIDTH; + int termPxHeight = termHeight * FixedWidthFontRenderer.FONT_HEIGHT; + + backgroundWidth = termPxWidth + MARGIN * 2 + BORDER * 2; + backgroundHeight = termPxHeight + MARGIN * 2 + BORDER * 2; + + super.init(); + + terminal = new WidgetTerminal( client, () -> computer, termWidth, termHeight, MARGIN, MARGIN, MARGIN, MARGIN ); + terminalWrapper = new WidgetWrapper( terminal, MARGIN + BORDER + x, MARGIN + BORDER + y, termPxWidth, termPxHeight ); + + children.add( terminalWrapper ); + setFocused( terminalWrapper ); + } + + @Override + public void removed() + { + super.removed(); + children.remove( terminal ); + terminal = null; + client.keyboard.setRepeatEvents( false ); + } + + @Override + public void tick() + { + super.tick(); + terminal.update(); + } + + @Override + public boolean keyPressed( int key, int scancode, int modifiers ) + { + // Forward the tab key to the terminal, rather than moving between controls. + if( key == GLFW.GLFW_KEY_TAB && getFocused() != null && getFocused() == terminalWrapper ) + { + return getFocused().keyPressed( key, scancode, modifiers ); + } + + return super.keyPressed( key, scancode, modifiers ); + } + + @Override + public void drawBackground( @Nonnull MatrixStack stack, float partialTicks, int mouseX, int mouseY ) + { + // Draw terminal + terminal.draw( terminalWrapper.getX(), terminalWrapper.getY() ); + + // Draw a border around the terminal + RenderSystem.color4f( 1, 1, 1, 1 ); + client.getTextureManager().bindTexture( ComputerBorderRenderer.getTexture( family ) ); + ComputerBorderRenderer.render( + terminalWrapper.getX() - MARGIN, terminalWrapper.getY() - MARGIN, getZOffset(), + terminalWrapper.getWidth() + MARGIN * 2, terminalWrapper.getHeight() + MARGIN * 2 + ); + } + + @Override + public void render( @Nonnull MatrixStack stack, int mouseX, int mouseY, float partialTicks ) + { + super.render( stack, mouseX, mouseY, partialTicks ); + drawMouseoverTooltip( stack, mouseX, mouseY ); + } + + @Override + public boolean mouseDragged( double x, double y, int button, double deltaX, double deltaY ) + { + return (getFocused() != null && getFocused().mouseDragged( x, y, button, deltaX, deltaY )) + || super.mouseDragged( x, y, button, deltaX, deltaY ); + } + + @Override + protected void drawForeground( @Nonnull MatrixStack transform, int mouseX, int mouseY ) + { + // Skip rendering labels. + } +} diff --git a/src/main/java/dan200/computercraft/client/gui/GuiDiskDrive.java b/src/main/java/dan200/computercraft/client/gui/GuiDiskDrive.java new file mode 100644 index 000000000..7f27d1b54 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/GuiDiskDrive.java @@ -0,0 +1,41 @@ +/* + * 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.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.shared.peripheral.diskdrive.ContainerDiskDrive; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import javax.annotation.Nonnull; + +public class GuiDiskDrive extends HandledScreen +{ + private static final Identifier BACKGROUND = new Identifier( "computercraft", "textures/gui/disk_drive.png" ); + + public GuiDiskDrive( ContainerDiskDrive container, PlayerInventory player, Text title ) + { + super( container, player, title ); + } + + @Override + protected void drawBackground( @Nonnull MatrixStack transform, float partialTicks, int mouseX, int mouseY ) + { + RenderSystem.color4f( 1.0F, 1.0F, 1.0F, 1.0F ); + client.getTextureManager().bindTexture( BACKGROUND ); + drawTexture( transform, x, y, 0, 0, backgroundWidth, backgroundHeight ); + } + + @Override + public void render( @Nonnull MatrixStack transform, int mouseX, int mouseY, float partialTicks ) + { + renderBackground( transform ); + super.render( transform, mouseX, mouseY, partialTicks ); + drawMouseoverTooltip( transform, mouseX, mouseY ); + } +} diff --git a/src/main/java/dan200/computercraft/client/gui/GuiPrinter.java b/src/main/java/dan200/computercraft/client/gui/GuiPrinter.java new file mode 100644 index 000000000..e81d847a6 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/GuiPrinter.java @@ -0,0 +1,51 @@ +/* + * 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.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.shared.peripheral.printer.ContainerPrinter; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import javax.annotation.Nonnull; + +public class GuiPrinter extends HandledScreen +{ + private static final Identifier BACKGROUND = new Identifier( "computercraft", "textures/gui/printer.png" ); + + public GuiPrinter( ContainerPrinter container, PlayerInventory player, Text title ) + { + super( container, player, title ); + } + + /*@Override + protected void drawGuiContainerForegroundLayer( int mouseX, int mouseY ) + { + String title = getTitle().getFormattedText(); + font.drawString( title, (xSize - font.getStringWidth( title )) / 2.0f, 6, 0x404040 ); + font.drawString( I18n.format( "container.inventory" ), 8, ySize - 96 + 2, 0x404040 ); + }*/ + + @Override + protected void drawBackground( @Nonnull MatrixStack transform, float partialTicks, int mouseX, int mouseY ) + { + RenderSystem.color4f( 1.0F, 1.0F, 1.0F, 1.0F ); + client.getTextureManager().bindTexture( BACKGROUND ); + drawTexture( transform, x, y, 0, 0, backgroundWidth, backgroundHeight ); + + if( getScreenHandler().isPrinting() ) drawTexture( transform, x + 34, y + 21, 176, 0, 25, 45 ); + } + + @Override + public void render( @Nonnull MatrixStack stack, int mouseX, int mouseY, float partialTicks ) + { + renderBackground( stack ); + super.render( stack, mouseX, mouseY, partialTicks ); + drawMouseoverTooltip( stack, mouseX, mouseY ); + } +} diff --git a/src/main/java/dan200/computercraft/client/gui/GuiPrintout.java b/src/main/java/dan200/computercraft/client/gui/GuiPrintout.java new file mode 100644 index 000000000..c60f12d36 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/GuiPrintout.java @@ -0,0 +1,123 @@ +/* + * 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.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.core.terminal.TextBuffer; +import dan200.computercraft.shared.common.ContainerHeldItem; +import dan200.computercraft.shared.media.items.ItemPrintout; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.text.Text; +import net.minecraft.util.math.Matrix4f; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nonnull; + +import static dan200.computercraft.client.render.PrintoutRenderer.*; + +public class GuiPrintout extends HandledScreen +{ + private final boolean m_book; + private final int m_pages; + private final TextBuffer[] m_text; + private final TextBuffer[] m_colours; + private int m_page; + + public GuiPrintout( ContainerHeldItem container, PlayerInventory player, Text title ) + { + super( container, player, title ); + + backgroundHeight = Y_SIZE; + + String[] text = ItemPrintout.getText( container.getStack() ); + m_text = new TextBuffer[text.length]; + for( int i = 0; i < m_text.length; i++ ) m_text[i] = new TextBuffer( text[i] ); + + String[] colours = ItemPrintout.getColours( container.getStack() ); + m_colours = new TextBuffer[colours.length]; + for( int i = 0; i < m_colours.length; i++ ) m_colours[i] = new TextBuffer( colours[i] ); + + m_page = 0; + m_pages = Math.max( m_text.length / ItemPrintout.LINES_PER_PAGE, 1 ); + m_book = ((ItemPrintout) container.getStack().getItem()).getType() == ItemPrintout.Type.BOOK; + } + + @Override + public boolean keyPressed( int key, int scancode, int modifiers ) + { + if( super.keyPressed( key, scancode, modifiers ) ) return true; + + if( key == GLFW.GLFW_KEY_RIGHT ) + { + if( m_page < m_pages - 1 ) m_page++; + return true; + } + + if( key == GLFW.GLFW_KEY_LEFT ) + { + if( m_page > 0 ) m_page--; + return true; + } + + return false; + } + + @Override + public boolean mouseScrolled( double x, double y, double delta ) + { + if( super.mouseScrolled( x, y, delta ) ) return true; + if( delta < 0 ) + { + // Scroll up goes to the next page + if( m_page < m_pages - 1 ) m_page++; + return true; + } + + if( delta > 0 ) + { + // Scroll down goes to the previous page + if( m_page > 0 ) m_page--; + return true; + } + + return false; + } + + @Override + protected void drawBackground( @Nonnull MatrixStack transform, float partialTicks, int mouseX, int mouseY ) + { + // Draw the printout + RenderSystem.color4f( 1.0f, 1.0f, 1.0f, 1.0f ); + RenderSystem.enableDepthTest(); + + VertexConsumerProvider.Immediate renderer = MinecraftClient.getInstance().getBufferBuilders().getEntityVertexConsumers(); + Matrix4f matrix = transform.peek().getModel(); + drawBorder( matrix, renderer, x, y, getZOffset(), m_page, m_pages, m_book ); + drawText( matrix, renderer, x + X_TEXT_MARGIN, y + Y_TEXT_MARGIN, ItemPrintout.LINES_PER_PAGE * m_page, m_text, m_colours ); + renderer.draw(); + } + + @Override + public void render( @Nonnull MatrixStack stack, int mouseX, int mouseY, float partialTicks ) + { + // We must take the background further back in order to not overlap with our printed pages. + setZOffset( getZOffset() - 1 ); + renderBackground( stack ); + setZOffset( getZOffset() + 1 ); + + super.render( stack, mouseX, mouseY, partialTicks ); + } + + @Override + protected void drawForeground( @Nonnull MatrixStack transform, int mouseX, int mouseY ) + { + // Skip rendering labels. + } +} diff --git a/src/main/java/dan200/computercraft/client/gui/GuiTurtle.java b/src/main/java/dan200/computercraft/client/gui/GuiTurtle.java new file mode 100644 index 000000000..771ff4a83 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/GuiTurtle.java @@ -0,0 +1,144 @@ +/* + * 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.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.gui.widgets.WidgetTerminal; +import dan200.computercraft.client.gui.widgets.WidgetWrapper; +import dan200.computercraft.shared.computer.core.ClientComputer; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.turtle.inventory.ContainerTurtle; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nonnull; + +public class GuiTurtle extends HandledScreen +{ + private static final Identifier BACKGROUND_NORMAL = new Identifier( "computercraft", "textures/gui/turtle_normal.png" ); + private static final Identifier BACKGROUND_ADVANCED = new Identifier( "computercraft", "textures/gui/turtle_advanced.png" ); + + private ContainerTurtle m_container; + + private final ComputerFamily m_family; + private final ClientComputer m_computer; + + private WidgetTerminal terminal; + private WidgetWrapper terminalWrapper; + + public GuiTurtle( ContainerTurtle container, PlayerInventory player, Text title ) + { + super( container, player, title ); + + m_container = container; + m_family = container.getFamily(); + m_computer = (ClientComputer) container.getComputer(); + + backgroundWidth = 254; + backgroundHeight = 217; + } + + @Override + protected void init() + { + super.init(); + client.keyboard.setRepeatEvents( true ); + + int termPxWidth = ComputerCraft.turtleTermWidth * FixedWidthFontRenderer.FONT_WIDTH; + int termPxHeight = ComputerCraft.turtleTermHeight * FixedWidthFontRenderer.FONT_HEIGHT; + + terminal = new WidgetTerminal( + client, () -> m_computer, + ComputerCraft.turtleTermWidth, + ComputerCraft.turtleTermHeight, + 2, 2, 2, 2 + ); + terminalWrapper = new WidgetWrapper( terminal, 2 + 8 + x, 2 + 8 + y, termPxWidth, termPxHeight ); + + children.add( terminalWrapper ); + setFocused( terminalWrapper ); + } + + @Override + public void removed() + { + super.removed(); + children.remove( terminal ); + terminal = null; + client.keyboard.setRepeatEvents( false ); + } + + @Override + public void tick() + { + super.tick(); + terminal.update(); + } + + @Override + public boolean keyPressed( int key, int scancode, int modifiers ) + { + // Forward the tab key to the terminal, rather than moving between controls. + if( key == GLFW.GLFW_KEY_TAB && getFocused() != null && getFocused() == terminalWrapper ) + { + return getFocused().keyPressed( key, scancode, modifiers ); + } + + return super.keyPressed( key, scancode, modifiers ); + } + + @Override + protected void drawBackground( @Nonnull MatrixStack transform, float partialTicks, int mouseX, int mouseY ) + { + // Draw term + Identifier texture = m_family == ComputerFamily.ADVANCED ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL; + terminal.draw( terminalWrapper.getX(), terminalWrapper.getY() ); + + // Draw border/inventory + RenderSystem.color4f( 1.0F, 1.0F, 1.0F, 1.0F ); + client.getTextureManager().bindTexture( texture ); + drawTexture( transform, x, y, 0, 0, backgroundWidth, backgroundHeight ); + + // Draw selection slot + int slot = m_container.getSelectedSlot(); + if( slot >= 0 ) + { + int slotX = slot % 4; + int slotY = slot / 4; + drawTexture( transform, + x + ContainerTurtle.TURTLE_START_X - 2 + slotX * 18, + y + ContainerTurtle.PLAYER_START_Y - 2 + slotY * 18, + 0, 217, 24, 24 + ); + } + } + + @Override + public void render( @Nonnull MatrixStack stack, int mouseX, int mouseY, float partialTicks ) + { + renderBackground( stack ); + super.render( stack, mouseX, mouseY, partialTicks ); + drawMouseoverTooltip( stack, mouseX, mouseY ); + } + + @Override + public boolean mouseDragged( double x, double y, int button, double deltaX, double deltaY ) + { + return (getFocused() != null && getFocused().mouseDragged( x, y, button, deltaX, deltaY )) + || super.mouseDragged( x, y, button, deltaX, deltaY ); + } + + @Override + protected void drawForeground( @Nonnull MatrixStack transform, int mouseX, int mouseY ) + { + // Skip rendering labels. + } +} diff --git a/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java b/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java new file mode 100644 index 000000000..1cc82bd7d --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java @@ -0,0 +1,354 @@ +/* + * 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.client.gui.widgets; + +import dan200.computercraft.client.gui.FixedWidthFontRenderer; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.computer.core.ClientComputer; +import dan200.computercraft.shared.computer.core.IComputer; +import org.lwjgl.glfw.GLFW; + +import java.util.BitSet; +import java.util.function.Supplier; +import net.minecraft.SharedConstants; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Element; + +import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_HEIGHT; +import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_WIDTH; + +public class WidgetTerminal implements Element +{ + private static final float TERMINATE_TIME = 0.5f; + + private final MinecraftClient client; + + private boolean focused; + + private final Supplier computer; + private final int termWidth; + private final int termHeight; + + private float terminateTimer = -1; + private float rebootTimer = -1; + private float shutdownTimer = -1; + + private int lastMouseButton = -1; + private int lastMouseX = -1; + private int lastMouseY = -1; + + private final int leftMargin; + private final int rightMargin; + private final int topMargin; + private final int bottomMargin; + + private final BitSet keysDown = new BitSet( 256 ); + + public WidgetTerminal( MinecraftClient client, Supplier computer, int termWidth, int termHeight, int leftMargin, int rightMargin, int topMargin, int bottomMargin ) + { + this.client = client; + this.computer = computer; + this.termWidth = termWidth; + this.termHeight = termHeight; + this.leftMargin = leftMargin; + this.rightMargin = rightMargin; + this.topMargin = topMargin; + this.bottomMargin = bottomMargin; + } + + @Override + public boolean charTyped( char ch, int modifiers ) + { + if( ch >= 32 && ch <= 126 || ch >= 160 && ch <= 255 ) // printable chars in byte range + { + // Queue the "char" event + queueEvent( "char", Character.toString( ch ) ); + } + + return true; + } + + @Override + public boolean keyPressed( int key, int scancode, int modifiers ) + { + if( key == GLFW.GLFW_KEY_ESCAPE ) return false; + if( (modifiers & GLFW.GLFW_MOD_CONTROL) != 0 ) + { + switch( key ) + { + case GLFW.GLFW_KEY_T: + if( terminateTimer < 0 ) terminateTimer = 0; + return true; + case GLFW.GLFW_KEY_S: + if( shutdownTimer < 0 ) shutdownTimer = 0; + return true; + case GLFW.GLFW_KEY_R: + if( rebootTimer < 0 ) rebootTimer = 0; + return true; + + case GLFW.GLFW_KEY_V: + // Ctrl+V for paste + String clipboard = client.keyboard.getClipboard(); + if( clipboard != null ) + { + // Clip to the first occurrence of \r or \n + int newLineIndex1 = clipboard.indexOf( "\r" ); + int newLineIndex2 = clipboard.indexOf( "\n" ); + if( newLineIndex1 >= 0 && newLineIndex2 >= 0 ) + { + clipboard = clipboard.substring( 0, Math.min( newLineIndex1, newLineIndex2 ) ); + } + else if( newLineIndex1 >= 0 ) + { + clipboard = clipboard.substring( 0, newLineIndex1 ); + } + else if( newLineIndex2 >= 0 ) + { + clipboard = clipboard.substring( 0, newLineIndex2 ); + } + + // Filter the string + clipboard = SharedConstants.stripInvalidChars( clipboard ); + if( !clipboard.isEmpty() ) + { + // Clip to 512 characters and queue the event + if( clipboard.length() > 512 ) clipboard = clipboard.substring( 0, 512 ); + queueEvent( "paste", clipboard ); + } + + return true; + } + } + } + + if( key >= 0 && terminateTimer < 0 && rebootTimer < 0 && shutdownTimer < 0 ) + { + // Queue the "key" event and add to the down set + boolean repeat = keysDown.get( key ); + keysDown.set( key ); + IComputer computer = this.computer.get(); + if( computer != null ) computer.keyDown( key, repeat ); + } + + return true; + } + + @Override + public boolean keyReleased( int key, int scancode, int modifiers ) + { + // Queue the "key_up" event and remove from the down set + if( key >= 0 && keysDown.get( key ) ) + { + keysDown.set( key, false ); + IComputer computer = this.computer.get(); + if( computer != null ) computer.keyUp( key ); + } + + switch( key ) + { + case GLFW.GLFW_KEY_T: + terminateTimer = -1; + break; + case GLFW.GLFW_KEY_R: + rebootTimer = -1; + break; + case GLFW.GLFW_KEY_S: + shutdownTimer = -1; + break; + case GLFW.GLFW_KEY_LEFT_CONTROL: + case GLFW.GLFW_KEY_RIGHT_CONTROL: + terminateTimer = rebootTimer = shutdownTimer = -1; + break; + } + + return true; + } + + @Override + public boolean mouseClicked( double mouseX, double mouseY, int button ) + { + ClientComputer computer = this.computer.get(); + if( computer == null || !computer.isColour() || button < 0 || button > 2 ) return false; + + Terminal term = computer.getTerminal(); + if( term != null ) + { + int charX = (int) (mouseX / FONT_WIDTH); + int charY = (int) (mouseY / FONT_HEIGHT); + charX = Math.min( Math.max( charX, 0 ), term.getWidth() - 1 ); + charY = Math.min( Math.max( charY, 0 ), term.getHeight() - 1 ); + + computer.mouseClick( button + 1, charX + 1, charY + 1 ); + + lastMouseButton = button; + lastMouseX = charX; + lastMouseY = charY; + } + + return true; + } + + @Override + public boolean mouseReleased( double mouseX, double mouseY, int button ) + { + ClientComputer computer = this.computer.get(); + if( computer == null || !computer.isColour() || button < 0 || button > 2 ) return false; + + Terminal term = computer.getTerminal(); + if( term != null ) + { + int charX = (int) (mouseX / FONT_WIDTH); + int charY = (int) (mouseY / FONT_HEIGHT); + charX = Math.min( Math.max( charX, 0 ), term.getWidth() - 1 ); + charY = Math.min( Math.max( charY, 0 ), term.getHeight() - 1 ); + + if( lastMouseButton == button ) + { + computer.mouseUp( lastMouseButton + 1, charX + 1, charY + 1 ); + lastMouseButton = -1; + } + + lastMouseX = charX; + lastMouseY = charY; + } + + return false; + } + + @Override + public boolean mouseDragged( double mouseX, double mouseY, int button, double v2, double v3 ) + { + ClientComputer computer = this.computer.get(); + if( computer == null || !computer.isColour() || button < 0 || button > 2 ) return false; + + Terminal term = computer.getTerminal(); + if( term != null ) + { + int charX = (int) (mouseX / FONT_WIDTH); + int charY = (int) (mouseY / FONT_HEIGHT); + charX = Math.min( Math.max( charX, 0 ), term.getWidth() - 1 ); + charY = Math.min( Math.max( charY, 0 ), term.getHeight() - 1 ); + + if( button == lastMouseButton && (charX != lastMouseX || charY != lastMouseY) ) + { + computer.mouseDrag( button + 1, charX + 1, charY + 1 ); + lastMouseX = charX; + lastMouseY = charY; + } + } + + return false; + } + + @Override + public boolean mouseScrolled( double mouseX, double mouseY, double delta ) + { + ClientComputer computer = this.computer.get(); + if( computer == null || !computer.isColour() || delta == 0 ) return false; + + Terminal term = computer.getTerminal(); + if( term != null ) + { + int charX = (int) (mouseX / FONT_WIDTH); + int charY = (int) (mouseY / FONT_HEIGHT); + charX = Math.min( Math.max( charX, 0 ), term.getWidth() - 1 ); + charY = Math.min( Math.max( charY, 0 ), term.getHeight() - 1 ); + + computer.mouseScroll( delta < 0 ? 1 : -1, charX + 1, charY + 1 ); + + lastMouseX = charX; + lastMouseY = charY; + } + + return true; + } + + public void update() + { + if( terminateTimer >= 0 && terminateTimer < TERMINATE_TIME && (terminateTimer += 0.05f) > TERMINATE_TIME ) + { + queueEvent( "terminate" ); + } + + if( shutdownTimer >= 0 && shutdownTimer < TERMINATE_TIME && (shutdownTimer += 0.05f) > TERMINATE_TIME ) + { + ClientComputer computer = this.computer.get(); + if( computer != null ) computer.shutdown(); + } + + if( rebootTimer >= 0 && rebootTimer < TERMINATE_TIME && (rebootTimer += 0.05f) > TERMINATE_TIME ) + { + ClientComputer computer = this.computer.get(); + if( computer != null ) computer.reboot(); + } + } + + @Override + public boolean changeFocus( boolean reversed ) + { + if( focused ) + { + // When blurring, we should make all keys go up + for( int key = 0; key < keysDown.size(); key++ ) + { + if( keysDown.get( key ) ) queueEvent( "key_up", key ); + } + keysDown.clear(); + + // When blurring, we should make the last mouse button go up + if( lastMouseButton > 0 ) + { + IComputer computer = this.computer.get(); + if( computer != null ) computer.mouseUp( lastMouseButton + 1, lastMouseX + 1, lastMouseY + 1 ); + lastMouseButton = -1; + } + + shutdownTimer = terminateTimer = rebootTimer = -1; + } + focused = !focused; + return true; + } + + public void draw( int originX, int originY ) + { + synchronized( computer ) + { + // Draw the screen contents + ClientComputer computer = this.computer.get(); + Terminal terminal = computer != null ? computer.getTerminal() : null; + if( terminal != null ) + { + FixedWidthFontRenderer.drawTerminal( originX, originY, terminal, !computer.isColour(), topMargin, bottomMargin, leftMargin, rightMargin ); + } + else + { + FixedWidthFontRenderer.drawEmptyTerminal( + originX - leftMargin, originY - rightMargin, + termWidth * FONT_WIDTH + leftMargin + rightMargin, + termHeight * FONT_HEIGHT + topMargin + bottomMargin + ); + } + } + } + + private void queueEvent( String event ) + { + ClientComputer computer = this.computer.get(); + if( computer != null ) computer.queueEvent( event ); + } + + private void queueEvent( String event, Object... args ) + { + ClientComputer computer = this.computer.get(); + if( computer != null ) computer.queueEvent( event, args ); + } + + @Override + public boolean isMouseOver( double x, double y ) + { + return true; + } +} diff --git a/src/main/java/dan200/computercraft/client/gui/widgets/WidgetWrapper.java b/src/main/java/dan200/computercraft/client/gui/widgets/WidgetWrapper.java new file mode 100644 index 000000000..c50e60db2 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/widgets/WidgetWrapper.java @@ -0,0 +1,105 @@ +/* + * 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.client.gui.widgets; + +import net.minecraft.client.gui.Element; + +public class WidgetWrapper implements Element +{ + private final Element listener; + private final int x; + private final int y; + private final int width; + private final int height; + + public WidgetWrapper( Element listener, int x, int y, int width, int height ) + { + this.listener = listener; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public boolean changeFocus( boolean b ) + { + return listener.changeFocus( b ); + } + + @Override + public boolean mouseClicked( double x, double y, int button ) + { + double dx = x - this.x, dy = y - this.y; + return dx >= 0 && dx < width && dy >= 0 && dy < height && listener.mouseClicked( dx, dy, button ); + } + + @Override + public boolean mouseReleased( double x, double y, int button ) + { + double dx = x - this.x, dy = y - this.y; + return dx >= 0 && dx < width && dy >= 0 && dy < height && listener.mouseReleased( dx, dy, button ); + } + + @Override + public boolean mouseDragged( double x, double y, int button, double deltaX, double deltaY ) + { + double dx = x - this.x, dy = y - this.y; + return dx >= 0 && dx < width && dy >= 0 && dy < height && listener.mouseDragged( dx, dy, button, deltaX, deltaY ); + } + + @Override + public boolean mouseScrolled( double x, double y, double delta ) + { + double dx = x - this.x, dy = y - this.y; + return dx >= 0 && dx < width && dy >= 0 && dy < height && listener.mouseScrolled( dx, dy, delta ); + } + + @Override + public boolean keyPressed( int key, int scancode, int modifiers ) + { + return listener.keyPressed( key, scancode, modifiers ); + } + + @Override + public boolean keyReleased( int key, int scancode, int modifiers ) + { + return listener.keyReleased( key, scancode, modifiers ); + } + + @Override + public boolean charTyped( char character, int modifiers ) + { + return listener.charTyped( character, modifiers ); + } + + public int getX() + { + return x; + } + + public int getY() + { + return y; + } + + public int getWidth() + { + return width; + } + + public int getHeight() + { + return height; + } + + @Override + public boolean isMouseOver( double x, double y ) + { + double dx = x - this.x, dy = y - this.y; + return dx >= 0 && dx < width && dy >= 0 && dy < height; + } +} diff --git a/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java b/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java new file mode 100644 index 000000000..a1f401e0b --- /dev/null +++ b/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java @@ -0,0 +1,109 @@ +/* + * 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.client.proxy; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.gui.*; +import dan200.computercraft.client.render.TileEntityMonitorRenderer; +import dan200.computercraft.client.render.TileEntityTurtleRenderer; +import dan200.computercraft.client.render.TurtlePlayerRenderer; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.common.IColouredItem; +import dan200.computercraft.shared.computer.inventory.ContainerComputer; +import dan200.computercraft.shared.computer.inventory.ContainerViewComputer; +import dan200.computercraft.shared.peripheral.monitor.ClientMonitor; +import dan200.computercraft.shared.pocket.inventory.ContainerPocketComputer; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import net.minecraft.client.gui.screen.ingame.HandledScreens; +import net.minecraft.client.item.ModelPredicateProvider; +import net.minecraft.client.item.ModelPredicateProviderRegistry; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderLayers; +import net.minecraft.item.Item; +import net.minecraft.util.Identifier; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.client.registry.ClientRegistry; +import net.minecraftforge.fml.client.registry.RenderingRegistry; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; + +import java.util.function.Supplier; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD ) +public final class ComputerCraftProxyClient +{ + @SubscribeEvent + public static void setupClient( FMLClientSetupEvent event ) + { + registerContainers(); + + // While turtles themselves are not transparent, their upgrades may be. + RenderLayers.setRenderLayer( Registry.ModBlocks.TURTLE_NORMAL.get(), RenderLayer.getTranslucent() ); + RenderLayers.setRenderLayer( Registry.ModBlocks.TURTLE_ADVANCED.get(), RenderLayer.getTranslucent() ); + + // Monitors' textures have transparent fronts and so count as cutouts. + RenderLayers.setRenderLayer( Registry.ModBlocks.MONITOR_NORMAL.get(), RenderLayer.getCutout() ); + RenderLayers.setRenderLayer( Registry.ModBlocks.MONITOR_ADVANCED.get(), RenderLayer.getCutout() ); + + // Setup TESRs + ClientRegistry.bindTileEntityRenderer( Registry.ModTiles.MONITOR_NORMAL.get(), TileEntityMonitorRenderer::new ); + ClientRegistry.bindTileEntityRenderer( Registry.ModTiles.MONITOR_ADVANCED.get(), TileEntityMonitorRenderer::new ); + ClientRegistry.bindTileEntityRenderer( Registry.ModTiles.TURTLE_NORMAL.get(), TileEntityTurtleRenderer::new ); + ClientRegistry.bindTileEntityRenderer( Registry.ModTiles.TURTLE_ADVANCED.get(), TileEntityTurtleRenderer::new ); + // TODO: ClientRegistry.bindTileEntityRenderer( TileCable.FACTORY, x -> new TileEntityCableRenderer() ); + + RenderingRegistry.registerEntityRenderingHandler( Registry.ModEntities.TURTLE_PLAYER.get(), TurtlePlayerRenderer::new ); + + registerItemProperty( "state", + ( stack, world, player ) -> ItemPocketComputer.getState( stack ).ordinal(), + Registry.ModItems.POCKET_COMPUTER_NORMAL, Registry.ModItems.POCKET_COMPUTER_ADVANCED + ); + registerItemProperty( "state", + ( stack, world, player ) -> IColouredItem.getColourBasic( stack ) != -1 ? 1 : 0, + Registry.ModItems.POCKET_COMPUTER_NORMAL, Registry.ModItems.POCKET_COMPUTER_ADVANCED + ); + } + + @SafeVarargs + private static void registerItemProperty( String name, ModelPredicateProvider getter, Supplier... items ) + { + Identifier id = new Identifier( ComputerCraft.MOD_ID, name ); + for( Supplier item : items ) + { + ModelPredicateProviderRegistry.register( item.get(), id, getter ); + } + } + + private static void registerContainers() + { + // My IDE doesn't think so, but we do actually need these generics. + + HandledScreens.>register( Registry.ModContainers.COMPUTER.get(), GuiComputer::create ); + HandledScreens.>register( Registry.ModContainers.POCKET_COMPUTER.get(), GuiComputer::createPocket ); + HandledScreens.register( Registry.ModContainers.TURTLE.get(), GuiTurtle::new ); + + HandledScreens.register( Registry.ModContainers.PRINTER.get(), GuiPrinter::new ); + HandledScreens.register( Registry.ModContainers.DISK_DRIVE.get(), GuiDiskDrive::new ); + HandledScreens.register( Registry.ModContainers.PRINTOUT.get(), GuiPrintout::new ); + + HandledScreens.>register( Registry.ModContainers.VIEW_COMPUTER.get(), GuiComputer::createView ); + } + + @Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT ) + public static final class ForgeHandlers + { + @SubscribeEvent + public static void onWorldUnload( WorldEvent.Unload event ) + { + if( event.getWorld().isClient() ) + { + ClientMonitor.destroyAll(); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/client/render/CableHighlightRenderer.java b/src/main/java/dan200/computercraft/client/render/CableHighlightRenderer.java new file mode 100644 index 000000000..d80d03ae0 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/CableHighlightRenderer.java @@ -0,0 +1,78 @@ +/* + * 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.client.render; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.peripheral.modem.wired.BlockCable; +import dan200.computercraft.shared.peripheral.modem.wired.CableShapes; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.entity.Entity; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.World; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.DrawHighlightEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT ) +public final class CableHighlightRenderer +{ + private CableHighlightRenderer() + { + } + + /** + * Draw an outline for a specific part of a cable "Multipart". + * + * @param event The event to observe + * @see WorldRenderer#drawSelectionBox(MatrixStack, IVertexBuilder, Entity, double, double, double, BlockPos, BlockState) + */ + @SubscribeEvent + public static void drawHighlight( DrawHighlightEvent.HighlightBlock event ) + { + BlockHitResult hit = event.getTarget(); + BlockPos pos = hit.getBlockPos(); + World world = event.getInfo().getFocusedEntity().getEntityWorld(); + Camera info = event.getInfo(); + + BlockState state = world.getBlockState( pos ); + + // We only care about instances with both cable and modem. + if( state.getBlock() != Registry.ModBlocks.CABLE.get() || state.get( BlockCable.MODEM ).getFacing() == null || !state.get( BlockCable.CABLE ) ) + { + return; + } + + event.setCanceled( true ); + + VoxelShape shape = WorldUtil.isVecInside( CableShapes.getModemShape( state ), hit.getPos().subtract( pos.getX(), pos.getY(), pos.getZ() ) ) + ? CableShapes.getModemShape( state ) + : CableShapes.getCableShape( state ); + + Vec3d cameraPos = info.getPos(); + double xOffset = pos.getX() - cameraPos.getX(); + double yOffset = pos.getY() - cameraPos.getY(); + double zOffset = pos.getZ() - cameraPos.getZ(); + + VertexConsumer buffer = event.getBuffers().getBuffer( RenderLayer.getLines() ); + Matrix4f matrix4f = event.getMatrix().peek().getModel(); + shape.forEachEdge( ( x1, y1, z1, x2, y2, z2 ) -> { + buffer.vertex( matrix4f, (float) (x1 + xOffset), (float) (y1 + yOffset), (float) (z1 + zOffset) ) + .color( 0, 0, 0, 0.4f ).next(); + buffer.vertex( matrix4f, (float) (x2 + xOffset), (float) (y2 + yOffset), (float) (z2 + zOffset) ) + .color( 0, 0, 0, 0.4f ).next(); + } ); + } +} diff --git a/src/main/java/dan200/computercraft/client/render/ComputerBorderRenderer.java b/src/main/java/dan200/computercraft/client/render/ComputerBorderRenderer.java new file mode 100644 index 000000000..2df721caa --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/ComputerBorderRenderer.java @@ -0,0 +1,175 @@ +/* + * 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.client.render; + +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import org.lwjgl.opengl.GL11; + +import javax.annotation.Nonnull; +import net.minecraft.client.render.BufferBuilder; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Matrix4f; + +public class ComputerBorderRenderer +{ + public static final Identifier BACKGROUND_NORMAL = new Identifier( ComputerCraft.MOD_ID, "textures/gui/corners_normal.png" ); + public static final Identifier BACKGROUND_ADVANCED = new Identifier( ComputerCraft.MOD_ID, "textures/gui/corners_advanced.png" ); + public static final Identifier BACKGROUND_COMMAND = new Identifier( ComputerCraft.MOD_ID, "textures/gui/corners_command.png" ); + public static final Identifier BACKGROUND_COLOUR = new Identifier( ComputerCraft.MOD_ID, "textures/gui/corners_colour.png" ); + + private static final Matrix4f IDENTITY = new Matrix4f(); + + static + { + IDENTITY.loadIdentity(); + } + + /** + * The margin between the terminal and its border. + */ + public static final int MARGIN = 2; + + /** + * The width of the terminal border. + */ + public static final int BORDER = 12; + + private static final int CORNER_TOP_Y = 28; + private static final int CORNER_BOTTOM_Y = CORNER_TOP_Y + BORDER; + private static final int CORNER_LEFT_X = BORDER; + private static final int CORNER_RIGHT_X = CORNER_LEFT_X + BORDER; + private static final int BORDER_RIGHT_X = 36; + private static final int GAP = 4; + + private static final float TEX_SCALE = 1 / 256.0f; + + private final Matrix4f transform; + private final VertexConsumer builder; + private final int z; + private final float r, g, b; + + public ComputerBorderRenderer( Matrix4f transform, VertexConsumer builder, int z, float r, float g, float b ) + { + this.transform = transform; + this.builder = builder; + this.z = z; + this.r = r; + this.g = g; + this.b = b; + } + + + @Nonnull + public static Identifier getTexture( @Nonnull ComputerFamily family ) + { + switch( family ) + { + case NORMAL: + default: + return BACKGROUND_NORMAL; + case ADVANCED: + return BACKGROUND_ADVANCED; + case COMMAND: + return BACKGROUND_COMMAND; + } + } + + public static void render( int x, int y, int z, int width, int height ) + { + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder buffer = tessellator.getBuffer(); + buffer.begin( GL11.GL_QUADS, VertexFormats.POSITION_COLOR_TEXTURE ); + + render( IDENTITY, buffer, x, y, z, width, height ); + + RenderSystem.enableAlphaTest(); + tessellator.draw(); + } + + public static void render( Matrix4f transform, VertexConsumer buffer, int x, int y, int z, int width, int height ) + { + render( transform, buffer, x, y, z, width, height, 1, 1, 1 ); + } + + public static void render( Matrix4f transform, VertexConsumer buffer, int x, int y, int z, int width, int height, float r, float g, float b ) + { + render( transform, buffer, x, y, z, width, height, 0, r, g, b ); + } + + public static void render( Matrix4f transform, VertexConsumer buffer, int x, int y, int z, int width, int height, int borderHeight, float r, float g, float b ) + { + new ComputerBorderRenderer( transform, buffer, z, r, g, b ).doRender( x, y, width, height, borderHeight ); + } + + public void doRender( int x, int y, int width, int height, int bottomHeight ) + { + int endX = x + width; + int endY = y + height; + + // Vertical bars + renderLine( x - BORDER, y, 0, CORNER_TOP_Y, BORDER, endY - y ); + renderLine( endX, y, BORDER_RIGHT_X, CORNER_TOP_Y, BORDER, endY - y ); + + // Top bar + renderLine( x, y - BORDER, 0, 0, endX - x, BORDER ); + renderCorner( x - BORDER, y - BORDER, CORNER_LEFT_X, CORNER_TOP_Y ); + renderCorner( endX, y - BORDER, CORNER_RIGHT_X, CORNER_TOP_Y ); + + // Bottom bar. We allow for drawing a stretched version, which allows for additional elements (such as the + // pocket computer's lights). + if( bottomHeight <= 0 ) + { + renderLine( x, endY, 0, BORDER, endX - x, BORDER ); + renderCorner( x - BORDER, endY, CORNER_LEFT_X, CORNER_BOTTOM_Y ); + renderCorner( endX, endY, CORNER_RIGHT_X, CORNER_BOTTOM_Y ); + } + else + { + // Bottom left, middle, right. We do this in three portions: the top inner corners, an extended region for + // lights, and then the bottom outer corners. + renderTexture( x - BORDER, endY, CORNER_LEFT_X, CORNER_BOTTOM_Y, BORDER, BORDER / 2 ); + renderTexture( x, endY, 0, BORDER, width, BORDER / 2, BORDER, BORDER / 2 ); + renderTexture( endX, endY, CORNER_RIGHT_X, CORNER_BOTTOM_Y, BORDER, BORDER / 2 ); + + renderTexture( x - BORDER, endY + BORDER / 2, CORNER_LEFT_X, CORNER_BOTTOM_Y + GAP, BORDER, bottomHeight, BORDER, GAP ); + renderTexture( x, endY + BORDER / 2, 0, BORDER + GAP, width, bottomHeight, BORDER, GAP ); + renderTexture( endX, endY + BORDER / 2, CORNER_RIGHT_X, CORNER_BOTTOM_Y + GAP, BORDER, bottomHeight, BORDER, GAP ); + + renderTexture( x - BORDER, endY + bottomHeight + BORDER / 2, CORNER_LEFT_X, CORNER_BOTTOM_Y + BORDER / 2, BORDER, BORDER / 2 ); + renderTexture( x, endY + bottomHeight + BORDER / 2, 0, BORDER + BORDER / 2, width, BORDER / 2 ); + renderTexture( endX, endY + bottomHeight + BORDER / 2, CORNER_RIGHT_X, CORNER_BOTTOM_Y + BORDER / 2, BORDER, BORDER / 2 ); + } + } + + private void renderCorner( int x, int y, int u, int v ) + { + renderTexture( x, y, u, v, BORDER, BORDER, BORDER, BORDER ); + } + + private void renderLine( int x, int y, int u, int v, int width, int height ) + { + renderTexture( x, y, u, v, width, height, BORDER, BORDER ); + } + + private void renderTexture( int x, int y, int u, int v, int width, int height ) + { + renderTexture( x, y, u, v, width, height, width, height ); + } + + private void renderTexture( int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight ) + { + builder.vertex( transform, x, y + height, z ).color( r, g, b, 1.0f ).texture( u * TEX_SCALE, (v + textureHeight) * TEX_SCALE ).next(); + builder.vertex( transform, x + width, y + height, z ).color( r, g, b, 1.0f ).texture( (u + textureWidth) * TEX_SCALE, (v + textureHeight) * TEX_SCALE ).next(); + builder.vertex( transform, x + width, y, z ).color( r, g, b, 1.0f ).texture( (u + textureWidth) * TEX_SCALE, v * TEX_SCALE ).next(); + builder.vertex( transform, x, y, z ).color( r, g, b, 1.0f ).texture( u * TEX_SCALE, v * TEX_SCALE ).next(); + } +} diff --git a/src/main/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java b/src/main/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java new file mode 100644 index 000000000..fc6a8b07d --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java @@ -0,0 +1,138 @@ +/* + * 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.client.render; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.item.HeldItemRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.client.util.math.Vector3f; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Arm; +import net.minecraft.util.Hand; +import net.minecraft.util.math.MathHelper; + +public abstract class ItemMapLikeRenderer +{ + /** + * The main rendering method for the item. + * + * @param transform The matrix transformation stack + * @param render The buffer to render to + * @param stack The stack to render + * @see FirstPersonRenderer#renderItemInFirstPerson(AbstractClientPlayerEntity, float, float, Hand, float, ItemStack, float, MatrixStack, IRenderTypeBuffer, int) + */ + protected abstract void renderItem( MatrixStack transform, VertexConsumerProvider render, ItemStack stack ); + + protected void renderItemFirstPerson( MatrixStack transform, VertexConsumerProvider render, int lightTexture, Hand hand, float pitch, float equipProgress, float swingProgress, ItemStack stack ) + { + PlayerEntity player = MinecraftClient.getInstance().player; + + transform.push(); + if( hand == Hand.MAIN_HAND && player.getOffHandStack().isEmpty() ) + { + renderItemFirstPersonCenter( transform, render, lightTexture, pitch, equipProgress, swingProgress, stack ); + } + else + { + renderItemFirstPersonSide( + transform, render, lightTexture, + hand == Hand.MAIN_HAND ? player.getMainArm() : player.getMainArm().getOpposite(), + equipProgress, swingProgress, stack + ); + } + transform.pop(); + } + + /** + * Renders the item to one side of the player. + * + * @param transform The matrix transformation stack + * @param render The buffer to render to + * @param combinedLight The current light level + * @param side The side to render on + * @param equipProgress The equip progress of this item + * @param swingProgress The swing progress of this item + * @param stack The stack to render + * @see FirstPersonRenderer#renderMapFirstPersonSide(MatrixStack, IRenderTypeBuffer, int, float, HandSide, float, ItemStack) + */ + private void renderItemFirstPersonSide( MatrixStack transform, VertexConsumerProvider render, int combinedLight, Arm side, float equipProgress, float swingProgress, ItemStack stack ) + { + MinecraftClient minecraft = MinecraftClient.getInstance(); + float offset = side == Arm.RIGHT ? 1f : -1f; + transform.translate( offset * 0.125f, -0.125f, 0f ); + + // If the player is not invisible then render a single arm + if( !minecraft.player.isInvisible() ) + { + transform.push(); + transform.multiply( Vector3f.POSITIVE_Z.getDegreesQuaternion( offset * 10f ) ); + minecraft.getHeldItemRenderer().renderArmHoldingItem( transform, render, combinedLight, equipProgress, swingProgress, side ); + transform.pop(); + } + + // Setup the appropriate transformations. This is just copied from the + // corresponding method in ItemRenderer. + transform.push(); + transform.translate( offset * 0.51f, -0.08f + equipProgress * -1.2f, -0.75f ); + float f1 = MathHelper.sqrt( swingProgress ); + float f2 = MathHelper.sin( f1 * (float) Math.PI ); + float f3 = -0.5f * f2; + float f4 = 0.4f * MathHelper.sin( f1 * ((float) Math.PI * 2f) ); + float f5 = -0.3f * MathHelper.sin( swingProgress * (float) Math.PI ); + transform.translate( offset * f3, f4 - 0.3f * f2, f5 ); + transform.multiply( Vector3f.POSITIVE_X.getDegreesQuaternion( f2 * -45f ) ); + transform.multiply( Vector3f.POSITIVE_Y.getDegreesQuaternion( offset * f2 * -30f ) ); + + renderItem( transform, render, stack ); + + transform.pop(); + } + + /** + * Render an item in the middle of the screen. + * + * @param transform The matrix transformation stack + * @param render The buffer to render to + * @param combinedLight The current light level + * @param pitch The pitch of the player + * @param equipProgress The equip progress of this item + * @param swingProgress The swing progress of this item + * @param stack The stack to render + * @see FirstPersonRenderer#renderMapFirstPerson(MatrixStack, IRenderTypeBuffer, int, float, float, float) + */ + private void renderItemFirstPersonCenter( MatrixStack transform, VertexConsumerProvider render, int combinedLight, float pitch, float equipProgress, float swingProgress, ItemStack stack ) + { + MinecraftClient minecraft = MinecraftClient.getInstance(); + HeldItemRenderer renderer = minecraft.getHeldItemRenderer(); + + // Setup the appropriate transformations. This is just copied from the + // corresponding method in ItemRenderer. + float swingRt = MathHelper.sqrt( swingProgress ); + float tX = -0.2f * MathHelper.sin( swingProgress * (float) Math.PI ); + float tZ = -0.4f * MathHelper.sin( swingRt * (float) Math.PI ); + transform.translate( 0, -tX / 2, tZ ); + + float pitchAngle = renderer.getMapAngle( pitch ); + transform.translate( 0, 0.04F + equipProgress * -1.2f + pitchAngle * -0.5f, -0.72f ); + transform.multiply( Vector3f.POSITIVE_X.getDegreesQuaternion( pitchAngle * -85.0f ) ); + if( !minecraft.player.isInvisible() ) + { + transform.push(); + transform.multiply( Vector3f.POSITIVE_Y.getDegreesQuaternion( 90.0F ) ); + renderer.renderArm( transform, render, combinedLight, Arm.RIGHT ); + renderer.renderArm( transform, render, combinedLight, Arm.LEFT ); + transform.pop(); + } + + float rX = MathHelper.sin( swingRt * (float) Math.PI ); + transform.multiply( Vector3f.POSITIVE_X.getDegreesQuaternion( rX * 20.0F ) ); + transform.scale( 2.0F, 2.0F, 2.0F ); + + renderItem( transform, render, stack ); + } +} diff --git a/src/main/java/dan200/computercraft/client/render/ItemPocketRenderer.java b/src/main/java/dan200/computercraft/client/render/ItemPocketRenderer.java new file mode 100644 index 000000000..7da6ceec1 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/ItemPocketRenderer.java @@ -0,0 +1,158 @@ +/* + * 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.client.render; + +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.gui.FixedWidthFontRenderer; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.computer.core.ClientComputer; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import dan200.computercraft.shared.util.Colour; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.BufferBuilder; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.client.util.math.Vector3f; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.Matrix4f; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.lwjgl.opengl.GL11; + +import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_HEIGHT; +import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_WIDTH; +import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER; +import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN; + +/** + * Emulates map rendering for pocket computers. + */ +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT ) +public final class ItemPocketRenderer extends ItemMapLikeRenderer +{ + private static final int LIGHT_HEIGHT = 8; + + private static final ItemPocketRenderer INSTANCE = new ItemPocketRenderer(); + + private ItemPocketRenderer() + { + } + + @SubscribeEvent + public static void onRenderInHand( RenderHandEvent event ) + { + ItemStack stack = event.getItemStack(); + if( !(stack.getItem() instanceof ItemPocketComputer) ) return; + + event.setCanceled( true ); + INSTANCE.renderItemFirstPerson( + event.getMatrixStack(), event.getBuffers(), event.getLight(), + event.getHand(), event.getInterpolatedPitch(), event.getEquipProgress(), event.getSwingProgress(), event.getItemStack() + ); + } + + @Override + protected void renderItem( MatrixStack transform, VertexConsumerProvider render, ItemStack stack ) + { + ClientComputer computer = ItemPocketComputer.createClientComputer( stack ); + Terminal terminal = computer == null ? null : computer.getTerminal(); + + int termWidth, termHeight; + if( terminal == null ) + { + termWidth = ComputerCraft.pocketTermWidth; + termHeight = ComputerCraft.pocketTermHeight; + } + else + { + termWidth = terminal.getWidth(); + termHeight = terminal.getHeight(); + } + + int width = termWidth * FONT_WIDTH + MARGIN * 2; + int height = termHeight * FONT_HEIGHT + MARGIN * 2; + + // Setup various transformations. Note that these are partially adapted from the corresponding method + // in ItemRenderer + transform.push(); + transform.multiply( Vector3f.POSITIVE_Y.getDegreesQuaternion( 180f ) ); + transform.multiply( Vector3f.POSITIVE_Z.getDegreesQuaternion( 180f ) ); + transform.scale( 0.5f, 0.5f, 0.5f ); + + float scale = 0.75f / Math.max( width + BORDER * 2, height + BORDER * 2 + LIGHT_HEIGHT ); + transform.scale( scale, scale, 0 ); + transform.translate( -0.5 * width, -0.5 * height, 0 ); + + // Render the main frame + ItemPocketComputer item = (ItemPocketComputer) stack.getItem(); + ComputerFamily family = item.getFamily(); + int frameColour = item.getColour( stack ); + + Matrix4f matrix = transform.peek().getModel(); + renderFrame( matrix, family, frameColour, width, height ); + + // Render the light + int lightColour = ItemPocketComputer.getLightState( stack ); + if( lightColour == -1 ) lightColour = Colour.BLACK.getHex(); + renderLight( matrix, lightColour, width, height ); + + if( computer != null && terminal != null ) + { + FixedWidthFontRenderer.drawTerminal( matrix, MARGIN, MARGIN, terminal, !computer.isColour(), MARGIN, MARGIN, MARGIN, MARGIN ); + } + else + { + FixedWidthFontRenderer.drawEmptyTerminal( matrix, 0, 0, width, height ); + } + + transform.pop(); + } + + private static void renderFrame( Matrix4f transform, ComputerFamily family, int colour, int width, int height ) + { + MinecraftClient.getInstance().getTextureManager() + .bindTexture( colour != -1 ? ComputerBorderRenderer.BACKGROUND_COLOUR : ComputerBorderRenderer.getTexture( family ) ); + + float r = ((colour >>> 16) & 0xFF) / 255.0f; + float g = ((colour >>> 8) & 0xFF) / 255.0f; + float b = (colour & 0xFF) / 255.0f; + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder buffer = tessellator.getBuffer(); + buffer.begin( GL11.GL_QUADS, VertexFormats.POSITION_COLOR_TEXTURE ); + + ComputerBorderRenderer.render( transform, buffer, 0, 0, 0, width, height, LIGHT_HEIGHT, r, g, b ); + + tessellator.draw(); + } + + private static void renderLight( Matrix4f transform, int colour, int width, int height ) + { + RenderSystem.enableBlend(); + RenderSystem.disableTexture(); + + float r = ((colour >>> 16) & 0xFF) / 255.0f; + float g = ((colour >>> 8) & 0xFF) / 255.0f; + float b = (colour & 0xFF) / 255.0f; + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder buffer = tessellator.getBuffer(); + buffer.begin( GL11.GL_QUADS, VertexFormats.POSITION_COLOR ); + buffer.vertex( transform, width - LIGHT_HEIGHT * 2, height + LIGHT_HEIGHT + BORDER / 2.0f, 0 ).color( r, g, b, 1.0f ).next(); + buffer.vertex( transform, width, height + LIGHT_HEIGHT + BORDER / 2.0f, 0 ).color( r, g, b, 1.0f ).next(); + buffer.vertex( transform, width, height + BORDER / 2.0f, 0 ).color( r, g, b, 1.0f ).next(); + buffer.vertex( transform, width - LIGHT_HEIGHT * 2, height + BORDER / 2.0f, 0 ).color( r, g, b, 1.0f ).next(); + + tessellator.draw(); + RenderSystem.enableTexture(); + } +} diff --git a/src/main/java/dan200/computercraft/client/render/ItemPrintoutRenderer.java b/src/main/java/dan200/computercraft/client/render/ItemPrintoutRenderer.java new file mode 100644 index 000000000..42bea5a46 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/ItemPrintoutRenderer.java @@ -0,0 +1,113 @@ +/* + * 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.client.render; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.media.items.ItemPrintout; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.client.util.math.Vector3f; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.Matrix4f; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.client.event.RenderItemInFrameEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_HEIGHT; +import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_WIDTH; +import static dan200.computercraft.client.render.PrintoutRenderer.*; +import static dan200.computercraft.shared.media.items.ItemPrintout.LINES_PER_PAGE; +import static dan200.computercraft.shared.media.items.ItemPrintout.LINE_MAX_LENGTH; + +/** + * Emulates map and item-frame rendering for printouts. + */ +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT ) +public final class ItemPrintoutRenderer extends ItemMapLikeRenderer +{ + private static final ItemPrintoutRenderer INSTANCE = new ItemPrintoutRenderer(); + + private ItemPrintoutRenderer() + { + } + + @SubscribeEvent + public static void onRenderInHand( RenderHandEvent event ) + { + ItemStack stack = event.getItemStack(); + if( !(stack.getItem() instanceof ItemPrintout) ) return; + + event.setCanceled( true ); + INSTANCE.renderItemFirstPerson( + event.getMatrixStack(), event.getBuffers(), event.getLight(), + event.getHand(), event.getInterpolatedPitch(), event.getEquipProgress(), event.getSwingProgress(), event.getItemStack() + ); + } + + @Override + protected void renderItem( MatrixStack transform, VertexConsumerProvider render, ItemStack stack ) + { + transform.multiply( Vector3f.POSITIVE_X.getDegreesQuaternion( 180f ) ); + transform.scale( 0.42f, 0.42f, -0.42f ); + transform.translate( -0.5f, -0.48f, 0.0f ); + + drawPrintout( transform, render, stack ); + } + + @SubscribeEvent + public static void onRenderInFrame( RenderItemInFrameEvent event ) + { + ItemStack stack = event.getItem(); + if( !(stack.getItem() instanceof ItemPrintout) ) return; + event.setCanceled( true ); + + MatrixStack transform = event.getMatrix(); + + // Move a little bit forward to ensure we're not clipping with the frame + transform.translate( 0.0f, 0.0f, -0.001f ); + transform.multiply( Vector3f.POSITIVE_Z.getDegreesQuaternion( 180f ) ); + transform.scale( 0.95f, 0.95f, -0.95f ); + transform.translate( -0.5f, -0.5f, 0.0f ); + + drawPrintout( transform, event.getBuffers(), stack ); + } + + private static void drawPrintout( MatrixStack transform, VertexConsumerProvider render, ItemStack stack ) + { + int pages = ItemPrintout.getPageCount( stack ); + boolean book = ((ItemPrintout) stack.getItem()).getType() == ItemPrintout.Type.BOOK; + + double width = LINE_MAX_LENGTH * FONT_WIDTH + X_TEXT_MARGIN * 2; + double height = LINES_PER_PAGE * FONT_HEIGHT + Y_TEXT_MARGIN * 2; + + // Non-books will be left aligned + if( !book ) width += offsetAt( pages ); + + double visualWidth = width, visualHeight = height; + + // Meanwhile books will be centred + if( book ) + { + visualWidth += 2 * COVER_SIZE + 2 * offsetAt( pages ); + visualHeight += 2 * COVER_SIZE; + } + + double max = Math.max( visualHeight, visualWidth ); + + // Scale the printout to fit correctly. + float scale = (float) (1.0 / max); + transform.scale( scale, scale, scale ); + transform.translate( (max - width) / 2.0, (max - height) / 2.0, 0.0 ); + + Matrix4f matrix = transform.peek().getModel(); + drawBorder( matrix, render, 0, 0, -0.01f, 0, pages, book ); + drawText( matrix, render, + X_TEXT_MARGIN, Y_TEXT_MARGIN, 0, ItemPrintout.getText( stack ), ItemPrintout.getColours( stack ) + ); + } +} diff --git a/src/main/java/dan200/computercraft/client/render/MonitorHighlightRenderer.java b/src/main/java/dan200/computercraft/client/render/MonitorHighlightRenderer.java new file mode 100644 index 000000000..019cc70a3 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/MonitorHighlightRenderer.java @@ -0,0 +1,96 @@ +/* + * 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.client.render; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.peripheral.monitor.TileMonitor; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.DrawHighlightEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.EnumSet; + +import static net.minecraft.util.math.Direction.*; + +/** + * Overrides monitor highlighting to only render the outline of the whole monitor, rather than the current + * block. This means you do not get an intrusive outline on top of the screen. + */ +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT ) +public final class MonitorHighlightRenderer +{ + private MonitorHighlightRenderer() + { + } + + @SubscribeEvent + public static void drawHighlight( DrawHighlightEvent.HighlightBlock event ) + { + // Preserve normal behaviour when crouching. + if( event.getInfo().getFocusedEntity().isInSneakingPose() ) return; + + World world = event.getInfo().getFocusedEntity().getEntityWorld(); + BlockPos pos = event.getTarget().getBlockPos(); + + BlockEntity tile = world.getBlockEntity( pos ); + if( !(tile instanceof TileMonitor) ) return; + + TileMonitor monitor = (TileMonitor) tile; + event.setCanceled( true ); + + // Determine which sides are part of the external faces of the monitor, and so which need to be rendered. + EnumSet faces = EnumSet.allOf( Direction.class ); + Direction front = monitor.getFront(); + faces.remove( front ); + if( monitor.getXIndex() != 0 ) faces.remove( monitor.getRight().getOpposite() ); + if( monitor.getXIndex() != monitor.getWidth() - 1 ) faces.remove( monitor.getRight() ); + if( monitor.getYIndex() != 0 ) faces.remove( monitor.getDown().getOpposite() ); + if( monitor.getYIndex() != monitor.getHeight() - 1 ) faces.remove( monitor.getDown() ); + + MatrixStack transformStack = event.getMatrix(); + Vec3d cameraPos = event.getInfo().getPos(); + transformStack.push(); + transformStack.translate( pos.getX() - cameraPos.getX(), pos.getY() - cameraPos.getY(), pos.getZ() - cameraPos.getZ() ); + + // I wish I could think of a better way to do this + VertexConsumer buffer = event.getBuffers().getBuffer( RenderLayer.getLines() ); + Matrix4f transform = transformStack.peek().getModel(); + if( faces.contains( NORTH ) || faces.contains( WEST ) ) line( buffer, transform, 0, 0, 0, UP ); + if( faces.contains( SOUTH ) || faces.contains( WEST ) ) line( buffer, transform, 0, 0, 1, UP ); + if( faces.contains( NORTH ) || faces.contains( EAST ) ) line( buffer, transform, 1, 0, 0, UP ); + if( faces.contains( SOUTH ) || faces.contains( EAST ) ) line( buffer, transform, 1, 0, 1, UP ); + if( faces.contains( NORTH ) || faces.contains( DOWN ) ) line( buffer, transform, 0, 0, 0, EAST ); + if( faces.contains( SOUTH ) || faces.contains( DOWN ) ) line( buffer, transform, 0, 0, 1, EAST ); + if( faces.contains( NORTH ) || faces.contains( UP ) ) line( buffer, transform, 0, 1, 0, EAST ); + if( faces.contains( SOUTH ) || faces.contains( UP ) ) line( buffer, transform, 0, 1, 1, EAST ); + if( faces.contains( WEST ) || faces.contains( DOWN ) ) line( buffer, transform, 0, 0, 0, SOUTH ); + if( faces.contains( EAST ) || faces.contains( DOWN ) ) line( buffer, transform, 1, 0, 0, SOUTH ); + if( faces.contains( WEST ) || faces.contains( UP ) ) line( buffer, transform, 0, 1, 0, SOUTH ); + if( faces.contains( EAST ) || faces.contains( UP ) ) line( buffer, transform, 1, 1, 0, SOUTH ); + + transformStack.pop(); + } + + private static void line( VertexConsumer buffer, Matrix4f transform, float x, float y, float z, Direction direction ) + { + buffer.vertex( transform, x, y, z ).color( 0, 0, 0, 0.4f ).next(); + buffer.vertex( transform, + x + direction.getOffsetX(), + y + direction.getOffsetY(), + z + direction.getOffsetZ() + ).color( 0, 0, 0, 0.4f ).next(); + } +} diff --git a/src/main/java/dan200/computercraft/client/render/MonitorTextureBufferShader.java b/src/main/java/dan200/computercraft/client/render/MonitorTextureBufferShader.java new file mode 100644 index 000000000..70fa464c9 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/MonitorTextureBufferShader.java @@ -0,0 +1,164 @@ +/* + * 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.client.render; + +import com.google.common.base.Strings; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.gui.FixedWidthFontRenderer; +import dan200.computercraft.shared.util.Palette; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL20; + +import java.io.InputStream; +import java.nio.FloatBuffer; +import net.minecraft.client.texture.TextureUtil; +import net.minecraft.util.math.Matrix4f; + +class MonitorTextureBufferShader +{ + static final int TEXTURE_INDEX = GL13.GL_TEXTURE3; + + private static final FloatBuffer MATRIX_BUFFER = BufferUtils.createFloatBuffer( 16 ); + private static final FloatBuffer PALETTE_BUFFER = BufferUtils.createFloatBuffer( 16 * 3 ); + + private static int uniformMv; + + private static int uniformFont; + private static int uniformWidth; + private static int uniformHeight; + private static int uniformTbo; + private static int uniformPalette; + + private static boolean initialised; + private static boolean ok; + private static int program; + + static void setupUniform( Matrix4f transform, int width, int height, Palette palette, boolean greyscale ) + { + MATRIX_BUFFER.rewind(); + transform.writeToBuffer( MATRIX_BUFFER ); + MATRIX_BUFFER.rewind(); + RenderSystem.glUniformMatrix4( uniformMv, false, MATRIX_BUFFER ); + + RenderSystem.glUniform1i( uniformWidth, width ); + RenderSystem.glUniform1i( uniformHeight, height ); + + // TODO: Cache this? Maybe?? + PALETTE_BUFFER.rewind(); + for( int i = 0; i < 16; i++ ) + { + double[] colour = palette.getColour( i ); + if( greyscale ) + { + float f = FixedWidthFontRenderer.toGreyscale( colour ); + PALETTE_BUFFER.put( f ).put( f ).put( f ); + } + else + { + PALETTE_BUFFER.put( (float) colour[0] ).put( (float) colour[1] ).put( (float) colour[2] ); + } + } + PALETTE_BUFFER.flip(); + RenderSystem.glUniform3( uniformPalette, PALETTE_BUFFER ); + } + + static boolean use() + { + if( initialised ) + { + if( ok ) GlStateManager.useProgram( program ); + return ok; + } + + if( ok = load() ) + { + GL20.glUseProgram( program ); + RenderSystem.glUniform1i( uniformFont, 0 ); + RenderSystem.glUniform1i( uniformTbo, TEXTURE_INDEX - GL13.GL_TEXTURE0 ); + } + + return ok; + } + + private static boolean load() + { + initialised = true; + + try + { + int vertexShader = loadShader( GL20.GL_VERTEX_SHADER, "assets/computercraft/shaders/monitor.vert" ); + int fragmentShader = loadShader( GL20.GL_FRAGMENT_SHADER, "assets/computercraft/shaders/monitor.frag" ); + + program = GlStateManager.createProgram(); + GlStateManager.attachShader( program, vertexShader ); + GlStateManager.attachShader( program, fragmentShader ); + GL20.glBindAttribLocation( program, 0, "v_pos" ); + + GlStateManager.linkProgram( program ); + boolean ok = GlStateManager.getProgram( program, GL20.GL_LINK_STATUS ) != 0; + String log = GlStateManager.getProgramInfoLog( program, Short.MAX_VALUE ).trim(); + if( !Strings.isNullOrEmpty( log ) ) + { + ComputerCraft.log.warn( "Problems when linking monitor shader: {}", log ); + } + + GL20.glDetachShader( program, vertexShader ); + GL20.glDetachShader( program, fragmentShader ); + GlStateManager.deleteShader( vertexShader ); + GlStateManager.deleteShader( fragmentShader ); + + if( !ok ) return false; + + uniformMv = getUniformLocation( program, "u_mv" ); + uniformFont = getUniformLocation( program, "u_font" ); + uniformWidth = getUniformLocation( program, "u_width" ); + uniformHeight = getUniformLocation( program, "u_height" ); + uniformTbo = getUniformLocation( program, "u_tbo" ); + uniformPalette = getUniformLocation( program, "u_palette" ); + + ComputerCraft.log.info( "Loaded monitor shader." ); + return true; + } + catch( Exception e ) + { + ComputerCraft.log.error( "Cannot load monitor shaders", e ); + return false; + } + } + + private static int loadShader( int kind, String path ) + { + InputStream stream = TileEntityMonitorRenderer.class.getClassLoader().getResourceAsStream( path ); + if( stream == null ) throw new IllegalArgumentException( "Cannot find " + path ); + String contents = TextureUtil.readAllToString( stream ); + + int shader = GlStateManager.createShader( kind ); + + GlStateManager.shaderSource( shader, contents ); + GlStateManager.compileShader( shader ); + + boolean ok = GlStateManager.getShader( shader, GL20.GL_COMPILE_STATUS ) != 0; + String log = GlStateManager.getShaderInfoLog( shader, Short.MAX_VALUE ).trim(); + if( !Strings.isNullOrEmpty( log ) ) + { + ComputerCraft.log.warn( "Problems when loading monitor shader {}: {}", path, log ); + } + + if( !ok ) throw new IllegalStateException( "Cannot compile shader " + path ); + return shader; + } + + private static int getUniformLocation( int program, String name ) + { + int uniform = GlStateManager.getUniformLocation( program, name ); + if( uniform == -1 ) throw new IllegalStateException( "Cannot find uniform " + name ); + return uniform; + } +} diff --git a/src/main/java/dan200/computercraft/client/render/PrintoutRenderer.java b/src/main/java/dan200/computercraft/client/render/PrintoutRenderer.java new file mode 100644 index 000000000..abb2d4f8c --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/PrintoutRenderer.java @@ -0,0 +1,185 @@ +/* + * 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.client.render; + +import dan200.computercraft.client.gui.FixedWidthFontRenderer; +import dan200.computercraft.core.terminal.TextBuffer; +import dan200.computercraft.shared.util.Palette; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderPhase; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Matrix4f; +import org.lwjgl.opengl.GL11; + +import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_HEIGHT; +import static dan200.computercraft.shared.media.items.ItemPrintout.LINES_PER_PAGE; + +public final class PrintoutRenderer +{ + private static final Identifier BG = new Identifier( "computercraft", "textures/gui/printout.png" ); + private static final float BG_SIZE = 256.0f; + + /** + * Width of a page. + */ + public static final int X_SIZE = 172; + + /** + * Height of a page. + */ + public static final int Y_SIZE = 209; + + /** + * Padding between the left and right of a page and the text. + */ + public static final int X_TEXT_MARGIN = 13; + + /** + * Padding between the top and bottom of a page and the text. + */ + public static final int Y_TEXT_MARGIN = 11; + + /** + * Width of the extra page texture. + */ + private static final int X_FOLD_SIZE = 12; + + /** + * Size of the leather cover. + */ + public static final int COVER_SIZE = 12; + + private static final int COVER_Y = Y_SIZE; + private static final int COVER_X = X_SIZE + 4 * X_FOLD_SIZE; + + private PrintoutRenderer() {} + + public static void drawText( Matrix4f transform, VertexConsumerProvider renderer, int x, int y, int start, TextBuffer[] text, TextBuffer[] colours ) + { + VertexConsumer buffer = renderer.getBuffer( FixedWidthFontRenderer.TYPE ); + for( int line = 0; line < LINES_PER_PAGE && line < text.length; line++ ) + { + FixedWidthFontRenderer.drawString( transform, buffer, + x, y + line * FONT_HEIGHT, text[start + line], colours[start + line], null, Palette.DEFAULT, + false, 0, 0 + ); + } + } + + public static void drawText( Matrix4f transform, VertexConsumerProvider renderer, int x, int y, int start, String[] text, String[] colours ) + { + VertexConsumer buffer = renderer.getBuffer( FixedWidthFontRenderer.TYPE ); + for( int line = 0; line < LINES_PER_PAGE && line < text.length; line++ ) + { + FixedWidthFontRenderer.drawString( transform, buffer, + x, y + line * FONT_HEIGHT, + new TextBuffer( text[start + line] ), new TextBuffer( colours[start + line] ), + null, Palette.DEFAULT, false, 0, 0 + ); + } + } + + public static void drawBorder( Matrix4f transform, VertexConsumerProvider renderer, float x, float y, float z, int page, int pages, boolean isBook ) + { + int leftPages = page; + int rightPages = pages - page - 1; + + VertexConsumer buffer = renderer.getBuffer( Type.TYPE ); + + if( isBook ) + { + // Border + float offset = offsetAt( pages ); + float left = x - 4 - offset; + float right = x + X_SIZE + offset - 4; + + // Left and right border + drawTexture( transform, buffer, left - 4, y - 8, z - 0.02f, COVER_X, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2 ); + drawTexture( transform, buffer, right, y - 8, z - 0.02f, COVER_X + COVER_SIZE, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2 ); + + // Draw centre panel (just stretched texture, sorry). + drawTexture( transform, buffer, + x - offset, y, z - 0.02f, X_SIZE + offset * 2, Y_SIZE, + COVER_X + COVER_SIZE / 2.0f, COVER_SIZE, COVER_SIZE, Y_SIZE + ); + + float borderX = left; + while( borderX < right ) + { + double thisWidth = Math.min( right - borderX, X_SIZE ); + drawTexture( transform, buffer, borderX, y - 8, z - 0.02f, 0, COVER_Y, (float) thisWidth, COVER_SIZE ); + drawTexture( transform, buffer, borderX, y + Y_SIZE - 4, z - 0.02f, 0, COVER_Y + COVER_SIZE, (float) thisWidth, COVER_SIZE ); + borderX += thisWidth; + } + } + + // Left half + drawTexture( transform, buffer, x, y, z, X_FOLD_SIZE * 2, 0, X_SIZE / 2.0f, Y_SIZE ); + for( int n = 0; n <= leftPages; n++ ) + { + drawTexture( transform, buffer, + x - offsetAt( n ), y, z - 1e-3f * n, + // Use the left "bold" fold for the outermost page + n == leftPages ? 0 : X_FOLD_SIZE, 0, + X_FOLD_SIZE, Y_SIZE + ); + } + + // Right half + drawTexture( transform, buffer, x + X_SIZE / 2.0f, y, z, X_FOLD_SIZE * 2 + X_SIZE / 2.0f, 0, X_SIZE / 2.0f, Y_SIZE ); + for( int n = 0; n <= rightPages; n++ ) + { + drawTexture( transform, buffer, + x + (X_SIZE - X_FOLD_SIZE) + offsetAt( n ), y, z - 1e-3f * n, + // Two folds, then the main page. Use the right "bold" fold for the outermost page. + X_FOLD_SIZE * 2 + X_SIZE + (n == rightPages ? X_FOLD_SIZE : 0), 0, + X_FOLD_SIZE, Y_SIZE + ); + } + } + + private static void drawTexture( Matrix4f matrix, VertexConsumer buffer, float x, float y, float z, float u, float v, float width, float height ) + { + buffer.vertex( matrix, x, y + height, z ).texture( u / BG_SIZE, (v + height) / BG_SIZE ).next(); + buffer.vertex( matrix, x + width, y + height, z ).texture( (u + width) / BG_SIZE, (v + height) / BG_SIZE ).next(); + buffer.vertex( matrix, x + width, y, z ).texture( (u + width) / BG_SIZE, v / BG_SIZE ).next(); + buffer.vertex( matrix, x, y, z ).texture( u / BG_SIZE, v / BG_SIZE ).next(); + } + + private static void drawTexture( Matrix4f matrix, VertexConsumer buffer, float x, float y, float z, float width, float height, float u, float v, float tWidth, float tHeight ) + { + buffer.vertex( matrix, x, y + height, z ).texture( u / BG_SIZE, (v + tHeight) / BG_SIZE ).next(); + buffer.vertex( matrix, x + width, y + height, z ).texture( (u + tWidth) / BG_SIZE, (v + tHeight) / BG_SIZE ).next(); + buffer.vertex( matrix, x + width, y, z ).texture( (u + tWidth) / BG_SIZE, v / BG_SIZE ).next(); + buffer.vertex( matrix, x, y, z ).texture( u / BG_SIZE, v / BG_SIZE ).next(); + } + + public static float offsetAt( int page ) + { + return (float) (32 * (1 - Math.pow( 1.2, -page ))); + } + + private static final class Type extends RenderPhase + { + static final RenderLayer TYPE = RenderLayer.of( + "printout_background", VertexFormats.POSITION_TEXTURE, GL11.GL_QUADS, 1024, + false, false, // useDelegate, needsSorting + RenderLayer.MultiPhaseParameters.builder() + .texture( new RenderPhase.Texture( BG, false, false ) ) // blur, minimap + .alpha( ONE_TENTH_ALPHA ) + .lightmap( DISABLE_LIGHTMAP ) + .build( false ) + ); + + private Type( String name, Runnable setup, Runnable destroy ) + { + super( name, setup, destroy ); + } + } +} diff --git a/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java b/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java new file mode 100644 index 000000000..a9d54c101 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java @@ -0,0 +1,243 @@ +/* + * 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.client.render; + +import com.mojang.blaze3d.platform.GlStateManager; +import dan200.computercraft.client.FrameInfo; +import dan200.computercraft.client.gui.FixedWidthFontRenderer; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.terminal.TextBuffer; +import dan200.computercraft.shared.peripheral.monitor.ClientMonitor; +import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; +import dan200.computercraft.shared.peripheral.monitor.TileMonitor; +import dan200.computercraft.shared.util.Colour; +import dan200.computercraft.shared.util.DirectionUtil; +import net.minecraft.client.gl.VertexBuffer; +import net.minecraft.client.render.BufferBuilder; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.render.block.entity.BlockEntityRenderDispatcher; +import net.minecraft.client.render.block.entity.BlockEntityRenderer; +import net.minecraft.client.util.GlAllocationUtils; +import net.minecraft.client.util.math.AffineTransformation; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.client.util.math.Vector3f; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Matrix4f; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL31; + +import javax.annotation.Nonnull; +import java.nio.ByteBuffer; + +import static dan200.computercraft.client.gui.FixedWidthFontRenderer.*; + +public class TileEntityMonitorRenderer extends BlockEntityRenderer +{ + /** + * {@link TileMonitor#RENDER_MARGIN}, but a tiny bit of additional padding to ensure that there is no space between + * the monitor frame and contents. + */ + private static final float MARGIN = (float) (TileMonitor.RENDER_MARGIN * 1.1); + private static ByteBuffer tboContents; + + private static final Matrix4f IDENTITY = AffineTransformation.identity().getMatrix(); + + public TileEntityMonitorRenderer( BlockEntityRenderDispatcher rendererDispatcher ) + { + super( rendererDispatcher ); + } + + @Override + public void render( @Nonnull TileMonitor monitor, float partialTicks, @Nonnull MatrixStack transform, @Nonnull VertexConsumerProvider renderer, int lightmapCoord, int overlayLight ) + { + // Render from the origin monitor + ClientMonitor originTerminal = monitor.getClientMonitor(); + + if( originTerminal == null ) return; + TileMonitor origin = originTerminal.getOrigin(); + BlockPos monitorPos = monitor.getPos(); + + // Ensure each monitor terminal is rendered only once. We allow rendering a specific tile + // multiple times in a single frame to ensure compatibility with shaders which may run a + // pass multiple times. + long renderFrame = FrameInfo.getRenderFrame(); + if( originTerminal.lastRenderFrame == renderFrame && !monitorPos.equals( originTerminal.lastRenderPos ) ) + { + return; + } + + originTerminal.lastRenderFrame = renderFrame; + originTerminal.lastRenderPos = monitorPos; + + BlockPos originPos = origin.getPos(); + + // Determine orientation + Direction dir = origin.getDirection(); + Direction front = origin.getFront(); + float yaw = dir.asRotation(); + float pitch = DirectionUtil.toPitchAngle( front ); + + // Setup initial transform + transform.push(); + transform.translate( + originPos.getX() - monitorPos.getX() + 0.5, + originPos.getY() - monitorPos.getY() + 0.5, + originPos.getZ() - monitorPos.getZ() + 0.5 + ); + + transform.multiply( Vector3f.NEGATIVE_Y.getDegreesQuaternion( yaw ) ); + transform.multiply( Vector3f.POSITIVE_X.getDegreesQuaternion( pitch ) ); + transform.translate( + -0.5 + TileMonitor.RENDER_BORDER + TileMonitor.RENDER_MARGIN, + origin.getHeight() - 0.5 - (TileMonitor.RENDER_BORDER + TileMonitor.RENDER_MARGIN) + 0, + 0.5 + ); + double xSize = origin.getWidth() - 2.0 * (TileMonitor.RENDER_MARGIN + TileMonitor.RENDER_BORDER); + double ySize = origin.getHeight() - 2.0 * (TileMonitor.RENDER_MARGIN + TileMonitor.RENDER_BORDER); + + // Draw the contents + Terminal terminal = originTerminal.getTerminal(); + if( terminal != null ) + { + // Draw a terminal + int width = terminal.getWidth(), height = terminal.getHeight(); + int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT; + double xScale = xSize / pixelWidth; + double yScale = ySize / pixelHeight; + transform.push(); + transform.scale( (float) xScale, (float) -yScale, 1.0f ); + + Matrix4f matrix = transform.peek().getModel(); + + // Sneaky hack here: we get a buffer now in order to flush existing ones and set up the appropriate + // render state. I've no clue how well this'll work in future versions of Minecraft, but it does the trick + // for now. + VertexConsumer buffer = renderer.getBuffer( FixedWidthFontRenderer.TYPE ); + FixedWidthFontRenderer.TYPE.startDrawing(); + + renderTerminal( matrix, originTerminal, (float) (MARGIN / xScale), (float) (MARGIN / yScale) ); + + // We don't draw the cursor with the VBO, as it's dynamic and so we'll end up refreshing far more than is + // reasonable. + FixedWidthFontRenderer.drawCursor( matrix, buffer, 0, 0, terminal, !originTerminal.isColour() ); + + transform.pop(); + } + else + { + FixedWidthFontRenderer.drawEmptyTerminal( + transform.peek().getModel(), renderer, + -MARGIN, MARGIN, + (float) (xSize + 2 * MARGIN), (float) -(ySize + MARGIN * 2) + ); + } + + FixedWidthFontRenderer.drawBlocker( + transform.peek().getModel(), renderer, + (float) -TileMonitor.RENDER_MARGIN, (float) TileMonitor.RENDER_MARGIN, + (float) (xSize + 2 * TileMonitor.RENDER_MARGIN), (float) -(ySize + TileMonitor.RENDER_MARGIN * 2) + ); + + transform.pop(); + } + + private static void renderTerminal( Matrix4f matrix, ClientMonitor monitor, float xMargin, float yMargin ) + { + Terminal terminal = monitor.getTerminal(); + + MonitorRenderer renderType = MonitorRenderer.current(); + boolean redraw = monitor.pollTerminalChanged(); + if( monitor.createBuffer( renderType ) ) redraw = true; + + switch( renderType ) + { + case TBO: + { + if( !MonitorTextureBufferShader.use() ) return; + + int width = terminal.getWidth(), height = terminal.getHeight(); + int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT; + + if( redraw ) + { + int size = width * height * 3; + if( tboContents == null || tboContents.capacity() < size ) + { + tboContents = GlAllocationUtils.allocateByteBuffer( size ); + } + + ByteBuffer monitorBuffer = tboContents; + monitorBuffer.clear(); + for( int y = 0; y < height; y++ ) + { + TextBuffer text = terminal.getLine( y ), textColour = terminal.getTextColourLine( y ), background = terminal.getBackgroundColourLine( y ); + for( int x = 0; x < width; x++ ) + { + monitorBuffer.put( (byte) (text.charAt( x ) & 0xFF) ); + monitorBuffer.put( (byte) getColour( textColour.charAt( x ), Colour.WHITE ) ); + monitorBuffer.put( (byte) getColour( background.charAt( x ), Colour.BLACK ) ); + } + } + monitorBuffer.flip(); + + GlStateManager.bindBuffers( GL31.GL_TEXTURE_BUFFER, monitor.tboBuffer ); + GlStateManager.bufferData( GL31.GL_TEXTURE_BUFFER, monitorBuffer, GL20.GL_STATIC_DRAW ); + GlStateManager.bindBuffers( GL31.GL_TEXTURE_BUFFER, 0 ); + } + + // Nobody knows what they're doing! + GlStateManager.activeTexture( MonitorTextureBufferShader.TEXTURE_INDEX ); + GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, monitor.tboTexture ); + GlStateManager.activeTexture( GL13.GL_TEXTURE0 ); + + MonitorTextureBufferShader.setupUniform( matrix, width, height, terminal.getPalette(), !monitor.isColour() ); + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder buffer = tessellator.getBuffer(); + buffer.begin( GL11.GL_TRIANGLE_STRIP, VertexFormats.POSITION ); + buffer.vertex( -xMargin, -yMargin, 0 ).next(); + buffer.vertex( -xMargin, pixelHeight + yMargin, 0 ).next(); + buffer.vertex( pixelWidth + xMargin, -yMargin, 0 ).next(); + buffer.vertex( pixelWidth + xMargin, pixelHeight + yMargin, 0 ).next(); + tessellator.draw(); + + GlStateManager.useProgram( 0 ); + break; + } + + case VBO: + { + VertexBuffer vbo = monitor.buffer; + if( redraw ) + { + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder builder = tessellator.getBuffer(); + builder.begin( FixedWidthFontRenderer.TYPE.getDrawMode(), FixedWidthFontRenderer.TYPE.getVertexFormat() ); + FixedWidthFontRenderer.drawTerminalWithoutCursor( + IDENTITY, builder, 0, 0, + terminal, !monitor.isColour(), yMargin, yMargin, xMargin, xMargin + ); + + builder.end(); + vbo.upload( builder ); + } + + vbo.bind(); + FixedWidthFontRenderer.TYPE.getVertexFormat().startDrawing( 0L ); + vbo.draw( matrix, FixedWidthFontRenderer.TYPE.getDrawMode() ); + VertexBuffer.unbind(); + FixedWidthFontRenderer.TYPE.getVertexFormat().endDrawing(); + break; + } + } + } +} diff --git a/src/main/java/dan200/computercraft/client/render/TileEntityTurtleRenderer.java b/src/main/java/dan200/computercraft/client/render/TileEntityTurtleRenderer.java new file mode 100644 index 000000000..db8f03147 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/TileEntityTurtleRenderer.java @@ -0,0 +1,191 @@ +/* + * 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.client.render; + +import dan200.computercraft.api.client.TransformedModel; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.turtle.blocks.TileTurtle; +import dan200.computercraft.shared.util.DirectionUtil; +import dan200.computercraft.shared.util.Holiday; +import dan200.computercraft.shared.util.HolidayUtil; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.render.TexturedRenderLayers; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.block.entity.BlockEntityRenderDispatcher; +import net.minecraft.client.render.block.entity.BlockEntityRenderer; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.BakedModelManager; +import net.minecraft.client.render.model.BakedQuad; +import net.minecraft.client.util.ModelIdentifier; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.client.util.math.Vector3f; +import net.minecraft.util.Identifier; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.client.model.data.EmptyModelData; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Random; + +public class TileEntityTurtleRenderer extends BlockEntityRenderer +{ + private static final ModelIdentifier NORMAL_TURTLE_MODEL = new ModelIdentifier( "computercraft:turtle_normal", "inventory" ); + private static final ModelIdentifier ADVANCED_TURTLE_MODEL = new ModelIdentifier( "computercraft:turtle_advanced", "inventory" ); + private static final ModelIdentifier COLOUR_TURTLE_MODEL = new ModelIdentifier( "computercraft:turtle_colour", "inventory" ); + private static final ModelIdentifier ELF_OVERLAY_MODEL = new ModelIdentifier( "computercraft:turtle_elf_overlay", "inventory" ); + + private final Random random = new Random( 0 ); + + public TileEntityTurtleRenderer( BlockEntityRenderDispatcher renderDispatcher ) + { + super( renderDispatcher ); + } + + public static ModelIdentifier getTurtleModel( ComputerFamily family, boolean coloured ) + { + switch( family ) + { + case NORMAL: + default: + return coloured ? COLOUR_TURTLE_MODEL : NORMAL_TURTLE_MODEL; + case ADVANCED: + return coloured ? COLOUR_TURTLE_MODEL : ADVANCED_TURTLE_MODEL; + } + } + + public static ModelIdentifier getTurtleOverlayModel( Identifier overlay, boolean christmas ) + { + if( overlay != null ) return new ModelIdentifier( overlay, "inventory" ); + if( christmas ) return ELF_OVERLAY_MODEL; + return null; + } + + @Override + public void render( @Nonnull TileTurtle turtle, float partialTicks, @Nonnull MatrixStack transform, @Nonnull VertexConsumerProvider renderer, int lightmapCoord, int overlayLight ) + { + // Render the label + String label = turtle.createProxy().getLabel(); + HitResult hit = dispatcher.crosshairTarget; + if( label != null && hit.getType() == HitResult.Type.BLOCK && turtle.getPos().equals( ((BlockHitResult) hit).getBlockPos() ) ) + { + MinecraftClient mc = MinecraftClient.getInstance(); + TextRenderer font = dispatcher.textRenderer; + + transform.push(); + transform.translate( 0.5, 1.2, 0.5 ); + transform.multiply( mc.getEntityRenderDispatcher().getRotation() ); + transform.scale( -0.025f, -0.025f, 0.025f ); + + Matrix4f matrix = transform.peek().getModel(); + int opacity = (int) (mc.options.getTextBackgroundOpacity( 0.25f ) * 255) << 24; + float width = -font.getWidth( label ) / 2.0f; + font.draw( label, width, (float) 0, 0x20ffffff, false, matrix, renderer, true, opacity, lightmapCoord ); + font.draw( label, width, (float) 0, 0xffffffff, false, matrix, renderer, false, 0, lightmapCoord ); + + transform.pop(); + } + + transform.push(); + + // Setup the transform. + Vec3d offset = turtle.getRenderOffset( partialTicks ); + float yaw = turtle.getRenderYaw( partialTicks ); + transform.translate( offset.x, offset.y, offset.z ); + + transform.translate( 0.5f, 0.5f, 0.5f ); + transform.multiply( Vector3f.POSITIVE_Y.getDegreesQuaternion( 180.0f - yaw ) ); + if( label != null && (label.equals( "Dinnerbone" ) || label.equals( "Grumm" )) ) + { + // Flip the model + transform.scale( 1.0f, -1.0f, 1.0f ); + } + transform.translate( -0.5f, -0.5f, -0.5f ); + + // Render the turtle + int colour = turtle.getColour(); + ComputerFamily family = turtle.getFamily(); + Identifier overlay = turtle.getOverlay(); + + VertexConsumer buffer = renderer.getBuffer( TexturedRenderLayers.getEntityTranslucentCull() ); + renderModel( transform, buffer, lightmapCoord, overlayLight, getTurtleModel( family, colour != -1 ), colour == -1 ? null : new int[] { colour } ); + + // Render the overlay + ModelIdentifier overlayModel = getTurtleOverlayModel( overlay, HolidayUtil.getCurrentHoliday() == Holiday.CHRISTMAS ); + if( overlayModel != null ) + { + renderModel( transform, buffer, lightmapCoord, overlayLight, overlayModel, null ); + } + + // Render the upgrades + renderUpgrade( transform, buffer, lightmapCoord, overlayLight, turtle, TurtleSide.LEFT, partialTicks ); + renderUpgrade( transform, buffer, lightmapCoord, overlayLight, turtle, TurtleSide.RIGHT, partialTicks ); + + transform.pop(); + } + + private void renderUpgrade( @Nonnull MatrixStack transform, @Nonnull VertexConsumer renderer, int lightmapCoord, int overlayLight, TileTurtle turtle, TurtleSide side, float f ) + { + ITurtleUpgrade upgrade = turtle.getUpgrade( side ); + if( upgrade == null ) return; + transform.push(); + + float toolAngle = turtle.getToolRenderAngle( side, f ); + transform.translate( 0.0f, 0.5f, 0.5f ); + transform.multiply( Vector3f.NEGATIVE_X.getDegreesQuaternion( toolAngle ) ); + transform.translate( 0.0f, -0.5f, -0.5f ); + + TransformedModel model = upgrade.getModel( turtle.getAccess(), side ); + model.getMatrix().push( transform ); + renderModel( transform, renderer, lightmapCoord, overlayLight, model.getModel(), null ); + transform.pop(); + + transform.pop(); + } + + private void renderModel( @Nonnull MatrixStack transform, @Nonnull VertexConsumer renderer, int lightmapCoord, int overlayLight, ModelIdentifier modelLocation, int[] tints ) + { + BakedModelManager modelManager = MinecraftClient.getInstance().getItemRenderer().getModels().getModelManager(); + renderModel( transform, renderer, lightmapCoord, overlayLight, modelManager.getModel( modelLocation ), tints ); + } + + private void renderModel( @Nonnull MatrixStack transform, @Nonnull VertexConsumer renderer, int lightmapCoord, int overlayLight, BakedModel model, int[] tints ) + { + random.setSeed( 0 ); + renderQuads( transform, renderer, lightmapCoord, overlayLight, model.getQuads( null, null, random, EmptyModelData.INSTANCE ), tints ); + for( Direction facing : DirectionUtil.FACINGS ) + { + renderQuads( transform, renderer, lightmapCoord, overlayLight, model.getQuads( null, facing, random, EmptyModelData.INSTANCE ), tints ); + } + } + + private static void renderQuads( @Nonnull MatrixStack transform, @Nonnull VertexConsumer buffer, int lightmapCoord, int overlayLight, List quads, int[] tints ) + { + MatrixStack.Entry matrix = transform.peek(); + + for( BakedQuad bakedquad : quads ) + { + int tint = -1; + if( tints != null && bakedquad.hasColor() ) + { + int idx = bakedquad.getColorIndex(); + if( idx >= 0 && idx < tints.length ) tint = tints[bakedquad.getColorIndex()]; + } + + float f = (float) (tint >> 16 & 255) / 255.0F; + float f1 = (float) (tint >> 8 & 255) / 255.0F; + float f2 = (float) (tint & 255) / 255.0F; + buffer.addVertexData( matrix, bakedquad, f, f1, f2, lightmapCoord, overlayLight, true ); + } + } +} diff --git a/src/main/java/dan200/computercraft/client/render/TurtleModelLoader.java b/src/main/java/dan200/computercraft/client/render/TurtleModelLoader.java new file mode 100644 index 000000000..fd6d6bbda --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/TurtleModelLoader.java @@ -0,0 +1,83 @@ +/* + * 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.client.render; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Pair; +import dan200.computercraft.ComputerCraft; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.ModelBakeSettings; +import net.minecraft.client.render.model.ModelLoader; +import net.minecraft.client.render.model.UnbakedModel; +import net.minecraft.client.render.model.json.ModelOverrideList; +import net.minecraft.client.renderer.model.*; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.util.SpriteIdentifier; +import net.minecraft.resource.ResourceManager; +import net.minecraft.util.Identifier; +import net.minecraft.util.JsonHelper; +import net.minecraftforge.client.model.IModelConfiguration; +import net.minecraftforge.client.model.IModelLoader; +import net.minecraftforge.client.model.geometry.IModelGeometry; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +public final class TurtleModelLoader implements IModelLoader +{ + private static final Identifier COLOUR_TURTLE_MODEL = new Identifier( ComputerCraft.MOD_ID, "block/turtle_colour" ); + + public static final TurtleModelLoader INSTANCE = new TurtleModelLoader(); + + private TurtleModelLoader() + { + } + + @Override + public void apply( @Nonnull ResourceManager manager ) + { + } + + @Nonnull + @Override + public TurtleModel read( @Nonnull JsonDeserializationContext deserializationContext, @Nonnull JsonObject modelContents ) + { + Identifier model = new Identifier( JsonHelper.getString( modelContents, "model" ) ); + return new TurtleModel( model ); + } + + public static final class TurtleModel implements IModelGeometry + { + private final Identifier family; + + private TurtleModel( Identifier family ) + { + this.family = family; + } + + @Override + public Collection getTextures( IModelConfiguration owner, Function modelGetter, Set> missingTextureErrors ) + { + Set materials = new HashSet<>(); + materials.addAll( modelGetter.apply( family ).getTextureDependencies( modelGetter, missingTextureErrors ) ); + materials.addAll( modelGetter.apply( COLOUR_TURTLE_MODEL ).getTextureDependencies( modelGetter, missingTextureErrors ) ); + return materials; + } + + @Override + public BakedModel bake( IModelConfiguration owner, ModelLoader bakery, Function spriteGetter, ModelBakeSettings transform, ModelOverrideList overrides, Identifier modelLocation ) + { + return new TurtleSmartItemModel( + bakery.getBakedModel( family, transform, spriteGetter ), + bakery.getBakedModel( COLOUR_TURTLE_MODEL, transform, spriteGetter ) + ); + } + } +} diff --git a/src/main/java/dan200/computercraft/client/render/TurtleMultiModel.java b/src/main/java/dan200/computercraft/client/render/TurtleMultiModel.java new file mode 100644 index 000000000..c0e351541 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/TurtleMultiModel.java @@ -0,0 +1,149 @@ +/* + * 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.client.render; + +import dan200.computercraft.api.client.TransformedModel; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.BakedQuad; +import net.minecraft.client.render.model.json.ModelOverrideList; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.util.math.AffineTransformation; +import net.minecraft.util.math.Direction; +import net.minecraftforge.client.model.data.EmptyModelData; +import net.minecraftforge.client.model.data.IModelData; +import net.minecraftforge.client.model.pipeline.BakedQuadBuilder; +import net.minecraftforge.client.model.pipeline.TRSRTransformer; + +import javax.annotation.Nonnull; +import java.util.*; + +public class TurtleMultiModel implements BakedModel +{ + private final BakedModel m_baseModel; + private final BakedModel m_overlayModel; + private final AffineTransformation m_generalTransform; + private final TransformedModel m_leftUpgradeModel; + private final TransformedModel m_rightUpgradeModel; + private List m_generalQuads = null; + private Map> m_faceQuads = new EnumMap<>( Direction.class ); + + public TurtleMultiModel( BakedModel baseModel, BakedModel overlayModel, AffineTransformation generalTransform, TransformedModel leftUpgradeModel, TransformedModel rightUpgradeModel ) + { + // Get the models + m_baseModel = baseModel; + m_overlayModel = overlayModel; + m_leftUpgradeModel = leftUpgradeModel; + m_rightUpgradeModel = rightUpgradeModel; + m_generalTransform = generalTransform; + } + + @Nonnull + @Override + @Deprecated + public List getQuads( BlockState state, Direction side, @Nonnull Random rand ) + { + return getQuads( state, side, rand, EmptyModelData.INSTANCE ); + } + + @Nonnull + @Override + public List getQuads( BlockState state, Direction side, @Nonnull Random rand, @Nonnull IModelData data ) + { + if( side != null ) + { + if( !m_faceQuads.containsKey( side ) ) m_faceQuads.put( side, buildQuads( state, side, rand ) ); + return m_faceQuads.get( side ); + } + else + { + if( m_generalQuads == null ) m_generalQuads = buildQuads( state, side, rand ); + return m_generalQuads; + } + } + + private List buildQuads( BlockState state, Direction side, Random rand ) + { + ArrayList quads = new ArrayList<>(); + + + transformQuadsTo( quads, m_baseModel.getQuads( state, side, rand, EmptyModelData.INSTANCE ), m_generalTransform ); + if( m_overlayModel != null ) + { + transformQuadsTo( quads, m_overlayModel.getQuads( state, side, rand, EmptyModelData.INSTANCE ), m_generalTransform ); + } + if( m_leftUpgradeModel != null ) + { + AffineTransformation upgradeTransform = m_generalTransform.compose( m_leftUpgradeModel.getMatrix() ); + transformQuadsTo( quads, m_leftUpgradeModel.getModel().getQuads( state, side, rand, EmptyModelData.INSTANCE ), upgradeTransform ); + } + if( m_rightUpgradeModel != null ) + { + AffineTransformation upgradeTransform = m_generalTransform.compose( m_rightUpgradeModel.getMatrix() ); + transformQuadsTo( quads, m_rightUpgradeModel.getModel().getQuads( state, side, rand, EmptyModelData.INSTANCE ), upgradeTransform ); + } + quads.trimToSize(); + return quads; + } + + @Override + public boolean useAmbientOcclusion() + { + return m_baseModel.useAmbientOcclusion(); + } + + @Override + public boolean hasDepth() + { + return m_baseModel.hasDepth(); + } + + @Override + public boolean isBuiltin() + { + return m_baseModel.isBuiltin(); + } + + @Override + public boolean isSideLit() + { + return m_baseModel.isSideLit(); + } + + @Nonnull + @Override + @Deprecated + public Sprite getSprite() + { + return m_baseModel.getSprite(); + } + + @Nonnull + @Override + @Deprecated + public net.minecraft.client.render.model.json.ModelTransformation getTransformation() + { + return m_baseModel.getTransformation(); + } + + @Nonnull + @Override + public ModelOverrideList getOverrides() + { + return ModelOverrideList.EMPTY; + } + + private void transformQuadsTo( List output, List quads, AffineTransformation transform ) + { + for( BakedQuad quad : quads ) + { + BakedQuadBuilder builder = new BakedQuadBuilder(); + TRSRTransformer transformer = new TRSRTransformer( builder, transform ); + quad.pipe( transformer ); + output.add( builder.build() ); + } + } +} diff --git a/src/main/java/dan200/computercraft/client/render/TurtlePlayerRenderer.java b/src/main/java/dan200/computercraft/client/render/TurtlePlayerRenderer.java new file mode 100644 index 000000000..9031d111e --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/TurtlePlayerRenderer.java @@ -0,0 +1,35 @@ +/* + * 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.client.render; + +import dan200.computercraft.shared.turtle.core.TurtlePlayer; +import javax.annotation.Nonnull; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.EntityRenderDispatcher; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; + +public class TurtlePlayerRenderer extends EntityRenderer +{ + public TurtlePlayerRenderer( EntityRenderDispatcher renderManager ) + { + super( renderManager ); + } + + @Nonnull + @Override + public Identifier getEntityTexture( @Nonnull TurtlePlayer entity ) + { + return ComputerBorderRenderer.BACKGROUND_NORMAL; + } + + @Override + public void render( @Nonnull TurtlePlayer entityIn, float entityYaw, float partialTicks, @Nonnull MatrixStack transform, @Nonnull VertexConsumerProvider buffer, int packedLightIn ) + { + } +} diff --git a/src/main/java/dan200/computercraft/client/render/TurtleSmartItemModel.java b/src/main/java/dan200/computercraft/client/render/TurtleSmartItemModel.java new file mode 100644 index 000000000..56136fafe --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/TurtleSmartItemModel.java @@ -0,0 +1,214 @@ +/* + * 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.client.render; + +import com.google.common.base.Objects; +import dan200.computercraft.api.client.TransformedModel; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.turtle.items.ItemTurtle; +import dan200.computercraft.shared.util.Holiday; +import dan200.computercraft.shared.util.HolidayUtil; +import net.minecraft.block.BlockState; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.BakedModelManager; +import net.minecraft.client.render.model.BakedQuad; +import net.minecraft.client.render.model.json.ModelOverrideList; +import net.minecraft.client.render.model.json.ModelTransformation; +import net.minecraft.client.renderer.model.*; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.util.ModelIdentifier; +import net.minecraft.client.util.math.AffineTransformation; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Direction; +import net.minecraftforge.client.model.data.IModelData; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.List; +import java.util.Random; + +public class TurtleSmartItemModel implements BakedModel +{ + private static final AffineTransformation identity, flip; + + static + { + MatrixStack stack = new MatrixStack(); + stack.scale( 0, -1, 0 ); + stack.translate( 0, 0, 1 ); + + identity = AffineTransformation.identity(); + flip = new AffineTransformation( stack.peek().getModel() ); + } + + private static class TurtleModelCombination + { + final boolean m_colour; + final ITurtleUpgrade m_leftUpgrade; + final ITurtleUpgrade m_rightUpgrade; + final Identifier m_overlay; + final boolean m_christmas; + final boolean m_flip; + + TurtleModelCombination( boolean colour, ITurtleUpgrade leftUpgrade, ITurtleUpgrade rightUpgrade, Identifier overlay, boolean christmas, boolean flip ) + { + m_colour = colour; + m_leftUpgrade = leftUpgrade; + m_rightUpgrade = rightUpgrade; + m_overlay = overlay; + m_christmas = christmas; + m_flip = flip; + } + + @Override + public boolean equals( Object other ) + { + if( other == this ) return true; + if( !(other instanceof TurtleModelCombination) ) return false; + + TurtleModelCombination otherCombo = (TurtleModelCombination) other; + return otherCombo.m_colour == m_colour && + otherCombo.m_leftUpgrade == m_leftUpgrade && + otherCombo.m_rightUpgrade == m_rightUpgrade && + Objects.equal( otherCombo.m_overlay, m_overlay ) && + otherCombo.m_christmas == m_christmas && + otherCombo.m_flip == m_flip; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 0; + result = prime * result + (m_colour ? 1 : 0); + result = prime * result + (m_leftUpgrade != null ? m_leftUpgrade.hashCode() : 0); + result = prime * result + (m_rightUpgrade != null ? m_rightUpgrade.hashCode() : 0); + result = prime * result + (m_overlay != null ? m_overlay.hashCode() : 0); + result = prime * result + (m_christmas ? 1 : 0); + result = prime * result + (m_flip ? 1 : 0); + return result; + } + } + + private final BakedModel familyModel; + private final BakedModel colourModel; + + private final HashMap m_cachedModels = new HashMap<>(); + private final ModelOverrideList m_overrides; + + public TurtleSmartItemModel( BakedModel familyModel, BakedModel colourModel ) + { + this.familyModel = familyModel; + this.colourModel = colourModel; + + m_overrides = new ModelOverrideList() + { + @Nonnull + @Override + public BakedModel apply( @Nonnull BakedModel originalModel, @Nonnull ItemStack stack, @Nullable ClientWorld world, @Nullable LivingEntity entity ) + { + ItemTurtle turtle = (ItemTurtle) stack.getItem(); + int colour = turtle.getColour( stack ); + ITurtleUpgrade leftUpgrade = turtle.getUpgrade( stack, TurtleSide.LEFT ); + ITurtleUpgrade rightUpgrade = turtle.getUpgrade( stack, TurtleSide.RIGHT ); + Identifier overlay = turtle.getOverlay( stack ); + boolean christmas = HolidayUtil.getCurrentHoliday() == Holiday.CHRISTMAS; + String label = turtle.getLabel( stack ); + boolean flip = label != null && (label.equals( "Dinnerbone" ) || label.equals( "Grumm" )); + TurtleModelCombination combo = new TurtleModelCombination( colour != -1, leftUpgrade, rightUpgrade, overlay, christmas, flip ); + + BakedModel model = m_cachedModels.get( combo ); + if( model == null ) m_cachedModels.put( combo, model = buildModel( combo ) ); + return model; + } + }; + } + + @Nonnull + @Override + public ModelOverrideList getOverrides() + { + return m_overrides; + } + + private BakedModel buildModel( TurtleModelCombination combo ) + { + MinecraftClient mc = MinecraftClient.getInstance(); + BakedModelManager modelManager = mc.getItemRenderer().getModels().getModelManager(); + ModelIdentifier overlayModelLocation = TileEntityTurtleRenderer.getTurtleOverlayModel( combo.m_overlay, combo.m_christmas ); + + BakedModel baseModel = combo.m_colour ? colourModel : familyModel; + BakedModel overlayModel = overlayModelLocation != null ? modelManager.getModel( overlayModelLocation ) : null; + AffineTransformation transform = combo.m_flip ? flip : identity; + TransformedModel leftModel = combo.m_leftUpgrade != null ? combo.m_leftUpgrade.getModel( null, TurtleSide.LEFT ) : null; + TransformedModel rightModel = combo.m_rightUpgrade != null ? combo.m_rightUpgrade.getModel( null, TurtleSide.RIGHT ) : null; + return new TurtleMultiModel( baseModel, overlayModel, transform, leftModel, rightModel ); + } + + @Nonnull + @Override + @Deprecated + public List getQuads( BlockState state, Direction facing, @Nonnull Random rand ) + { + return familyModel.getQuads( state, facing, rand ); + } + + @Nonnull + @Override + @Deprecated + public List getQuads( BlockState state, Direction facing, @Nonnull Random rand, @Nonnull IModelData data ) + { + return familyModel.getQuads( state, facing, rand, data ); + } + + @Override + public boolean useAmbientOcclusion() + { + return familyModel.useAmbientOcclusion(); + } + + @Override + public boolean hasDepth() + { + return familyModel.hasDepth(); + } + + @Override + public boolean isBuiltin() + { + return familyModel.isBuiltin(); + } + + @Override + public boolean isSideLit() + { + return familyModel.isSideLit(); + } + + @Nonnull + @Override + @Deprecated + public Sprite getSprite() + { + return familyModel.getSprite(); + } + + @Nonnull + @Override + @Deprecated + public ModelTransformation getTransformation() + { + return familyModel.getTransformation(); + } + +} diff --git a/src/main/java/dan200/computercraft/core/apis/ApiFactories.java b/src/main/java/dan200/computercraft/core/apis/ApiFactories.java new file mode 100644 index 000000000..32a4faf0c --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/ApiFactories.java @@ -0,0 +1,35 @@ +/* + * 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.apis; + +import dan200.computercraft.api.lua.ILuaAPIFactory; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Objects; + +public final class ApiFactories +{ + private ApiFactories() + { + } + + private static final Collection factories = new LinkedHashSet<>(); + private static final Collection factoriesView = Collections.unmodifiableCollection( factories ); + + public static synchronized void register( @Nonnull ILuaAPIFactory factory ) + { + Objects.requireNonNull( factory, "provider cannot be null" ); + factories.add( factory ); + } + + public static Iterable getAll() + { + return factoriesView; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/ComputerAccess.java b/src/main/java/dan200/computercraft/core/apis/ComputerAccess.java new file mode 100644 index 000000000..1eebc5e01 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/ComputerAccess.java @@ -0,0 +1,147 @@ +/* + * 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.apis; + +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IWorkMonitor; +import dan200.computercraft.core.filesystem.FileSystem; +import dan200.computercraft.core.filesystem.FileSystemException; + +import javax.annotation.Nonnull; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public abstract class ComputerAccess implements IComputerAccess +{ + private final IAPIEnvironment m_environment; + private final Set m_mounts = new HashSet<>(); + + protected ComputerAccess( IAPIEnvironment environment ) + { + this.m_environment = environment; + } + + public void unmountAll() + { + FileSystem fileSystem = m_environment.getFileSystem(); + for( String mount : m_mounts ) + { + fileSystem.unmount( mount ); + } + m_mounts.clear(); + } + + @Override + public synchronized String mount( @Nonnull String desiredLoc, @Nonnull IMount mount, @Nonnull String driveName ) + { + Objects.requireNonNull( desiredLoc, "desiredLocation cannot be null" ); + Objects.requireNonNull( mount, "mount cannot be null" ); + Objects.requireNonNull( driveName, "driveName cannot be null" ); + + // Mount the location + String location; + FileSystem fileSystem = m_environment.getFileSystem(); + if( fileSystem == null ) throw new IllegalStateException( "File system has not been created" ); + + synchronized( fileSystem ) + { + location = findFreeLocation( desiredLoc ); + if( location != null ) + { + try + { + fileSystem.mount( driveName, location, mount ); + } + catch( FileSystemException ignored ) + { + } + } + } + + if( location != null ) m_mounts.add( location ); + return location; + } + + @Override + public synchronized String mountWritable( @Nonnull String desiredLoc, @Nonnull IWritableMount mount, @Nonnull String driveName ) + { + Objects.requireNonNull( desiredLoc, "desiredLocation cannot be null" ); + Objects.requireNonNull( mount, "mount cannot be null" ); + Objects.requireNonNull( driveName, "driveName cannot be null" ); + + // Mount the location + String location; + FileSystem fileSystem = m_environment.getFileSystem(); + if( fileSystem == null ) throw new IllegalStateException( "File system has not been created" ); + + synchronized( fileSystem ) + { + location = findFreeLocation( desiredLoc ); + if( location != null ) + { + try + { + fileSystem.mountWritable( driveName, location, mount ); + } + catch( FileSystemException ignored ) + { + } + } + } + + if( location != null ) m_mounts.add( location ); + return location; + } + + @Override + public void unmount( String location ) + { + if( location == null ) return; + if( !m_mounts.contains( location ) ) throw new IllegalStateException( "You didn't mount this location" ); + + m_environment.getFileSystem().unmount( location ); + m_mounts.remove( location ); + } + + @Override + public int getID() + { + return m_environment.getComputerID(); + } + + @Override + public void queueEvent( @Nonnull String event, Object... arguments ) + { + Objects.requireNonNull( event, "event cannot be null" ); + m_environment.queueEvent( event, arguments ); + } + + @Nonnull + @Override + public IWorkMonitor getMainThreadMonitor() + { + return m_environment.getMainThreadMonitor(); + } + + private String findFreeLocation( String desiredLoc ) + { + try + { + FileSystem fileSystem = m_environment.getFileSystem(); + if( !fileSystem.exists( desiredLoc ) ) return desiredLoc; + + // We used to check foo2, foo3, foo4, etc here but the disk drive does this itself now + return null; + } + catch( FileSystemException e ) + { + return null; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/FSAPI.java b/src/main/java/dan200/computercraft/core/apis/FSAPI.java new file mode 100644 index 000000000..3220d5b31 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/FSAPI.java @@ -0,0 +1,498 @@ +/* + * 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.apis; + +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.apis.handles.BinaryReadableHandle; +import dan200.computercraft.core.apis.handles.BinaryWritableHandle; +import dan200.computercraft.core.apis.handles.EncodedReadableHandle; +import dan200.computercraft.core.apis.handles.EncodedWritableHandle; +import dan200.computercraft.core.filesystem.FileSystem; +import dan200.computercraft.core.filesystem.FileSystemException; +import dan200.computercraft.core.filesystem.FileSystemWrapper; +import dan200.computercraft.core.tracking.TrackingField; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.HashMap; +import java.util.Map; +import java.util.OptionalLong; +import java.util.function.Function; + +/** + * The FS API allows you to manipulate files and the filesystem. + * + * @cc.module fs + */ +public class FSAPI implements ILuaAPI +{ + private final IAPIEnvironment environment; + private FileSystem fileSystem = null; + + public FSAPI( IAPIEnvironment env ) + { + environment = env; + } + + @Override + public String[] getNames() + { + return new String[] { "fs" }; + } + + @Override + public void startup() + { + fileSystem = environment.getFileSystem(); + } + + @Override + public void shutdown() + { + fileSystem = null; + } + + /** + * Returns a list of files in a directory. + * + * @param path The path to list. + * @return A table with a list of files in the directory. + * @throws LuaException If the path doesn't exist. + */ + @LuaFunction + public final String[] list( String path ) throws LuaException + { + environment.addTrackingChange( TrackingField.FS_OPS ); + try + { + return fileSystem.list( path ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Combines two parts of a path into one full path, adding separators as + * needed. + * + * @param pathA The first part of the path. For example, a parent directory path. + * @param pathB The second part of the path. For example, a file name. + * @return The new path, with separators added between parts as needed. + */ + @LuaFunction + public final String combine( String pathA, String pathB ) + { + return fileSystem.combine( pathA, pathB ); + } + + /** + * Returns the file name portion of a path. + * + * @param path The path to get the name from. + * @return The final part of the path (the file name). + */ + @LuaFunction + public final String getName( String path ) + { + return FileSystem.getName( path ); + } + + /** + * Returns the parent directory portion of a path. + * + * @param path The path to get the directory from. + * @return The path with the final part removed (the parent directory). + */ + @LuaFunction + public final String getDir( String path ) + { + return FileSystem.getDirectory( path ); + } + + /** + * Returns the size of the specified file. + * + * @param path The file to get the file size of. + * @return The size of the file, in bytes. + * @throws LuaException If the path doesn't exist. + */ + @LuaFunction + public final long getSize( String path ) throws LuaException + { + try + { + return fileSystem.getSize( path ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Returns whether the specified path exists. + * + * @param path The path to check the existence of. + * @return Whether the path exists. + */ + @LuaFunction + public final boolean exists( String path ) + { + try + { + return fileSystem.exists( path ); + } + catch( FileSystemException e ) + { + return false; + } + } + + /** + * Returns whether the specified path is a directory. + * + * @param path The path to check. + * @return Whether the path is a directory. + */ + @LuaFunction + public final boolean isDir( String path ) + { + try + { + return fileSystem.isDir( path ); + } + catch( FileSystemException e ) + { + return false; + } + } + + /** + * Returns whether a path is read-only. + * + * @param path The path to check. + * @return Whether the path cannot be written to. + */ + @LuaFunction + public final boolean isReadOnly( String path ) + { + try + { + return fileSystem.isReadOnly( path ); + } + catch( FileSystemException e ) + { + return false; + } + } + + /** + * Creates a directory, and any missing parents, at the specified path. + * + * @param path The path to the directory to create. + * @throws LuaException If the directory couldn't be created. + */ + @LuaFunction + public final void makeDir( String path ) throws LuaException + { + try + { + environment.addTrackingChange( TrackingField.FS_OPS ); + fileSystem.makeDir( path ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Moves a file or directory from one path to another. + * + * Any parent directories are created as needed. + * + * @param path The current file or directory to move from. + * @param dest The destination path for the file or directory. + * @throws LuaException If the file or directory couldn't be moved. + */ + @LuaFunction + public final void move( String path, String dest ) throws LuaException + { + try + { + environment.addTrackingChange( TrackingField.FS_OPS ); + fileSystem.move( path, dest ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Copies a file or directory to a new path. + * + * Any parent directories are created as needed. + * + * @param path The file or directory to copy. + * @param dest The path to the destination file or directory. + * @throws LuaException If the file or directory couldn't be copied. + */ + @LuaFunction + public final void copy( String path, String dest ) throws LuaException + { + try + { + environment.addTrackingChange( TrackingField.FS_OPS ); + fileSystem.copy( path, dest ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Deletes a file or directory. + * + * If the path points to a directory, all of the enclosed files and + * subdirectories are also deleted. + * + * @param path The path to the file or directory to delete. + * @throws LuaException If the file or directory couldn't be deleted. + */ + @LuaFunction + public final void delete( String path ) throws LuaException + { + try + { + environment.addTrackingChange( TrackingField.FS_OPS ); + fileSystem.delete( path ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + // FIXME: Add individual handle type documentation + + /** + * Opens a file for reading or writing at a path. + * + * The mode parameter can be {@code r} to read, {@code w} to write (deleting + * all contents), or {@code a} to append (keeping contents). If {@code b} is + * added to the end, the file will be opened in binary mode; otherwise, it's + * opened in text mode. + * + * @param path The path to the file to open. + * @param mode The mode to open the file with. + * @return A file handle object for the file, or {@code nil} + an error message on error. + * @throws LuaException If an invalid mode was specified. + * @cc.treturn [1] table A file handle object for the file. + * @cc.treturn [2] nil If the file does not exist, or cannot be opened. + * @cc.treturn string|nil A message explaining why the file cannot be opened. + */ + @LuaFunction + public final Object[] open( String path, String mode ) throws LuaException + { + environment.addTrackingChange( TrackingField.FS_OPS ); + try + { + switch( mode ) + { + case "r": + { + // Open the file for reading, then create a wrapper around the reader + FileSystemWrapper reader = fileSystem.openForRead( path, EncodedReadableHandle::openUtf8 ); + return new Object[] { new EncodedReadableHandle( reader.get(), reader ) }; + } + case "w": + { + // Open the file for writing, then create a wrapper around the writer + FileSystemWrapper writer = fileSystem.openForWrite( path, false, EncodedWritableHandle::openUtf8 ); + return new Object[] { new EncodedWritableHandle( writer.get(), writer ) }; + } + case "a": + { + // Open the file for appending, then create a wrapper around the writer + FileSystemWrapper writer = fileSystem.openForWrite( path, true, EncodedWritableHandle::openUtf8 ); + return new Object[] { new EncodedWritableHandle( writer.get(), writer ) }; + } + case "rb": + { + // Open the file for binary reading, then create a wrapper around the reader + FileSystemWrapper reader = fileSystem.openForRead( path, Function.identity() ); + return new Object[] { BinaryReadableHandle.of( reader.get(), reader ) }; + } + case "wb": + { + // Open the file for binary writing, then create a wrapper around the writer + FileSystemWrapper writer = fileSystem.openForWrite( path, false, Function.identity() ); + return new Object[] { BinaryWritableHandle.of( writer.get(), writer ) }; + } + case "ab": + { + // Open the file for binary appending, then create a wrapper around the reader + FileSystemWrapper writer = fileSystem.openForWrite( path, true, Function.identity() ); + return new Object[] { BinaryWritableHandle.of( writer.get(), writer ) }; + } + default: + throw new LuaException( "Unsupported mode" ); + } + } + catch( FileSystemException e ) + { + return new Object[] { null, e.getMessage() }; + } + } + + /** + * Returns the name of the mount that the specified path is located on. + * + * @param path The path to get the drive of. + * @return The name of the drive that the file is on; e.g. {@code hdd} for local files, or {@code rom} for ROM files. + * @throws LuaException If the path doesn't exist. + * @cc.treturn string The name of the drive that the file is on; e.g. {@code hdd} for local files, or {@code rom} for ROM files. + */ + @LuaFunction + public final Object[] getDrive( String path ) throws LuaException + { + try + { + return fileSystem.exists( path ) ? new Object[] { fileSystem.getMountLabel( path ) } : null; + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Returns the amount of free space available on the drive the path is + * located on. + * + * @param path The path to check the free space for. + * @return The amount of free space available, in bytes. + * @throws LuaException If the path doesn't exist. + * @cc.treturn number|"unlimited" The amount of free space available, in bytes, or "unlimited". + */ + @LuaFunction + public final Object getFreeSpace( String path ) throws LuaException + { + try + { + long freeSpace = fileSystem.getFreeSpace( path ); + return freeSpace >= 0 ? freeSpace : "unlimited"; + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Searches for files matching a string with wildcards. + * + * This string is formatted like a normal path string, but can include any + * number of wildcards ({@code *}) to look for files matching anything. + * For example, {@code rom/* /command*} will look for any path starting with + * {@code command} inside any subdirectory of {@code /rom}. + * + * @param path The wildcard-qualified path to search for. + * @return A list of paths that match the search string. + * @throws LuaException If the path doesn't exist. + */ + @LuaFunction + public final String[] find( String path ) throws LuaException + { + try + { + environment.addTrackingChange( TrackingField.FS_OPS ); + return fileSystem.find( path ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Returns true if a path is mounted to the parent filesystem. + * + * The root filesystem "/" is considered a mount, along with disk folders and the rom folder. Other programs + * (such as network shares) can extend this to make other mount types by correctly assigning their return value for + * getDrive. + * + * @param path The path of the drive to get. + * @return The drive's capacity. + * @throws LuaException If the capacity cannot be determined. + * @cc.treturn number|nil This drive's capacity. This will be nil for "read-only" drives, such as the ROM or + * treasure disks. + */ + @LuaFunction + public final Object getCapacity( String path ) throws LuaException + { + try + { + OptionalLong capacity = fileSystem.getCapacity( path ); + return capacity.isPresent() ? capacity.getAsLong() : null; + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Get attributes about a specific file or folder. + * + * The returned attributes table contains information about the size of the file, whether it is a directory, and + * when it was created and last modified. + * + * The creation and modification times are given as the number of milliseconds since the UNIX epoch. This may be + * given to {@link OSAPI#date} in order to convert it to more usable form. + * + * @param path The path to get attributes for. + * @return The resulting attributes. + * @throws LuaException If the path does not exist. + * @cc.treturn { size = number, isDir = boolean, created = number, modified = number } The resulting attributes. + * @see #getSize If you only care about the file's size. + * @see #isDir If you only care whether a path is a directory or not. + */ + @LuaFunction + public final Map attributes( String path ) throws LuaException + { + try + { + BasicFileAttributes attributes = fileSystem.getAttributes( path ); + Map result = new HashMap<>(); + result.put( "modification", getFileTime( attributes.lastModifiedTime() ) ); + result.put( "modified", getFileTime( attributes.lastModifiedTime() ) ); + result.put( "created", getFileTime( attributes.creationTime() ) ); + result.put( "size", attributes.isDirectory() ? 0 : attributes.size() ); + result.put( "isDir", attributes.isDirectory() ); + return result; + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + private static long getFileTime( FileTime time ) + { + return time == null ? 0 : time.toMillis(); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/FastLuaException.java b/src/main/java/dan200/computercraft/core/apis/FastLuaException.java new file mode 100644 index 000000000..6ef64f275 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/FastLuaException.java @@ -0,0 +1,35 @@ +/* + * 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.apis; + +import dan200.computercraft.api.lua.LuaException; + +import javax.annotation.Nullable; + +/** + * A Lua exception which does not contain its stack trace. + */ +public class FastLuaException extends LuaException +{ + private static final long serialVersionUID = 5957864899303561143L; + + public FastLuaException( @Nullable String message ) + { + super( message ); + } + + public FastLuaException( @Nullable String message, int level ) + { + super( message, level ); + } + + @Override + public synchronized Throwable fillInStackTrace() + { + return this; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java new file mode 100644 index 000000000..71163f064 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java @@ -0,0 +1,202 @@ +/* + * 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.apis; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.apis.http.*; +import dan200.computercraft.core.apis.http.request.HttpRequest; +import dan200.computercraft.core.apis.http.websocket.Websocket; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; + +import javax.annotation.Nonnull; +import java.net.URI; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import static dan200.computercraft.core.apis.TableHelper.*; + +/** + * The http library allows communicating with web servers, sending and receiving data from them. + * + * @cc.module http + * @hidden + */ +public class HTTPAPI implements ILuaAPI +{ + private final IAPIEnvironment m_apiEnvironment; + + private final ResourceGroup checkUrls = new ResourceGroup<>(); + private final ResourceGroup requests = new ResourceQueue<>( () -> ComputerCraft.httpMaxRequests ); + private final ResourceGroup websockets = new ResourceGroup<>( () -> ComputerCraft.httpMaxWebsockets ); + + public HTTPAPI( IAPIEnvironment environment ) + { + m_apiEnvironment = environment; + } + + @Override + public String[] getNames() + { + return new String[] { "http" }; + } + + @Override + public void startup() + { + checkUrls.startup(); + requests.startup(); + websockets.startup(); + } + + @Override + public void shutdown() + { + checkUrls.shutdown(); + requests.shutdown(); + websockets.shutdown(); + } + + @Override + public void update() + { + // It's rather ugly to run this here, but we need to clean up + // resources as often as possible to reduce blocking. + Resource.cleanup(); + } + + @LuaFunction + public final Object[] request( IArguments args ) throws LuaException + { + String address, postString, requestMethod; + Map headerTable; + boolean binary, redirect; + + if( args.get( 0 ) instanceof Map ) + { + Map options = args.getTable( 0 ); + address = getStringField( options, "url" ); + postString = optStringField( options, "body", null ); + headerTable = optTableField( options, "headers", Collections.emptyMap() ); + binary = optBooleanField( options, "binary", false ); + requestMethod = optStringField( options, "method", null ); + redirect = optBooleanField( options, "redirect", true ); + + } + else + { + // Get URL and post information + address = args.getString( 0 ); + postString = args.optString( 1, null ); + headerTable = args.optTable( 2, Collections.emptyMap() ); + binary = args.optBoolean( 3, false ); + requestMethod = null; + redirect = true; + } + + HttpHeaders headers = getHeaders( headerTable ); + + HttpMethod httpMethod; + if( requestMethod == null ) + { + httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST; + } + else + { + httpMethod = HttpMethod.valueOf( requestMethod.toUpperCase( Locale.ROOT ) ); + if( httpMethod == null || requestMethod.equalsIgnoreCase( "CONNECT" ) ) + { + throw new LuaException( "Unsupported HTTP method" ); + } + } + + try + { + URI uri = HttpRequest.checkUri( address ); + HttpRequest request = new HttpRequest( requests, m_apiEnvironment, address, postString, headers, binary, redirect ); + + // Make the request + request.queue( r -> r.request( uri, httpMethod ) ); + + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; + } + } + + @LuaFunction + public final Object[] checkURL( String address ) + { + try + { + URI uri = HttpRequest.checkUri( address ); + new CheckUrl( checkUrls, m_apiEnvironment, address, uri ).queue( CheckUrl::run ); + + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; + } + } + + @LuaFunction + public final Object[] websocket( String address, Optional> headerTbl ) throws LuaException + { + if( !ComputerCraft.httpWebsocketEnabled ) + { + throw new LuaException( "Websocket connections are disabled" ); + } + + HttpHeaders headers = getHeaders( headerTbl.orElse( Collections.emptyMap() ) ); + + try + { + URI uri = Websocket.checkUri( address ); + if( !new Websocket( websockets, m_apiEnvironment, uri, address, headers ).queue( Websocket::connect ) ) + { + throw new LuaException( "Too many websockets already open" ); + } + + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; + } + } + + @Nonnull + private static HttpHeaders getHeaders( @Nonnull Map headerTable ) throws LuaException + { + HttpHeaders headers = new DefaultHttpHeaders(); + for( Map.Entry entry : headerTable.entrySet() ) + { + Object value = entry.getValue(); + if( entry.getKey() instanceof String && value instanceof String ) + { + try + { + headers.add( (String) entry.getKey(), value ); + } + catch( IllegalArgumentException e ) + { + throw new LuaException( e.getMessage() ); + } + } + } + return headers; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java new file mode 100644 index 000000000..19e2d2f14 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java @@ -0,0 +1,79 @@ +/* + * 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.apis; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IWorkMonitor; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.computer.IComputerEnvironment; +import dan200.computercraft.core.filesystem.FileSystem; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.tracking.TrackingField; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public interface IAPIEnvironment +{ + String TIMER_EVENT = "timer"; + + @FunctionalInterface + interface IPeripheralChangeListener + { + void onPeripheralChanged( ComputerSide side, @Nullable IPeripheral newPeripheral ); + } + + int getComputerID(); + + @Nonnull + IComputerEnvironment getComputerEnvironment(); + + @Nonnull + IWorkMonitor getMainThreadMonitor(); + + @Nonnull + Terminal getTerminal(); + + FileSystem getFileSystem(); + + void shutdown(); + + void reboot(); + + void queueEvent( String event, Object... args ); + + void setOutput( ComputerSide side, int output ); + + int getOutput( ComputerSide side ); + + int getInput( ComputerSide side ); + + void setBundledOutput( ComputerSide side, int output ); + + int getBundledOutput( ComputerSide side ); + + int getBundledInput( ComputerSide side ); + + void setPeripheralChangeListener( @Nullable IPeripheralChangeListener listener ); + + @Nullable + IPeripheral getPeripheral( ComputerSide side ); + + String getLabel(); + + void setLabel( @Nullable String label ); + + int startTimer( long ticks ); + + void cancelTimer( int id ); + + void addTrackingChange( @Nonnull TrackingField field, long change ); + + default void addTrackingChange( @Nonnull TrackingField field ) + { + addTrackingChange( field, 1 ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java b/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java new file mode 100644 index 000000000..dc5c4fa1f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java @@ -0,0 +1,280 @@ +/* + * 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.apis; + +import dan200.computercraft.api.lua.LuaException; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.TextStyle; +import java.time.temporal.*; +import java.util.HashMap; +import java.util.Map; +import java.util.function.LongUnaryOperator; + +final class LuaDateTime +{ + private LuaDateTime() + { + } + + static void format( DateTimeFormatterBuilder formatter, String format, ZoneOffset offset ) throws LuaException + { + for( int i = 0; i < format.length(); ) + { + char c; + switch( c = format.charAt( i++ ) ) + { + case '\n': + formatter.appendLiteral( '\n' ); + break; + default: + formatter.appendLiteral( c ); + break; + case '%': + if( i >= format.length() ) break; + switch( c = format.charAt( i++ ) ) + { + default: + throw new LuaException( "bad argument #1: invalid conversion specifier '%" + c + "'" ); + + case '%': + formatter.appendLiteral( '%' ); + break; + case 'a': + formatter.appendText( ChronoField.DAY_OF_WEEK, TextStyle.SHORT ); + break; + case 'A': + formatter.appendText( ChronoField.DAY_OF_WEEK, TextStyle.FULL ); + break; + case 'b': + case 'h': + formatter.appendText( ChronoField.MONTH_OF_YEAR, TextStyle.SHORT ); + break; + case 'B': + formatter.appendText( ChronoField.MONTH_OF_YEAR, TextStyle.FULL ); + break; + case 'c': + format( formatter, "%a %b %e %H:%M:%S %Y", offset ); + break; + case 'C': + formatter.appendValueReduced( CENTURY, 2, 2, 0 ); + break; + case 'd': + formatter.appendValue( ChronoField.DAY_OF_MONTH, 2 ); + break; + case 'D': + case 'x': + format( formatter, "%m/%d/%y", offset ); + break; + case 'e': + formatter.padNext( 2 ).appendValue( ChronoField.DAY_OF_MONTH ); + break; + case 'F': + format( formatter, "%Y-%m-%d", offset ); + break; + case 'g': + formatter.appendValueReduced( IsoFields.WEEK_BASED_YEAR, 2, 2, 0 ); + break; + case 'G': + formatter.appendValue( IsoFields.WEEK_BASED_YEAR ); + break; + case 'H': + formatter.appendValue( ChronoField.HOUR_OF_DAY, 2 ); + break; + case 'I': + formatter.appendValue( ChronoField.HOUR_OF_AMPM ); + break; + case 'j': + formatter.appendValue( ChronoField.DAY_OF_YEAR, 3 ); + break; + case 'm': + formatter.appendValue( ChronoField.MONTH_OF_YEAR, 2 ); + break; + case 'M': + formatter.appendValue( ChronoField.MINUTE_OF_HOUR, 2 ); + break; + case 'n': + formatter.appendLiteral( '\n' ); + break; + case 'p': + formatter.appendText( ChronoField.AMPM_OF_DAY ); + break; + case 'r': + format( formatter, "%I:%M:%S %p", offset ); + break; + case 'R': + format( formatter, "%H:%M", offset ); + break; + case 'S': + formatter.appendValue( ChronoField.SECOND_OF_MINUTE, 2 ); + break; + case 't': + formatter.appendLiteral( '\t' ); + break; + case 'T': + case 'X': + format( formatter, "%H:%M:%S", offset ); + break; + case 'u': + formatter.appendValue( ChronoField.DAY_OF_WEEK ); + break; + case 'U': + formatter.appendValue( ChronoField.ALIGNED_WEEK_OF_YEAR, 2 ); + break; + case 'V': + formatter.appendValue( IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2 ); + break; + case 'w': + formatter.appendValue( ZERO_WEEK ); + break; + case 'W': + formatter.appendValue( WeekFields.ISO.weekOfYear(), 2 ); + break; + case 'y': + formatter.appendValueReduced( ChronoField.YEAR, 2, 2, 0 ); + break; + case 'Y': + formatter.appendValue( ChronoField.YEAR ); + break; + case 'z': + formatter.appendOffset( "+HHMM", "+0000" ); + break; + case 'Z': + formatter.appendChronologyId(); + break; + } + } + } + } + + static long fromTable( Map table ) throws LuaException + { + int year = getField( table, "year", -1 ); + int month = getField( table, "month", -1 ); + int day = getField( table, "day", -1 ); + int hour = getField( table, "hour", 12 ); + int minute = getField( table, "min", 12 ); + int second = getField( table, "sec", 12 ); + LocalDateTime time = LocalDateTime.of( year, month, day, hour, minute, second ); + + Boolean isDst = getBoolField( table, "isdst" ); + if( isDst != null ) + { + boolean requireDst = isDst; + for( ZoneOffset possibleOffset : ZoneOffset.systemDefault().getRules().getValidOffsets( time ) ) + { + Instant instant = time.toInstant( possibleOffset ); + if( possibleOffset.getRules().getDaylightSavings( instant ).isZero() == requireDst ) + { + return instant.getEpochSecond(); + } + } + } + + ZoneOffset offset = ZoneOffset.systemDefault().getRules().getOffset( time ); + return time.toInstant( offset ).getEpochSecond(); + } + + static Map toTable( TemporalAccessor date, ZoneId offset, Instant instant ) + { + HashMap table = new HashMap<>( 9 ); + table.put( "year", date.getLong( ChronoField.YEAR ) ); + table.put( "month", date.getLong( ChronoField.MONTH_OF_YEAR ) ); + table.put( "day", date.getLong( ChronoField.DAY_OF_MONTH ) ); + table.put( "hour", date.getLong( ChronoField.HOUR_OF_DAY ) ); + table.put( "min", date.getLong( ChronoField.MINUTE_OF_HOUR ) ); + table.put( "sec", date.getLong( ChronoField.SECOND_OF_MINUTE ) ); + table.put( "wday", date.getLong( WeekFields.SUNDAY_START.dayOfWeek() ) ); + table.put( "yday", date.getLong( ChronoField.DAY_OF_YEAR ) ); + table.put( "isdst", offset.getRules().isDaylightSavings( instant ) ); + return table; + } + + private static int getField( Map table, String field, int def ) throws LuaException + { + Object value = table.get( field ); + if( value instanceof Number ) return ((Number) value).intValue(); + if( def < 0 ) throw new LuaException( "field \"" + field + "\" missing in date table" ); + return def; + } + + private static Boolean getBoolField( Map table, String field ) throws LuaException + { + Object value = table.get( field ); + if( value instanceof Boolean || value == null ) return (Boolean) value; + throw new LuaException( "field \"" + field + "\" missing in date table" ); + } + + private static final TemporalField CENTURY = map( ChronoField.YEAR, ValueRange.of( 0, 6 ), x -> (x / 100) % 100 ); + private static final TemporalField ZERO_WEEK = map( WeekFields.SUNDAY_START.dayOfWeek(), ValueRange.of( 0, 6 ), x -> x - 1 ); + + private static TemporalField map( TemporalField field, ValueRange range, LongUnaryOperator convert ) + { + return new TemporalField() + { + private final ValueRange range = ValueRange.of( 0, 99 ); + + @Override + public TemporalUnit getBaseUnit() + { + return field.getBaseUnit(); + } + + @Override + public TemporalUnit getRangeUnit() + { + return field.getRangeUnit(); + } + + @Override + public ValueRange range() + { + return range; + } + + @Override + public boolean isDateBased() + { + return field.isDateBased(); + } + + @Override + public boolean isTimeBased() + { + return field.isTimeBased(); + } + + @Override + public boolean isSupportedBy( TemporalAccessor temporal ) + { + return field.isSupportedBy( temporal ); + } + + @Override + public ValueRange rangeRefinedBy( TemporalAccessor temporal ) + { + return range; + } + + @Override + public long getFrom( TemporalAccessor temporal ) + { + return convert.applyAsLong( temporal.getLong( field ) ); + } + + @Override + @SuppressWarnings( "unchecked" ) + public R adjustInto( R temporal, long newValue ) + { + return (R) temporal.with( field, newValue ); + } + }; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/src/main/java/dan200/computercraft/core/apis/OSAPI.java new file mode 100644 index 000000000..d2e3cbb71 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/OSAPI.java @@ -0,0 +1,462 @@ +/* + * 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.apis; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.shared.util.StringUtil; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import javax.annotation.Nonnull; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatterBuilder; +import java.util.*; + +import static dan200.computercraft.api.lua.LuaValues.checkFinite; + +/** + * The {@link OSAPI} API allows interacting with the current computer. + * + * @cc.module os + */ +public class OSAPI implements ILuaAPI +{ + private final IAPIEnvironment apiEnvironment; + + private final Int2ObjectMap m_alarms = new Int2ObjectOpenHashMap<>(); + private int m_clock; + private double m_time; + private int m_day; + + private int m_nextAlarmToken = 0; + + private static class Alarm implements Comparable + { + final double m_time; + final int m_day; + + Alarm( double time, int day ) + { + m_time = time; + m_day = day; + } + + @Override + public int compareTo( @Nonnull Alarm o ) + { + double t = m_day * 24.0 + m_time; + double ot = m_day * 24.0 + m_time; + return Double.compare( t, ot ); + } + } + + public OSAPI( IAPIEnvironment environment ) + { + apiEnvironment = environment; + } + + @Override + public String[] getNames() + { + return new String[] { "os" }; + } + + @Override + public void startup() + { + m_time = apiEnvironment.getComputerEnvironment().getTimeOfDay(); + m_day = apiEnvironment.getComputerEnvironment().getDay(); + m_clock = 0; + + synchronized( m_alarms ) + { + m_alarms.clear(); + } + } + + @Override + public void update() + { + m_clock++; + + // Wait for all of our alarms + synchronized( m_alarms ) + { + double previousTime = m_time; + int previousDay = m_day; + double time = apiEnvironment.getComputerEnvironment().getTimeOfDay(); + int day = apiEnvironment.getComputerEnvironment().getDay(); + + if( time > previousTime || day > previousDay ) + { + double now = m_day * 24.0 + m_time; + Iterator> it = m_alarms.int2ObjectEntrySet().iterator(); + while( it.hasNext() ) + { + Int2ObjectMap.Entry entry = it.next(); + Alarm alarm = entry.getValue(); + double t = alarm.m_day * 24.0 + alarm.m_time; + if( now >= t ) + { + apiEnvironment.queueEvent( "alarm", entry.getIntKey() ); + it.remove(); + } + } + } + + m_time = time; + m_day = day; + } + } + + @Override + public void shutdown() + { + synchronized( m_alarms ) + { + m_alarms.clear(); + } + } + + private static float getTimeForCalendar( Calendar c ) + { + float time = c.get( Calendar.HOUR_OF_DAY ); + time += c.get( Calendar.MINUTE ) / 60.0f; + time += c.get( Calendar.SECOND ) / (60.0f * 60.0f); + return time; + } + + private static int getDayForCalendar( Calendar c ) + { + GregorianCalendar g = c instanceof GregorianCalendar ? (GregorianCalendar) c : new GregorianCalendar(); + int year = c.get( Calendar.YEAR ); + int day = 0; + for( int y = 1970; y < year; y++ ) + { + day += g.isLeapYear( y ) ? 366 : 365; + } + day += c.get( Calendar.DAY_OF_YEAR ); + return day; + } + + private static long getEpochForCalendar( Calendar c ) + { + return c.getTime().getTime(); + } + + /** + * Adds an event to the event queue. This event can later be pulled with + * os.pullEvent. + * + * @param name The name of the event to queue. + * @param args The parameters of the event. + * @cc.tparam string name The name of the event to queue. + * @cc.param ... The parameters of the event. + * @cc.see os.pullEvent To pull the event queued + */ + @LuaFunction + public final void queueEvent( String name, IArguments args ) + { + apiEnvironment.queueEvent( name, args.drop( 1 ).getAll() ); + } + + /** + * Starts a timer that will run for the specified number of seconds. Once + * the timer fires, a timer event will be added to the queue with the ID + * returned from this function as the first parameter. + * + * @param timer The number of seconds until the timer fires. + * @return The ID of the new timer. + * @throws LuaException If the time is below zero. + */ + @LuaFunction + public final int startTimer( double timer ) throws LuaException + { + return apiEnvironment.startTimer( Math.round( checkFinite( 0, timer ) / 0.05 ) ); + } + + /** + * Cancels a timer previously started with startTimer. This will stop the + * timer from firing. + * + * @param token The ID of the timer to cancel. + * @see #startTimer To start a timer. + */ + @LuaFunction + public final void cancelTimer( int token ) + { + apiEnvironment.cancelTimer( token ); + } + + /** + * Sets an alarm that will fire at the specified world time. When it fires, + * an alarm event will be added to the event queue. + * + * @param time The time at which to fire the alarm, in the range [0.0, 24.0). + * @return The ID of the alarm that was set. + * @throws LuaException If the time is out of range. + */ + @LuaFunction + public final int setAlarm( double time ) throws LuaException + { + checkFinite( 0, time ); + if( time < 0.0 || time >= 24.0 ) throw new LuaException( "Number out of range" ); + synchronized( m_alarms ) + { + int day = time > m_time ? m_day : m_day + 1; + m_alarms.put( m_nextAlarmToken, new Alarm( time, day ) ); + return m_nextAlarmToken++; + } + } + + /** + * Cancels an alarm previously started with setAlarm. This will stop the + * alarm from firing. + * + * @param token The ID of the alarm to cancel. + * @see #setAlarm To set an alarm. + */ + @LuaFunction + public final void cancelAlarm( int token ) + { + synchronized( m_alarms ) + { + m_alarms.remove( token ); + } + } + + /** + * Shuts down the computer immediately. + */ + @LuaFunction( "shutdown" ) + public final void doShutdown() + { + apiEnvironment.shutdown(); + } + + /** + * Reboots the computer immediately. + */ + @LuaFunction( "reboot" ) + public final void doReboot() + { + apiEnvironment.reboot(); + } + + /** + * Returns the ID of the computer. + * + * @return The ID of the computer. + */ + @LuaFunction( { "getComputerID", "computerID" } ) + public final int getComputerID() + { + return apiEnvironment.getComputerID(); + } + + /** + * Returns the label of the computer, or {@code nil} if none is set. + * + * @return The label of the computer. + * @cc.treturn string The label of the computer. + */ + @LuaFunction( { "getComputerLabel", "computerLabel" } ) + public final Object[] getComputerLabel() + { + String label = apiEnvironment.getLabel(); + return label == null ? null : new Object[] { label }; + } + + /** + * Set the label of this computer. + * + * @param label The new label. May be {@code nil} in order to clear it. + */ + @LuaFunction + public final void setComputerLabel( Optional label ) + { + apiEnvironment.setLabel( StringUtil.normaliseLabel( label.orElse( null ) ) ); + } + + /** + * Returns the number of seconds that the computer has been running. + * + * @return The computer's uptime. + */ + @LuaFunction + public final double clock() + { + return m_clock * 0.05; + } + + /** + * Returns the current time depending on the string passed in. This will + * always be in the range [0.0, 24.0). + * + * * If called with {@code ingame}, the current world time will be returned. + * This is the default if nothing is passed. + * * If called with {@code utc}, returns the hour of the day in UTC time. + * * If called with {@code local}, returns the hour of the day in the + * timezone the server is located in. + * + * This function can also be called with a table returned from {@link #date}, + * which will convert the date fields into a UNIX timestamp (number of + * seconds since 1 January 1970). + * + * @param args The locale of the time, or a table filled by {@code os.date("*t")} to decode. Defaults to {@code ingame} locale if not specified. + * @return The hour of the selected locale, or a UNIX timestamp from the table, depending on the argument passed in. + * @throws LuaException If an invalid locale is passed. + * @cc.tparam [opt] string|table locale The locale of the time, or a table filled by {@code os.date("*t")} to decode. Defaults to {@code ingame} locale if not specified. + * @see #date To get a date table that can be converted with this function. + */ + @LuaFunction + public final Object time( IArguments args ) throws LuaException + { + Object value = args.get( 0 ); + if( value instanceof Map ) return LuaDateTime.fromTable( (Map) value ); + + String param = args.optString( 0, "ingame" ); + switch( param.toLowerCase( Locale.ROOT ) ) + { + case "utc": // Get Hour of day (UTC) + return getTimeForCalendar( Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ) ); + case "local": // Get Hour of day (local time) + return getTimeForCalendar( Calendar.getInstance() ); + case "ingame": // Get in-game hour + return m_time; + default: + throw new LuaException( "Unsupported operation" ); + } + } + + /** + * Returns the day depending on the locale specified. + * + * * If called with {@code ingame}, returns the number of days since the + * world was created. This is the default. + * * If called with {@code utc}, returns the number of days since 1 January + * 1970 in the UTC timezone. + * * If called with {@code local}, returns the number of days since 1 + * January 1970 in the server's local timezone. + * + * @param args The locale to get the day for. Defaults to {@code ingame} if not set. + * @return The day depending on the selected locale. + * @throws LuaException If an invalid locale is passed. + */ + @LuaFunction + public final int day( Optional args ) throws LuaException + { + switch( args.orElse( "ingame" ).toLowerCase( Locale.ROOT ) ) + { + case "utc": // Get numbers of days since 1970-01-01 (utc) + return getDayForCalendar( Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ) ); + case "local": // Get numbers of days since 1970-01-01 (local time) + return getDayForCalendar( Calendar.getInstance() ); + case "ingame":// Get game day + return m_day; + default: + throw new LuaException( "Unsupported operation" ); + } + } + + /** + * Returns the number of seconds since an epoch depending on the locale. + * + * * If called with {@code ingame}, returns the number of seconds since the + * world was created. This is the default. + * * If called with {@code utc}, returns the number of seconds since 1 + * January 1970 in the UTC timezone. + * * If called with {@code local}, returns the number of seconds since 1 + * January 1970 in the server's local timezone. + * + * @param args The locale to get the seconds for. Defaults to {@code ingame} if not set. + * @return The seconds since the epoch depending on the selected locale. + * @throws LuaException If an invalid locale is passed. + */ + @LuaFunction + public final long epoch( Optional args ) throws LuaException + { + switch( args.orElse( "ingame" ).toLowerCase( Locale.ROOT ) ) + { + case "utc": + { + // Get utc epoch + Calendar c = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); + return getEpochForCalendar( c ); + } + case "local": + { + // Get local epoch + Calendar c = Calendar.getInstance(); + return getEpochForCalendar( c ); + } + case "ingame": + // Get in-game epoch + synchronized( m_alarms ) + { + return m_day * 86400000 + (int) (m_time * 3600000.0f); + } + default: + throw new LuaException( "Unsupported operation" ); + } + } + + /** + * Returns a date string (or table) using a specified format string and + * optional time to format. + * + * The format string takes the same formats as C's {@code strftime} function + * (http://www.cplusplus.com/reference/ctime/strftime/). In extension, it + * can be prefixed with an exclamation mark ({@code !}) to use UTC time + * instead of the server's local timezone. + * + * If the format is exactly {@code *t} (optionally prefixed with {@code !}), a + * table will be returned instead. This table has fields for the year, month, + * day, hour, minute, second, day of the week, day of the year, and whether + * Daylight Savings Time is in effect. This table can be converted to a UNIX + * timestamp (days since 1 January 1970) with {@link #date}. + * + * @param formatA The format of the string to return. This defaults to {@code %c}, which expands to a string similar to "Sat Dec 24 16:58:00 2011". + * @param timeA The time to convert to a string. This defaults to the current time. + * @return The resulting format string. + * @throws LuaException If an invalid format is passed. + */ + @LuaFunction + public final Object date( Optional formatA, Optional timeA ) throws LuaException + { + String format = formatA.orElse( "%c" ); + long time = timeA.orElseGet( () -> Instant.now().getEpochSecond() ); + + Instant instant = Instant.ofEpochSecond( time ); + ZonedDateTime date; + ZoneOffset offset; + if( format.startsWith( "!" ) ) + { + offset = ZoneOffset.UTC; + date = ZonedDateTime.ofInstant( instant, offset ); + format = format.substring( 1 ); + } + else + { + ZoneId id = ZoneId.systemDefault(); + offset = id.getRules().getOffset( instant ); + date = ZonedDateTime.ofInstant( instant, id ); + } + + if( format.equals( "*t" ) ) return LuaDateTime.toTable( date, offset, instant ); + + DateTimeFormatterBuilder formatter = new DateTimeFormatterBuilder(); + LuaDateTime.format( formatter, format, offset ); + return formatter.toFormatter( Locale.ROOT ).format( date ); + } + +} diff --git a/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java b/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java new file mode 100644 index 000000000..b132ad48c --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java @@ -0,0 +1,368 @@ +/* + * 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.apis; + +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.*; +import dan200.computercraft.api.peripheral.IDynamicPeripheral; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IWorkMonitor; +import dan200.computercraft.api.peripheral.NotAttachedException; +import dan200.computercraft.core.asm.LuaMethod; +import dan200.computercraft.core.asm.NamedMethod; +import dan200.computercraft.core.asm.PeripheralMethod; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.tracking.TrackingField; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; + +/** + * CC's "native" peripheral API. This is wrapped within CraftOS to provide a version which works with modems. + * + * @cc.module peripheral + * @hidden + */ +public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener +{ + private class PeripheralWrapper extends ComputerAccess + { + private final String side; + private final IPeripheral peripheral; + + private final String type; + private final Map methodMap; + private boolean attached; + + PeripheralWrapper( IPeripheral peripheral, String side ) + { + super( environment ); + this.side = side; + this.peripheral = peripheral; + attached = false; + + type = Objects.requireNonNull( peripheral.getType(), "Peripheral type cannot be null" ); + + methodMap = PeripheralAPI.getMethods( peripheral ); + } + + public IPeripheral getPeripheral() + { + return peripheral; + } + + public String getType() + { + return type; + } + + public Collection getMethods() + { + return methodMap.keySet(); + } + + public synchronized boolean isAttached() + { + return attached; + } + + public synchronized void attach() + { + attached = true; + peripheral.attach( this ); + } + + public void detach() + { + // Call detach + peripheral.detach( this ); + + synchronized( this ) + { + // Unmount everything the detach function forgot to do + unmountAll(); + } + + attached = false; + } + + public MethodResult call( ILuaContext context, String methodName, IArguments arguments ) throws LuaException + { + PeripheralMethod method; + synchronized( this ) + { + method = methodMap.get( methodName ); + } + + if( method == null ) throw new LuaException( "No such method " + methodName ); + + environment.addTrackingChange( TrackingField.PERIPHERAL_OPS ); + return method.apply( peripheral, context, this, arguments ); + } + + // IComputerAccess implementation + @Override + public synchronized String mount( @Nonnull String desiredLoc, @Nonnull IMount mount, @Nonnull String driveName ) + { + if( !attached ) throw new NotAttachedException(); + return super.mount( desiredLoc, mount, driveName ); + } + + @Override + public synchronized String mountWritable( @Nonnull String desiredLoc, @Nonnull IWritableMount mount, @Nonnull String driveName ) + { + if( !attached ) throw new NotAttachedException(); + return super.mountWritable( desiredLoc, mount, driveName ); + } + + @Override + public synchronized void unmount( String location ) + { + if( !attached ) throw new NotAttachedException(); + super.unmount( location ); + } + + @Override + public int getID() + { + if( !attached ) throw new NotAttachedException(); + return super.getID(); + } + + @Override + public void queueEvent( @Nonnull String event, Object... arguments ) + { + if( !attached ) throw new NotAttachedException(); + super.queueEvent( event, arguments ); + } + + @Nonnull + @Override + public String getAttachmentName() + { + if( !attached ) throw new NotAttachedException(); + return side; + } + + @Nonnull + @Override + public Map getAvailablePeripherals() + { + if( !attached ) throw new NotAttachedException(); + + Map peripherals = new HashMap<>(); + for( PeripheralWrapper wrapper : PeripheralAPI.this.peripherals ) + { + if( wrapper != null && wrapper.isAttached() ) + { + peripherals.put( wrapper.getAttachmentName(), wrapper.getPeripheral() ); + } + } + + return Collections.unmodifiableMap( peripherals ); + } + + @Nullable + @Override + public IPeripheral getAvailablePeripheral( @Nonnull String name ) + { + if( !attached ) throw new NotAttachedException(); + + for( PeripheralWrapper wrapper : peripherals ) + { + if( wrapper != null && wrapper.isAttached() && wrapper.getAttachmentName().equals( name ) ) + { + return wrapper.getPeripheral(); + } + } + return null; + } + + @Nonnull + @Override + public IWorkMonitor getMainThreadMonitor() + { + if( !attached ) throw new NotAttachedException(); + return super.getMainThreadMonitor(); + } + } + + private final IAPIEnvironment environment; + private final PeripheralWrapper[] peripherals = new PeripheralWrapper[6]; + private boolean running; + + public PeripheralAPI( IAPIEnvironment environment ) + { + this.environment = environment; + this.environment.setPeripheralChangeListener( this ); + running = false; + } + + // IPeripheralChangeListener + + @Override + public void onPeripheralChanged( ComputerSide side, IPeripheral newPeripheral ) + { + synchronized( peripherals ) + { + int index = side.ordinal(); + if( peripherals[index] != null ) + { + // Queue a detachment + final PeripheralWrapper wrapper = peripherals[index]; + if( wrapper.isAttached() ) wrapper.detach(); + + // Queue a detachment event + environment.queueEvent( "peripheral_detach", side.getName() ); + } + + // Assign the new peripheral + peripherals[index] = newPeripheral == null ? null + : new PeripheralWrapper( newPeripheral, side.getName() ); + + if( peripherals[index] != null ) + { + // Queue an attachment + final PeripheralWrapper wrapper = peripherals[index]; + if( running && !wrapper.isAttached() ) wrapper.attach(); + + // Queue an attachment event + environment.queueEvent( "peripheral", side.getName() ); + } + } + } + + @Override + public String[] getNames() + { + return new String[] { "peripheral" }; + } + + @Override + public void startup() + { + synchronized( peripherals ) + { + running = true; + for( int i = 0; i < 6; i++ ) + { + PeripheralWrapper wrapper = peripherals[i]; + if( wrapper != null && !wrapper.isAttached() ) wrapper.attach(); + } + } + } + + @Override + public void shutdown() + { + synchronized( peripherals ) + { + running = false; + for( int i = 0; i < 6; i++ ) + { + PeripheralWrapper wrapper = peripherals[i]; + if( wrapper != null && wrapper.isAttached() ) + { + wrapper.detach(); + } + } + } + } + + @LuaFunction + public final boolean isPresent( String sideName ) + { + ComputerSide side = ComputerSide.valueOfInsensitive( sideName ); + if( side != null ) + { + synchronized( peripherals ) + { + PeripheralWrapper p = peripherals[side.ordinal()]; + if( p != null ) return true; + } + } + return false; + } + + @LuaFunction + public final Object[] getType( String sideName ) + { + ComputerSide side = ComputerSide.valueOfInsensitive( sideName ); + if( side == null ) return null; + + synchronized( peripherals ) + { + PeripheralWrapper p = peripherals[side.ordinal()]; + if( p != null ) return new Object[] { p.getType() }; + } + return null; + } + + @LuaFunction + public final Object[] getMethods( String sideName ) + { + ComputerSide side = ComputerSide.valueOfInsensitive( sideName ); + if( side == null ) return null; + + synchronized( peripherals ) + { + PeripheralWrapper p = peripherals[side.ordinal()]; + if( p != null ) return new Object[] { p.getMethods() }; + } + return null; + } + + @LuaFunction + public final MethodResult call( ILuaContext context, IArguments args ) throws LuaException + { + ComputerSide side = ComputerSide.valueOfInsensitive( args.getString( 0 ) ); + String methodName = args.getString( 1 ); + IArguments methodArgs = args.drop( 2 ); + + if( side == null ) throw new LuaException( "No peripheral attached" ); + + PeripheralWrapper p; + synchronized( peripherals ) + { + p = peripherals[side.ordinal()]; + } + if( p == null ) throw new LuaException( "No peripheral attached" ); + + try + { + return p.call( context, methodName, methodArgs ).adjustError( 1 ); + } + catch( LuaException e ) + { + // We increase the error level by one in order to shift the error level to where peripheral.call was + // invoked. It would be possible to do it in Lua code, but would add significantly more overhead. + if( e.getLevel() > 0 ) throw new FastLuaException( e.getMessage(), e.getLevel() + 1 ); + throw e; + } + } + + public static Map getMethods( IPeripheral peripheral ) + { + String[] dynamicMethods = peripheral instanceof IDynamicPeripheral + ? Objects.requireNonNull( ((IDynamicPeripheral) peripheral).getMethodNames(), "Peripheral methods cannot be null" ) + : LuaMethod.EMPTY_METHODS; + + List> methods = PeripheralMethod.GENERATOR.getMethods( peripheral.getClass() ); + + Map methodMap = new HashMap<>( methods.size() + dynamicMethods.length ); + for( int i = 0; i < dynamicMethods.length; i++ ) + { + methodMap.put( dynamicMethods[i], PeripheralMethod.DYNAMIC.get( i ) ); + } + for( NamedMethod method : methods ) + { + methodMap.put( method.getName(), method.getMethod() ); + } + return methodMap; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java b/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java new file mode 100644 index 000000000..ab096fd08 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java @@ -0,0 +1,215 @@ +/* + * 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.apis; + +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.computer.ComputerSide; + +/** + * Interact with redstone attached to this computer. + * + * The {@link RedstoneAPI} library exposes three "types" of redstone control: + * - Binary input/output ({@link #setOutput}/{@link #getInput}): These simply check if a redstone wire has any input or + * output. A signal strength of 1 and 15 are treated the same. + * - Analogue input/output ({@link #setAnalogOutput}/{@link #getAnalogInput}): These work with the actual signal + * strength of the redstone wired, from 0 to 15. + * - Bundled cables ({@link #setBundledOutput}/{@link #getBundledInput}): These interact with "bundled" cables, such + * as those from Project:Red. These allow you to send 16 separate on/off signals. Each channel corresponds to a + * colour, with the first being @{colors.white} and the last @{colors.black}. + * + * Whenever a redstone input changes, a {@code redstone} event will be fired. This may be used instead of repeativly + * polling. + * + * This module may also be referred to as {@code rs}. For example, one may call {@code rs.getSides()} instead of + * {@link #getSides}. + * + * @cc.usage Toggle the redstone signal above the computer every 0.5 seconds. + * + *
+ * while true do
+ *   redstone.setOutput("top", not redstone.getOutput("top"))
+ *   sleep(0.5)
+ * end
+ * 
+ * @cc.usage Mimic a redstone comparator in [subtraction mode][comparator]. + * + *
+ * while true do
+ *   local rear = rs.getAnalogueInput("back")
+ *   local sides = math.max(rs.getAnalogueInput("left"), rs.getAnalogueInput("right"))
+ *   rs.setAnalogueOutput("front", math.max(rear - sides, 0))
+ *
+ *   os.pullEvent("redstone") -- Wait for a change to inputs.
+ * end
+ * 
+ * + * [comparator]: https://minecraft.gamepedia.com/Redstone_Comparator#Subtract_signal_strength "Redstone Comparator on + * the Minecraft wiki." + * @cc.module redstone + */ +public class RedstoneAPI implements ILuaAPI +{ + private final IAPIEnvironment environment; + + public RedstoneAPI( IAPIEnvironment environment ) + { + this.environment = environment; + } + + @Override + public String[] getNames() + { + return new String[] { "rs", "redstone" }; + } + + /** + * Returns a table containing the six sides of the computer. Namely, "top", "bottom", "left", "right", "front" and + * "back". + * + * @return A table of valid sides. + */ + @LuaFunction + public final String[] getSides() + { + return ComputerSide.NAMES; + } + + /** + * Turn the redstone signal of a specific side on or off. + * + * @param side The side to set. + * @param on Whether the redstone signal should be on or off. When on, a signal strength of 15 is emitted. + */ + @LuaFunction + public final void setOutput( ComputerSide side, boolean on ) + { + environment.setOutput( side, on ? 15 : 0 ); + } + + /** + * Get the current redstone output of a specific side. + * + * @param side The side to get. + * @return Whether the redstone output is on or off. + * @see #setOutput + */ + @LuaFunction + public final boolean getOutput( ComputerSide side ) + { + return environment.getOutput( side ) > 0; + } + + /** + * Get the current redstone input of a specific side. + * + * @param side The side to get. + * @return Whether the redstone input is on or off. + */ + @LuaFunction + public final boolean getInput( ComputerSide side ) + { + return environment.getInput( side ) > 0; + } + + /** + * Set the redstone signal strength for a specific side. + * + * @param side The side to set. + * @param value The signal strength between 0 and 15. + * @throws LuaException If {@code value} is not betwene 0 and 15. + */ + @LuaFunction( { "setAnalogOutput", "setAnalogueOutput" } ) + public final void setAnalogOutput( ComputerSide side, int value ) throws LuaException + { + if( value < 0 || value > 15 ) throw new LuaException( "Expected number in range 0-15" ); + environment.setOutput( side, value ); + } + + /** + * Get the redstone output signal strength for a specific side. + * + * @param side The side to get. + * @return The output signal strength, between 0 and 15. + * @see #setAnalogOutput + */ + @LuaFunction( { "getAnalogOutput", "getAnalogueOutput" } ) + public final int getAnalogOutput( ComputerSide side ) + { + return environment.getOutput( side ); + } + + /** + * Get the redstone input signal strength for a specific side. + * + * @param side The side to get. + * @return The input signal strength, between 0 and 15. + */ + @LuaFunction( { "getAnalogInput", "getAnalogueInput" } ) + public final int getAnalogInput( ComputerSide side ) + { + return environment.getInput( side ); + } + + /** + * Set the bundled cable output for a specific side. + * + * @param side The side to set. + * @param output The colour bitmask to set. + * @cc.see colors.subtract For removing a colour from the bitmask. + * @cc.see colors.combine For adding a color to the bitmask. + */ + @LuaFunction + public final void setBundledOutput( ComputerSide side, int output ) + { + environment.setBundledOutput( side, output ); + } + + /** + * Get the bundled cable output for a specific side. + * + * @param side The side to get. + * @return The bundle cable's output. + */ + @LuaFunction + public final int getBundledOutput( ComputerSide side ) + { + return environment.getBundledOutput( side ); + } + + /** + * Get the bundled cable input for a specific side. + * + * @param side The side to get. + * @return The bundle cable's input. + * @see #testBundledInput To determine if a specific colour is set. + */ + @LuaFunction + public final int getBundledInput( ComputerSide side ) + { + return environment.getBundledOutput( side ); + } + + /** + * Determine if a specific combination of colours are on for the given side. + * + * @param side The side to test. + * @param mask The mask to test. + * @return If the colours are on. + * @cc.usage Check if @{colors.white} and @{colors.black} are on above the computer. + *
+     * print(redstone.testBundledInput("top", colors.combine(colors.white, colors.black)))
+     * 
+ * @see #getBundledInput + */ + @LuaFunction + public final boolean testBundledInput( ComputerSide side, int mask ) + { + int input = environment.getBundledInput( side ); + return (input & mask) == mask; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/TableHelper.java b/src/main/java/dan200/computercraft/core/apis/TableHelper.java new file mode 100644 index 000000000..296bfb0a1 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/TableHelper.java @@ -0,0 +1,208 @@ +/* + * 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.apis; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaValues; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Map; + +import static dan200.computercraft.api.lua.LuaValues.getNumericType; + +/** + * Various helpers for tables. + */ +public final class TableHelper +{ + private TableHelper() + { + throw new IllegalStateException( "Cannot instantiate singleton " + getClass().getName() ); + } + + @Nonnull + public static LuaException badKey( @Nonnull String key, @Nonnull String expected, @Nullable Object actual ) + { + return badKey( key, expected, LuaValues.getType( actual ) ); + } + + @Nonnull + public static LuaException badKey( @Nonnull String key, @Nonnull String expected, @Nonnull String actual ) + { + return new LuaException( "bad field '" + key + "' (" + expected + " expected, got " + actual + ")" ); + } + + public static double getNumberField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + Object value = table.get( key ); + if( value instanceof Number ) + { + return ((Number) value).doubleValue(); + } + else + { + throw badKey( key, "number", value ); + } + } + + public static int getIntField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + Object value = table.get( key ); + if( value instanceof Number ) + { + return (int) ((Number) value).longValue(); + } + else + { + throw badKey( key, "number", value ); + } + } + + public static double getRealField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + return checkReal( key, getNumberField( table, key ) ); + } + + public static boolean getBooleanField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + Object value = table.get( key ); + if( value instanceof Boolean ) + { + return (Boolean) value; + } + else + { + throw badKey( key, "boolean", value ); + } + } + + @Nonnull + public static String getStringField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + Object value = table.get( key ); + if( value instanceof String ) + { + return (String) value; + } + else + { + throw badKey( key, "string", value ); + } + } + + @SuppressWarnings( "unchecked" ) + @Nonnull + public static Map getTableField( @Nonnull Map table, @Nonnull String key ) throws LuaException + { + Object value = table.get( key ); + if( value instanceof Map ) + { + return (Map) value; + } + else + { + throw badKey( key, "table", value ); + } + } + + public static double optNumberField( @Nonnull Map table, @Nonnull String key, double def ) throws LuaException + { + Object value = table.get( key ); + if( value == null ) + { + return def; + } + else if( value instanceof Number ) + { + return ((Number) value).doubleValue(); + } + else + { + throw badKey( key, "number", value ); + } + } + + public static int optIntField( @Nonnull Map table, @Nonnull String key, int def ) throws LuaException + { + Object value = table.get( key ); + if( value == null ) + { + return def; + } + else if( value instanceof Number ) + { + return (int) ((Number) value).longValue(); + } + else + { + throw badKey( key, "number", value ); + } + } + + public static double optRealField( @Nonnull Map table, @Nonnull String key, double def ) throws LuaException + { + return checkReal( key, optNumberField( table, key, def ) ); + } + + public static boolean optBooleanField( @Nonnull Map table, @Nonnull String key, boolean def ) throws LuaException + { + Object value = table.get( key ); + if( value == null ) + { + return def; + } + else if( value instanceof Boolean ) + { + return (Boolean) value; + } + else + { + throw badKey( key, "boolean", value ); + } + } + + public static String optStringField( @Nonnull Map table, @Nonnull String key, String def ) throws LuaException + { + Object value = table.get( key ); + if( value == null ) + { + return def; + } + else if( value instanceof String ) + { + return (String) value; + } + else + { + throw badKey( key, "string", value ); + } + } + + @SuppressWarnings( "unchecked" ) + public static Map optTableField( @Nonnull Map table, @Nonnull String key, Map def ) throws LuaException + { + Object value = table.get( key ); + if( value == null ) + { + return def; + } + else if( value instanceof Map ) + { + return (Map) value; + } + else + { + throw badKey( key, "table", value ); + } + } + + private static double checkReal( @Nonnull String key, double value ) throws LuaException + { + if( !Double.isFinite( value ) ) throw badKey( key, "number", getNumericType( value ) ); + return value; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/TermAPI.java b/src/main/java/dan200/computercraft/core/apis/TermAPI.java new file mode 100644 index 000000000..243cca146 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/TermAPI.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.core.apis; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.computer.IComputerEnvironment; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.util.Colour; + +import javax.annotation.Nonnull; + +/** + * The Terminal API provides functions for writing text to the terminal and monitors, and drawing ASCII graphics. + * + * @cc.module term + */ +public class TermAPI extends TermMethods implements ILuaAPI +{ + private final Terminal terminal; + private final IComputerEnvironment environment; + + public TermAPI( IAPIEnvironment environment ) + { + terminal = environment.getTerminal(); + this.environment = environment.getComputerEnvironment(); + } + + @Override + public String[] getNames() + { + return new String[] { "term" }; + } + + /** + * Get the default palette value for a colour. + * + * @param colour The colour whose palette should be fetched. + * @return The RGB values. + * @throws LuaException When given an invalid colour. + * @cc.treturn number The red channel, will be between 0 and 1. + * @cc.treturn number The green channel, will be between 0 and 1. + * @cc.treturn number The blue channel, will be between 0 and 1. + * @see TermMethods#setPaletteColour(IArguments) To change the palette colour. + */ + @LuaFunction( { "nativePaletteColour", "nativePaletteColor" } ) + public final Object[] nativePaletteColour( int colour ) throws LuaException + { + int actualColour = 15 - parseColour( colour ); + Colour c = Colour.fromInt( actualColour ); + + float[] rgb = c.getRGB(); + + Object[] rgbObj = new Object[rgb.length]; + for( int i = 0; i < rgbObj.length; ++i ) rgbObj[i] = rgb[i]; + return rgbObj; + } + + @Nonnull + @Override + public Terminal getTerminal() + { + return terminal; + } + + @Override + public boolean isColour() + { + return environment.isColour(); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/TermMethods.java b/src/main/java/dan200/computercraft/core/apis/TermMethods.java new file mode 100644 index 000000000..43090405d --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/TermMethods.java @@ -0,0 +1,383 @@ +/* + * 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.apis; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.util.Palette; +import dan200.computercraft.shared.util.StringUtil; +import org.apache.commons.lang3.ArrayUtils; + +import javax.annotation.Nonnull; + +/** + * A base class for all objects which interact with a terminal. Namely the {@link TermAPI} and monitors. + * + * @cc.module term.Redirect + */ +public abstract class TermMethods +{ + private static int getHighestBit( int group ) + { + int bit = 0; + while( group > 0 ) + { + group >>= 1; + bit++; + } + return bit; + } + + @Nonnull + public abstract Terminal getTerminal() throws LuaException; + + public abstract boolean isColour() throws LuaException; + + /** + * Write {@code text} at the current cursor position, moving the cursor to the end of the text. + * + * Unlike functions like {@code write} and {@code print}, this does not wrap the text - it simply copies the + * text to the current terminal line. + * + * @param arguments The text to write. + * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.param text The text to write. + */ + @LuaFunction + public final void write( IArguments arguments ) throws LuaException + { + String text = StringUtil.toString( arguments.get( 0 ) ); + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.write( text ); + terminal.setCursorPos( terminal.getCursorX() + text.length(), terminal.getCursorY() ); + } + } + + /** + * Move all positions up (or down) by {@code y} pixels. + * + * Every pixel in the terminal will be replaced by the line {@code y} pixels below it. If {@code y} is negative, it + * will copy pixels from above instead. + * + * @param y The number of lines to move up by. This may be a negative number. + * @throws LuaException (hidden) If the terminal cannot be found. + */ + @LuaFunction + public final void scroll( int y ) throws LuaException + { + getTerminal().scroll( y ); + } + + /** + * Get the position of the cursor. + * + * @return The cursor's position. + * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.treturn number The x position of the cursor. + * @cc.treturn number The y position of the cursor. + */ + @LuaFunction + public final Object[] getCursorPos() throws LuaException + { + Terminal terminal = getTerminal(); + return new Object[] { terminal.getCursorX() + 1, terminal.getCursorY() + 1 }; + } + + /** + * Set the position of the cursor. {@link #write(IArguments) terminal writes} will begin from this position. + * + * @param x The new x position of the cursor. + * @param y The new y position of the cursor. + * @throws LuaException (hidden) If the terminal cannot be found. + */ + @LuaFunction + public final void setCursorPos( int x, int y ) throws LuaException + { + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.setCursorPos( x - 1, y - 1 ); + } + } + + /** + * Checks if the cursor is currently blinking. + * + * @return If the cursor is blinking. + * @throws LuaException (hidden) If the terminal cannot be found. + */ + @LuaFunction + public final boolean getCursorBlink() throws LuaException + { + return getTerminal().getCursorBlink(); + } + + /** + * Sets whether the cursor should be visible (and blinking) at the current {@link #getCursorPos() cursor position}. + * + * @param blink Whether the cursor should blink. + * @throws LuaException (hidden) If the terminal cannot be found. + */ + @LuaFunction + public final void setCursorBlink( boolean blink ) throws LuaException + { + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.setCursorBlink( blink ); + } + } + + /** + * Get the size of the terminal. + * + * @return The terminal's size. + * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.treturn number The terminal's width. + * @cc.treturn number The terminal's height. + */ + @LuaFunction + public final Object[] getSize() throws LuaException + { + Terminal terminal = getTerminal(); + return new Object[] { terminal.getWidth(), terminal.getHeight() }; + } + + /** + * Clears the terminal, filling it with the {@link #getBackgroundColour() current background colour}. + * + * @throws LuaException (hidden) If the terminal cannot be found. + */ + @LuaFunction + public final void clear() throws LuaException + { + getTerminal().clear(); + } + + /** + * Clears the line the cursor is currently on, filling it with the {@link #getBackgroundColour() current background + * colour}. + * + * @throws LuaException (hidden) If the terminal cannot be found. + */ + @LuaFunction + public final void clearLine() throws LuaException + { + getTerminal().clearLine(); + } + + /** + * Return the colour that new text will be written as. + * + * @return The current text colour. + * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.see colors For a list of colour constants, returned by this function. + */ + @LuaFunction( { "getTextColour", "getTextColor" } ) + public final int getTextColour() throws LuaException + { + return encodeColour( getTerminal().getTextColour() ); + } + + /** + * Set the colour that new text will be written as. + * + * @param colourArg The new text colour. + * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.see colors For a list of colour constants. + */ + @LuaFunction( { "setTextColour", "setTextColor" } ) + public final void setTextColour( int colourArg ) throws LuaException + { + int colour = parseColour( colourArg ); + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.setTextColour( colour ); + } + } + + /** + * Return the current background colour. This is used when {@link #write writing text} and {@link #clear clearing} + * the terminal. + * + * @return The current background colour. + * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.see colors For a list of colour constants, returned by this function. + */ + @LuaFunction( { "getBackgroundColour", "getBackgroundColor" } ) + public final int getBackgroundColour() throws LuaException + { + return encodeColour( getTerminal().getBackgroundColour() ); + } + + /** + * Set the current background colour. This is used when {@link #write writing text} and {@link #clear clearing} the + * terminal. + * + * @param colourArg The new background colour. + * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.see colors For a list of colour constants. + */ + @LuaFunction( { "setBackgroundColour", "setBackgroundColor" } ) + public final void setBackgroundColour( int colourArg ) throws LuaException + { + int colour = parseColour( colourArg ); + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.setBackgroundColour( colour ); + } + } + + /** + * Determine if this terminal supports colour. + * + * Terminals which do not support colour will still allow writing coloured text/backgrounds, but it will be + * displayed in greyscale. + * + * @return Whether this terminal supports colour. + * @throws LuaException (hidden) If the terminal cannot be found. + */ + @LuaFunction( { "isColour", "isColor" } ) + public final boolean getIsColour() throws LuaException + { + return isColour(); + } + + /** + * Writes {@code text} to the terminal with the specific foreground and background characters. + * + * As with {@link #write(IArguments)}, the text will be written at the current cursor location, with the cursor + * moving to the end of the text. + * + * {@code textColour} and {@code backgroundColour} must both be strings the same length as {@code text}. All + * characters represent a single hexadecimal digit, which is converted to one of CC's colours. For instance, + * {@code "a"} corresponds to purple. + * + * @param text The text to write. + * @param textColour The corresponding text colours. + * @param backgroundColour The corresponding background colours. + * @throws LuaException If the three inputs are not the same length. + * @cc.see colors For a list of colour constants, and their hexadecimal values. + * @cc.usage Prints "Hello, world!" in rainbow text. + *
{@code
+     * term.blit("Hello, world!","01234456789ab","0000000000000")
+     * }
+ */ + @LuaFunction + public final void blit( String text, String textColour, String backgroundColour ) throws LuaException + { + if( textColour.length() != text.length() || backgroundColour.length() != text.length() ) + { + throw new LuaException( "Arguments must be the same length" ); + } + + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.blit( text, textColour, backgroundColour ); + terminal.setCursorPos( terminal.getCursorX() + text.length(), terminal.getCursorY() ); + } + } + + /** + * Set the palette for a specific colour. + * + * ComputerCraft's palette system allows you to change how a specific colour should be displayed. For instance, you + * can make @{colors.red} more red by setting its palette to #FF0000. This does now allow you to draw more + * colours - you are still limited to 16 on the screen at one time - but you can change which colours are + * used. + * + * @param args The new palette values. + * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.tparam [1] number index The colour whose palette should be changed. + * @cc.tparam number colour A 24-bit integer representing the RGB value of the colour. For instance the integer + * `0xFF0000` corresponds to the colour #FF0000. + * @cc.tparam [2] number index The colour whose palette should be changed. + * @cc.tparam number r The intensity of the red channel, between 0 and 1. + * @cc.tparam number g The intensity of the green channel, between 0 and 1. + * @cc.tparam number b The intensity of the blue channel, between 0 and 1. + * @cc.usage Change the @{colors.red|red colour} from the default #CC4C4C to #FF0000. + *
{@code
+     * term.setPaletteColour(colors.red, 0xFF0000)
+     * term.setTextColour(colors.red)
+     * print("Hello, world!")
+     * }
+ * @cc.usage As above, but specifying each colour channel separately. + *
{@code
+     * term.setPaletteColour(colors.red, 1, 0, 0)
+     * term.setTextColour(colors.red)
+     * print("Hello, world!")
+     * }
+ * @cc.see colors.unpackRGB To convert from the 24-bit format to three separate channels. + * @cc.see colors.packRGB To convert from three separate channels to the 24-bit format. + */ + @LuaFunction( { "setPaletteColour", "setPaletteColor" } ) + public final void setPaletteColour( IArguments args ) throws LuaException + { + int colour = 15 - parseColour( args.getInt( 0 ) ); + if( args.count() == 2 ) + { + int hex = args.getInt( 1 ); + double[] rgb = Palette.decodeRGB8( hex ); + setColour( getTerminal(), colour, rgb[0], rgb[1], rgb[2] ); + } + else + { + double r = args.getFiniteDouble( 1 ); + double g = args.getFiniteDouble( 2 ); + double b = args.getFiniteDouble( 3 ); + setColour( getTerminal(), colour, r, g, b ); + } + } + + /** + * Get the current palette for a specific colour. + * + * @param colourArg The colour whose palette should be fetched. + * @return The resulting colour. + * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.treturn number The red channel, will be between 0 and 1. + * @cc.treturn number The green channel, will be between 0 and 1. + * @cc.treturn number The blue channel, will be between 0 and 1. + */ + @LuaFunction( { "getPaletteColour", "getPaletteColor" } ) + public final Object[] getPaletteColour( int colourArg ) throws LuaException + { + int colour = 15 - parseColour( colourArg ); + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + return ArrayUtils.toObject( terminal.getPalette().getColour( colour ) ); + } + } + + public static int parseColour( int colour ) throws LuaException + { + if( colour <= 0 ) throw new LuaException( "Colour out of range" ); + colour = getHighestBit( colour ) - 1; + if( colour < 0 || colour > 15 ) throw new LuaException( "Colour out of range" ); + return colour; + } + + + public static int encodeColour( int colour ) + { + return 1 << colour; + } + + public static void setColour( Terminal terminal, int colour, double r, double g, double b ) + { + terminal.getPalette().setColour( colour, r, g, b ); + terminal.setChanged(); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java b/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java new file mode 100644 index 000000000..adcc75ff6 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java @@ -0,0 +1,94 @@ +/* + * 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.apis.handles; + +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NonWritableChannelException; +import java.nio.channels.SeekableByteChannel; +import java.util.Objects; + +/** + * A seekable, readable byte channel which is backed by a simple byte array. + */ +public class ArrayByteChannel implements SeekableByteChannel +{ + private boolean closed = false; + private int position = 0; + + private final byte[] backing; + + public ArrayByteChannel( byte[] backing ) + { + this.backing = backing; + } + + @Override + public int read( ByteBuffer destination ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + Objects.requireNonNull( destination, "destination" ); + + if( position >= backing.length ) return -1; + + int remaining = Math.min( backing.length - position, destination.remaining() ); + destination.put( backing, position, remaining ); + position += remaining; + return remaining; + } + + @Override + public int write( ByteBuffer src ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + throw new NonWritableChannelException(); + } + + @Override + public long position() throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + return position; + } + + @Override + public SeekableByteChannel position( long newPosition ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + if( newPosition < 0 || newPosition > Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "Position out of bounds" ); + } + position = (int) newPosition; + return this; + } + + @Override + public long size() throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + return backing.length; + } + + @Override + public SeekableByteChannel truncate( long size ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + throw new NonWritableChannelException(); + } + + @Override + public boolean isOpen() + { + return !closed; + } + + @Override + public void close() + { + closed = true; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java new file mode 100644 index 000000000..807e1c331 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java @@ -0,0 +1,270 @@ +/* + * 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.apis.handles; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"} + * mode. + * + * @cc.module fs.BinaryReadHandle + */ +public class BinaryReadableHandle extends HandleGeneric +{ + private static final int BUFFER_SIZE = 8192; + + private final ReadableByteChannel reader; + final SeekableByteChannel seekable; + private final ByteBuffer single = ByteBuffer.allocate( 1 ); + + BinaryReadableHandle( ReadableByteChannel reader, SeekableByteChannel seekable, Closeable closeable ) + { + super( closeable ); + this.reader = reader; + this.seekable = seekable; + } + + public static BinaryReadableHandle of( ReadableByteChannel channel, Closeable closeable ) + { + SeekableByteChannel seekable = asSeekable( channel ); + return seekable == null ? new BinaryReadableHandle( channel, null, closeable ) : new Seekable( seekable, closeable ); + } + + public static BinaryReadableHandle of( ReadableByteChannel channel ) + { + return of( channel, channel ); + } + + /** + * Read a number of bytes from this file. + * + * @param countArg The number of bytes to read. When absent, a single byte will be read as a number. This + * may be 0 to determine we are at the end of the file. + * @return The read bytes. + * @throws LuaException When trying to read a negative number of bytes. + * @throws LuaException If the file has been closed. + * @cc.treturn [1] nil If we are at the end of the file. + * @cc.treturn [2] number The value of the byte read. This is returned when the {@code count} is absent. + * @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given. + */ + @LuaFunction + public final Object[] read( Optional countArg ) throws LuaException + { + checkOpen(); + try + { + if( countArg.isPresent() ) + { + int count = countArg.get(); + if( count < 0 ) throw new LuaException( "Cannot read a negative number of bytes" ); + if( count == 0 && seekable != null ) + { + return seekable.position() >= seekable.size() ? null : new Object[] { "" }; + } + + if( count <= BUFFER_SIZE ) + { + ByteBuffer buffer = ByteBuffer.allocate( count ); + + int read = reader.read( buffer ); + if( read < 0 ) return null; + buffer.flip(); + return new Object[] { buffer }; + } + else + { + // Read the initial set of characters, failing if none are read. + ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE ); + int read = reader.read( buffer ); + if( read < 0 ) return null; + + // If we failed to read "enough" here, let's just abort + if( read >= count || read < BUFFER_SIZE ) + { + buffer.flip(); + return new Object[] { buffer }; + } + + // Build up an array of ByteBuffers. Hopefully this means we can perform less allocation + // than doubling up the buffer each time. + int totalRead = read; + List parts = new ArrayList<>( 4 ); + parts.add( buffer ); + while( read >= BUFFER_SIZE && totalRead < count ) + { + buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) ); + read = reader.read( buffer ); + if( read < 0 ) break; + + totalRead += read; + parts.add( buffer ); + } + + // Now just copy all the bytes across! + byte[] bytes = new byte[totalRead]; + int pos = 0; + for( ByteBuffer part : parts ) + { + System.arraycopy( part.array(), 0, bytes, pos, part.position() ); + pos += part.position(); + } + return new Object[] { bytes }; + } + } + else + { + single.clear(); + int b = reader.read( single ); + return b == -1 ? null : new Object[] { single.get( 0 ) & 0xFF }; + } + } + catch( IOException e ) + { + return null; + } + } + + /** + * Read the remainder of the file. + * + * @return The file, or {@code null} if at the end of it. + * @throws LuaException If the file has been closed. + * @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end. + */ + @LuaFunction + public final Object[] readAll() throws LuaException + { + checkOpen(); + try + { + int expected = 32; + if( seekable != null ) expected = Math.max( expected, (int) (seekable.size() - seekable.position()) ); + ByteArrayOutputStream stream = new ByteArrayOutputStream( expected ); + + ByteBuffer buf = ByteBuffer.allocate( 8192 ); + boolean readAnything = false; + while( true ) + { + buf.clear(); + int r = reader.read( buf ); + if( r == -1 ) break; + + readAnything = true; + stream.write( buf.array(), 0, r ); + } + return readAnything ? new Object[] { stream.toByteArray() } : null; + } + catch( IOException e ) + { + return null; + } + } + + /** + * Read a line from the file. + * + * @param withTrailingArg Whether to include the newline characters with the returned string. Defaults to {@code false}. + * @return The read string. + * @throws LuaException If the file has been closed. + * @cc.treturn string|nil The read line or {@code nil} if at the end of the file. + */ + @LuaFunction + public final Object[] readLine( Optional withTrailingArg ) throws LuaException + { + checkOpen(); + boolean withTrailing = withTrailingArg.orElse( false ); + try + { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + boolean readAnything = false, readRc = false; + while( true ) + { + single.clear(); + int read = reader.read( single ); + if( read <= 0 ) + { + // Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it + // back. + if( readRc ) stream.write( '\r' ); + return readAnything ? new Object[] { stream.toByteArray() } : null; + } + + readAnything = true; + + byte chr = single.get( 0 ); + if( chr == '\n' ) + { + if( withTrailing ) + { + if( readRc ) stream.write( '\r' ); + stream.write( chr ); + } + return new Object[] { stream.toByteArray() }; + } + else + { + // We want to skip \r\n, but obviously need to include cases where \r is not followed by \n. + // Note, this behaviour is non-standard compliant (strictly speaking we should have no + // special logic for \r), but we preserve compatibility with EncodedReadableHandle and + // previous behaviour of the io library. + if( readRc ) stream.write( '\r' ); + readRc = chr == '\r'; + if( !readRc ) stream.write( chr ); + } + } + } + catch( IOException e ) + { + return null; + } + } + + public static class Seekable extends BinaryReadableHandle + { + Seekable( SeekableByteChannel seekable, Closeable closeable ) + { + super( seekable, seekable, closeable ); + } + + /** + * Seek to a new position within the file, changing where bytes are written to. The new position is an offset + * given by {@code offset}, relative to a start position determined by {@code whence}: + * + * - {@code "set"}: {@code offset} is relative to the beginning of the file. + * - {@code "cur"}: Relative to the current position. This is the default. + * - {@code "end"}: Relative to the end of the file. + * + * In case of success, {@code seek} returns the new file position from the beginning of the file. + * + * @param whence Where the offset is relative to. + * @param offset The offset to seek to. + * @return The new position. + * @throws LuaException If the file has been closed. + * @cc.treturn [1] number The new position. + * @cc.treturn [2] nil If seeking failed. + * @cc.treturn string The reason seeking failed. + */ + @LuaFunction + public final Object[] seek( Optional whence, Optional offset ) throws LuaException + { + checkOpen(); + return handleSeek( seekable, whence, offset ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java new file mode 100644 index 000000000..e1a71c964 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java @@ -0,0 +1,141 @@ +/* + * 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.apis.handles; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.LuaValues; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.Optional; + +/** + * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"} + * modes. + * + * @cc.module fs.BinaryWriteHandle + */ +public class BinaryWritableHandle extends HandleGeneric +{ + private final WritableByteChannel writer; + final SeekableByteChannel seekable; + private final ByteBuffer single = ByteBuffer.allocate( 1 ); + + protected BinaryWritableHandle( WritableByteChannel writer, SeekableByteChannel seekable, Closeable closeable ) + { + super( closeable ); + this.writer = writer; + this.seekable = seekable; + } + + public static BinaryWritableHandle of( WritableByteChannel channel, Closeable closeable ) + { + SeekableByteChannel seekable = asSeekable( channel ); + return seekable == null ? new BinaryWritableHandle( channel, null, closeable ) : new Seekable( seekable, closeable ); + } + + public static BinaryWritableHandle of( WritableByteChannel channel ) + { + return of( channel, channel ); + } + + /** + * Write a string or byte to the file. + * + * @param arguments The value to write. + * @throws LuaException If the file has been closed. + * @cc.tparam [1] number The byte to write. + * @cc.tparam [2] string The string to write. + */ + @LuaFunction + public final void write( IArguments arguments ) throws LuaException + { + checkOpen(); + try + { + Object arg = arguments.get( 0 ); + if( arg instanceof Number ) + { + int number = ((Number) arg).intValue(); + single.clear(); + single.put( (byte) number ); + single.flip(); + + writer.write( single ); + } + else if( arg instanceof String ) + { + writer.write( arguments.getBytes( 0 ) ); + } + else + { + throw LuaValues.badArgumentOf( 0, "string or number", arg ); + } + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Save the current file without closing it. + * + * @throws LuaException If the file has been closed. + */ + @LuaFunction + public final void flush() throws LuaException + { + checkOpen(); + try + { + // Technically this is not needed + if( writer instanceof FileChannel ) ((FileChannel) writer).force( false ); + } + catch( IOException ignored ) + { + } + } + + public static class Seekable extends BinaryWritableHandle + { + public Seekable( SeekableByteChannel seekable, Closeable closeable ) + { + super( seekable, seekable, closeable ); + } + + /** + * Seek to a new position within the file, changing where bytes are written to. The new position is an offset + * given by {@code offset}, relative to a start position determined by {@code whence}: + * + * - {@code "set"}: {@code offset} is relative to the beginning of the file. + * - {@code "cur"}: Relative to the current position. This is the default. + * - {@code "end"}: Relative to the end of the file. + * + * In case of success, {@code seek} returns the new file position from the beginning of the file. + * + * @param whence Where the offset is relative to. + * @param offset The offset to seek to. + * @return The new position. + * @throws LuaException If the file has been closed. + * @cc.treturn [1] number The new position. + * @cc.treturn [2] nil If seeking failed. + * @cc.treturn string The reason seeking failed. + */ + @LuaFunction + public final Object[] seek( Optional whence, Optional offset ) throws LuaException + { + checkOpen(); + return handleSeek( seekable, whence, offset ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java new file mode 100644 index 000000000..3f2080f3f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java @@ -0,0 +1,188 @@ +/* + * 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.apis.handles; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; + +import javax.annotation.Nonnull; +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"} + * mode. + * + * @cc.module fs.ReadHandle + */ +public class EncodedReadableHandle extends HandleGeneric +{ + private static final int BUFFER_SIZE = 8192; + + private final BufferedReader reader; + + public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull Closeable closable ) + { + super( closable ); + this.reader = reader; + } + + public EncodedReadableHandle( @Nonnull BufferedReader reader ) + { + this( reader, reader ); + } + + /** + * Read a line from the file. + * + * @param withTrailingArg Whether to include the newline characters with the returned string. Defaults to {@code false}. + * @return The read string. + * @throws LuaException If the file has been closed. + * @cc.treturn string|nil The read line or {@code nil} if at the end of the file. + */ + @LuaFunction + public final Object[] readLine( Optional withTrailingArg ) throws LuaException + { + checkOpen(); + boolean withTrailing = withTrailingArg.orElse( false ); + try + { + String line = reader.readLine(); + if( line != null ) + { + // While this is technically inaccurate, it's better than nothing + if( withTrailing ) line += "\n"; + return new Object[] { line }; + } + else + { + return null; + } + } + catch( IOException e ) + { + return null; + } + } + + /** + * Read the remainder of the file. + * + * @return The file, or {@code null} if at the end of it. + * @throws LuaException If the file has been closed. + * @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end. + */ + @LuaFunction + public final Object[] readAll() throws LuaException + { + checkOpen(); + try + { + StringBuilder result = new StringBuilder(); + String line = reader.readLine(); + while( line != null ) + { + result.append( line ); + line = reader.readLine(); + if( line != null ) + { + result.append( "\n" ); + } + } + return new Object[] { result.toString() }; + } + catch( IOException e ) + { + return null; + } + } + + /** + * Read a number of characters from this file. + * + * @param countA The number of characters to read, defaulting to 1. + * @return The read characters. + * @throws LuaException When trying to read a negative number of characters. + * @throws LuaException If the file has been closed. + * @cc.treturn string|nil The read characters, or {@code nil} if at the of the file. + */ + @LuaFunction + public final Object[] read( Optional countA ) throws LuaException + { + checkOpen(); + try + { + int count = countA.orElse( 1 ); + if( count < 0 ) + { + // Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so + // it seems best to remain somewhat consistent. + throw new LuaException( "Cannot read a negative number of characters" ); + } + else if( count <= BUFFER_SIZE ) + { + // If we've got a small count, then allocate that and read it. + char[] chars = new char[count]; + int read = reader.read( chars ); + + return read < 0 ? null : new Object[] { new String( chars, 0, read ) }; + } + else + { + // If we've got a large count, read in bunches of 8192. + char[] buffer = new char[BUFFER_SIZE]; + + // Read the initial set of characters, failing if none are read. + int read = reader.read( buffer, 0, Math.min( buffer.length, count ) ); + if( read < 0 ) return null; + + StringBuilder out = new StringBuilder( read ); + int totalRead = read; + out.append( buffer, 0, read ); + + // Otherwise read until we either reach the limit or we no longer consume + // the full buffer. + while( read >= BUFFER_SIZE && totalRead < count ) + { + read = reader.read( buffer, 0, Math.min( BUFFER_SIZE, count - totalRead ) ); + if( read < 0 ) break; + + totalRead += read; + out.append( buffer, 0, read ); + } + + return new Object[] { out.toString() }; + } + } + catch( IOException e ) + { + return null; + } + } + + public static BufferedReader openUtf8( ReadableByteChannel channel ) + { + return open( channel, StandardCharsets.UTF_8 ); + } + + public static BufferedReader open( ReadableByteChannel channel, Charset charset ) + { + // Create a charset decoder with the same properties as StreamDecoder does for + // InputStreams: namely, replace everything instead of erroring. + CharsetDecoder decoder = charset.newDecoder() + .onMalformedInput( CodingErrorAction.REPLACE ) + .onUnmappableCharacter( CodingErrorAction.REPLACE ); + return new BufferedReader( Channels.newReader( channel, decoder, -1 ) ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java new file mode 100644 index 000000000..4d548ce03 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java @@ -0,0 +1,116 @@ +/* + * 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.apis.handles; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.shared.util.StringUtil; + +import javax.annotation.Nonnull; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; + +/** + * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes. + * + * @cc.module fs.WriteHandle + */ +public class EncodedWritableHandle extends HandleGeneric +{ + private final BufferedWriter writer; + + public EncodedWritableHandle( @Nonnull BufferedWriter writer, @Nonnull Closeable closable ) + { + super( closable ); + this.writer = writer; + } + + /** + * Write a string of characters to the file. + * + * @param args The value to write. + * @throws LuaException If the file has been closed. + * @cc.param The value to write to the file. + */ + @LuaFunction + public final void write( IArguments args ) throws LuaException + { + checkOpen(); + String text = StringUtil.toString( args.get( 0 ) ); + try + { + writer.write( text, 0, text.length() ); + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Write a string of characters to the file, follwing them with a new line character. + * + * @param args The value to write. + * @throws LuaException If the file has been closed. + * @cc.param The value to write to the file. + */ + @LuaFunction + public final void writeLine( IArguments args ) throws LuaException + { + checkOpen(); + String text = StringUtil.toString( args.get( 0 ) ); + try + { + writer.write( text, 0, text.length() ); + writer.newLine(); + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + /** + * Save the current file without closing it. + * + * @throws LuaException If the file has been closed. + */ + @LuaFunction + public final void flush() throws LuaException + { + checkOpen(); + try + { + writer.flush(); + } + catch( IOException ignored ) + { + } + } + + public static BufferedWriter openUtf8( WritableByteChannel channel ) + { + return open( channel, StandardCharsets.UTF_8 ); + } + + public static BufferedWriter open( WritableByteChannel channel, Charset charset ) + { + // Create a charset encoder with the same properties as StreamEncoder does for + // OutputStreams: namely, replace everything instead of erroring. + CharsetEncoder encoder = charset.newEncoder() + .onMalformedInput( CodingErrorAction.REPLACE ) + .onUnmappableCharacter( CodingErrorAction.REPLACE ); + return new BufferedWriter( Channels.newWriter( channel, encoder, -1 ) ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java new file mode 100644 index 000000000..418ab07dd --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java @@ -0,0 +1,114 @@ +/* + * 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.apis.handles; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; +import java.io.Closeable; +import java.io.IOException; +import java.nio.channels.Channel; +import java.nio.channels.SeekableByteChannel; +import java.util.Optional; + +public abstract class HandleGeneric +{ + private Closeable closable; + private boolean open = true; + + protected HandleGeneric( @Nonnull Closeable closable ) + { + this.closable = closable; + } + + protected void checkOpen() throws LuaException + { + if( !open ) throw new LuaException( "attempt to use a closed file" ); + } + + protected final void close() + { + open = false; + + IoUtil.closeQuietly( closable ); + closable = null; + } + + /** + * Close this file, freeing any resources it uses. + * + * Once a file is closed it may no longer be read or written to. + * + * @throws LuaException If the file has already been closed. + */ + @LuaFunction( "close" ) + public final void doClose() throws LuaException + { + checkOpen(); + close(); + } + + + /** + * Shared implementation for various file handle types. + * + * @param channel The channel to seek in + * @param whence The seeking mode. + * @param offset The offset to seek to. + * @return The new position of the file, or null if some error occurred. + * @throws LuaException If the arguments were invalid + * @see {@code file:seek} in the Lua manual. + */ + protected static Object[] handleSeek( SeekableByteChannel channel, Optional whence, Optional offset ) throws LuaException + { + long actualOffset = offset.orElse( 0L ); + try + { + switch( whence.orElse( "cur" ) ) + { + case "set": + channel.position( actualOffset ); + break; + case "cur": + channel.position( channel.position() + actualOffset ); + break; + case "end": + channel.position( channel.size() + actualOffset ); + break; + default: + throw new LuaException( "bad argument #1 to 'seek' (invalid option '" + whence + "'" ); + } + + return new Object[] { channel.position() }; + } + catch( IllegalArgumentException e ) + { + return new Object[] { null, "Position is negative" }; + } + catch( IOException e ) + { + return null; + } + } + + protected static SeekableByteChannel asSeekable( Channel channel ) + { + if( !(channel instanceof SeekableByteChannel) ) return null; + + SeekableByteChannel seekable = (SeekableByteChannel) channel; + try + { + seekable.position( seekable.position() ); + return seekable; + } + catch( IOException | UnsupportedOperationException e ) + { + return null; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/CheckUrl.java b/src/main/java/dan200/computercraft/core/apis/http/CheckUrl.java new file mode 100644 index 000000000..a2b9f896d --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/CheckUrl.java @@ -0,0 +1,67 @@ +/* + * 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.apis.http; + +import dan200.computercraft.core.apis.IAPIEnvironment; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.concurrent.Future; + +/** + * Checks a URL using {@link NetworkUtils#getAddress(String, int, boolean)}} + * + * This requires a DNS lookup, and so needs to occur off-thread. + */ +public class CheckUrl extends Resource +{ + private static final String EVENT = "http_check"; + + private Future future; + + private final IAPIEnvironment environment; + private final String address; + private final String host; + + public CheckUrl( ResourceGroup limiter, IAPIEnvironment environment, String address, URI uri ) + { + super( limiter ); + this.environment = environment; + this.address = address; + host = uri.getHost(); + } + + public void run() + { + if( isClosed() ) return; + future = NetworkUtils.EXECUTOR.submit( this::doRun ); + checkClosed(); + } + + private void doRun() + { + if( isClosed() ) return; + + try + { + InetSocketAddress netAddress = NetworkUtils.getAddress( host, 80, false ); + NetworkUtils.getOptions( host, netAddress ); + + if( tryClose() ) environment.queueEvent( EVENT, address, true ); + } + catch( HTTPRequestException e ) + { + if( tryClose() ) environment.queueEvent( EVENT, address, false, e.getMessage() ); + } + } + + @Override + protected void dispose() + { + super.dispose(); + future = closeFuture( future ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/HTTPRequestException.java b/src/main/java/dan200/computercraft/core/apis/http/HTTPRequestException.java new file mode 100644 index 000000000..72655a05c --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/HTTPRequestException.java @@ -0,0 +1,22 @@ +/* + * 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.apis.http; + +public class HTTPRequestException extends Exception +{ + private static final long serialVersionUID = 7591208619422744652L; + + public HTTPRequestException( String s ) + { + super( s ); + } + + @Override + public Throwable fillInStackTrace() + { + return this; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java b/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java new file mode 100644 index 000000000..aa2a9a6be --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java @@ -0,0 +1,148 @@ +/* + * 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.apis.http; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.core.apis.http.options.Action; +import dan200.computercraft.core.apis.http.options.AddressRule; +import dan200.computercraft.core.apis.http.options.Options; +import dan200.computercraft.shared.util.ThreadUtils; +import io.netty.buffer.ByteBuf; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; + +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManagerFactory; +import java.net.InetSocketAddress; +import java.security.KeyStore; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Just a shared object for executing simple HTTP related tasks. + */ +public final class NetworkUtils +{ + public static final ExecutorService EXECUTOR = new ThreadPoolExecutor( + 4, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue<>(), + ThreadUtils.builder( "Network" ) + .setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 ) + .build() + ); + + public static final EventLoopGroup LOOP_GROUP = new NioEventLoopGroup( 4, ThreadUtils.builder( "Netty" ) + .setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 ) + .build() + ); + + private NetworkUtils() + { + } + + private static final Object sslLock = new Object(); + private static TrustManagerFactory trustManager; + private static SslContext sslContext; + private static boolean triedSslContext = false; + + private static TrustManagerFactory getTrustManager() + { + if( trustManager != null ) return trustManager; + synchronized( sslLock ) + { + if( trustManager != null ) return trustManager; + + TrustManagerFactory tmf = null; + try + { + tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() ); + tmf.init( (KeyStore) null ); + } + catch( Exception e ) + { + ComputerCraft.log.error( "Cannot setup trust manager", e ); + } + + return trustManager = tmf; + } + } + + public static SslContext getSslContext() throws HTTPRequestException + { + if( sslContext != null || triedSslContext ) return sslContext; + synchronized( sslLock ) + { + if( sslContext != null || triedSslContext ) return sslContext; + try + { + return sslContext = SslContextBuilder + .forClient() + .trustManager( getTrustManager() ) + .build(); + } + catch( SSLException e ) + { + ComputerCraft.log.error( "Cannot construct SSL context", e ); + triedSslContext = true; + sslContext = null; + + throw new HTTPRequestException( "Cannot create a secure connection" ); + } + } + } + + /** + * Create a {@link InetSocketAddress} from the resolved {@code host} and port. + * + * Note, this may require a DNS lookup, and so should not be executed on the main CC thread. + * + * @param host The host to resolve. + * @param port The port, or -1 if not defined. + * @param ssl Whether to connect with SSL. This is used to find the default port if not otherwise specified. + * @return The resolved address. + * @throws HTTPRequestException If the host is not malformed. + */ + public static InetSocketAddress getAddress( String host, int port, boolean ssl ) throws HTTPRequestException + { + if( port < 0 ) port = ssl ? 443 : 80; + InetSocketAddress socketAddress = new InetSocketAddress( host, port ); + if( socketAddress.isUnresolved() ) throw new HTTPRequestException( "Unknown host" ); + return socketAddress; + } + + /** + * Get options for a specific domain. + * + * @param host The host to resolve. + * @param address The address, resolved by {@link #getAddress(String, int, boolean)}. + * @return The options for this host. + * @throws HTTPRequestException If the host is not permitted + */ + public static Options getOptions( String host, InetSocketAddress address ) throws HTTPRequestException + { + Options options = AddressRule.apply( ComputerCraft.httpRules, host, address.getAddress() ); + if( options.action == Action.DENY ) throw new HTTPRequestException( "Domain not permitted" ); + return options; + } + + /** + * Read a {@link ByteBuf} into a byte array. + * + * @param buffer The buffer to read. + * @return The resulting bytes. + */ + public static byte[] toBytes( ByteBuf buffer ) + { + byte[] bytes = new byte[buffer.readableBytes()]; + buffer.readBytes( bytes ); + return bytes; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/Resource.java b/src/main/java/dan200/computercraft/core/apis/http/Resource.java new file mode 100644 index 000000000..469a57511 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/Resource.java @@ -0,0 +1,150 @@ +/* + * 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.apis.http; + +import dan200.computercraft.shared.util.IoUtil; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; + +import java.io.Closeable; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +/** + * A holder for one or more resources, with a lifetime. + * + * @param The type of this resource. Should be the class extending from {@link Resource}. + */ +public abstract class Resource> implements Closeable +{ + private final AtomicBoolean closed = new AtomicBoolean( false ); + private final ResourceGroup limiter; + + protected Resource( ResourceGroup limiter ) + { + this.limiter = limiter; + } + + /** + * Whether this resource is closed. + * + * @return Whether this resource is closed. + */ + public final boolean isClosed() + { + return closed.get(); + } + + /** + * Checks if this has been cancelled. If so, it'll clean up any existing resources and cancel any pending futures. + * + * @return Whether this resource has been closed. + */ + public final boolean checkClosed() + { + if( !closed.get() ) return false; + dispose(); + return true; + } + + /** + * Try to close the current resource. + * + * @return Whether this was successfully closed, or {@code false} if it has already been closed. + */ + protected final boolean tryClose() + { + if( closed.getAndSet( true ) ) return false; + dispose(); + return true; + } + + /** + * Clean up any pending resources + * + * Note, this may be called multiple times, and so should be thread-safe and + * avoid any major side effects. + */ + protected void dispose() + { + @SuppressWarnings( "unchecked" ) + T thisT = (T) this; + limiter.release( thisT ); + } + + /** + * Create a {@link WeakReference} which will close {@code this} when collected. + * + * @param The object we are wrapping in a reference. + * @param object The object to reference to + * @return The weak reference. + */ + protected WeakReference createOwnerReference( R object ) + { + return new CloseReference<>( this, object ); + } + + @Override + public final void close() + { + tryClose(); + } + + public boolean queue( Consumer task ) + { + @SuppressWarnings( "unchecked" ) + T thisT = (T) this; + return limiter.queue( thisT, () -> task.accept( thisT ) ); + } + + protected static T closeCloseable( T closeable ) + { + IoUtil.closeQuietly( closeable ); + return null; + } + + protected static ChannelFuture closeChannel( ChannelFuture future ) + { + if( future != null ) + { + future.cancel( false ); + Channel channel = future.channel(); + if( channel != null && channel.isOpen() ) channel.close(); + } + + return null; + } + + protected static > T closeFuture( T future ) + { + if( future != null ) future.cancel( true ); + return null; + } + + + private static final ReferenceQueue QUEUE = new ReferenceQueue<>(); + + private static class CloseReference extends WeakReference + { + final Resource resource; + + CloseReference( Resource resource, T referent ) + { + super( referent, QUEUE ); + this.resource = resource; + } + } + + public static void cleanup() + { + Reference reference; + while( (reference = QUEUE.poll()) != null ) ((CloseReference) reference).resource.close(); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/ResourceGroup.java b/src/main/java/dan200/computercraft/core/apis/http/ResourceGroup.java new file mode 100644 index 000000000..cfce1f3ce --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/ResourceGroup.java @@ -0,0 +1,82 @@ +/* + * 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.apis.http; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +/** + * A collection of {@link Resource}s, with an upper bound on capacity. + * + * @param The type of the resource this group manages. + */ +public class ResourceGroup> +{ + private static final IntSupplier ZERO = () -> 0; + + final IntSupplier limit; + + boolean active = false; + + final Set resources = Collections.newSetFromMap( new ConcurrentHashMap<>() ); + + public ResourceGroup( IntSupplier limit ) + { + this.limit = limit; + } + + public ResourceGroup() + { + limit = ZERO; + } + + public void startup() + { + active = true; + } + + public synchronized void shutdown() + { + active = false; + + for( T resource : resources ) resource.close(); + resources.clear(); + + Resource.cleanup(); + } + + + public final boolean queue( T resource, Runnable setup ) + { + return queue( () -> { + setup.run(); + return resource; + } ); + } + + public synchronized boolean queue( Supplier resource ) + { + Resource.cleanup(); + if( !active ) return false; + + int limit = this.limit.getAsInt(); + if( limit <= 0 || resources.size() < limit ) + { + resources.add( resource.get() ); + return true; + } + + return false; + } + + public synchronized void release( T resource ) + { + resources.remove( resource ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/ResourceQueue.java b/src/main/java/dan200/computercraft/core/apis/http/ResourceQueue.java new file mode 100644 index 000000000..4159097b0 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/ResourceQueue.java @@ -0,0 +1,60 @@ +/* + * 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.apis.http; + +import java.util.ArrayDeque; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +/** + * A {@link ResourceGroup} which will queue items when the group at capacity. + * + * @param The type of the resource this queue manages. + */ +public class ResourceQueue> extends ResourceGroup +{ + private final ArrayDeque> pending = new ArrayDeque<>(); + + public ResourceQueue( IntSupplier limit ) + { + super( limit ); + } + + public ResourceQueue() + { + } + + @Override + public synchronized void shutdown() + { + super.shutdown(); + pending.clear(); + } + + @Override + public synchronized boolean queue( Supplier resource ) + { + if( !active ) return false; + + if( !super.queue( resource ) ) pending.add( resource ); + return true; + } + + @Override + public synchronized void release( T resource ) + { + super.release( resource ); + + if( !active ) return; + + int limit = this.limit.getAsInt(); + if( limit <= 0 || resources.size() < limit ) + { + Supplier next = pending.poll(); + if( next != null ) resources.add( next.get() ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/options/Action.java b/src/main/java/dan200/computercraft/core/apis/http/options/Action.java new file mode 100644 index 000000000..ac23cc575 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/options/Action.java @@ -0,0 +1,23 @@ +/* + * 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.apis.http.options; + +import javax.annotation.Nonnull; + +public enum Action +{ + ALLOW, + DENY; + + private final PartialOptions partial = new PartialOptions( this, null, null, null, null ); + + @Nonnull + public PartialOptions toPartial() + { + return partial; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java b/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java new file mode 100644 index 000000000..03bf372ba --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java @@ -0,0 +1,186 @@ +/* + * 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.apis.http.options; + +import com.google.common.net.InetAddresses; +import dan200.computercraft.ComputerCraft; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.regex.Pattern; + +/** + * A pattern which matches an address, and controls whether it is accessible or not. + */ +public final class AddressRule +{ + public static final long MAX_DOWNLOAD = 16 * 1024 * 1024; + public static final long MAX_UPLOAD = 4 * 1024 * 1024; + public static final int TIMEOUT = 30_000; + public static final int WEBSOCKET_MESSAGE = 128 * 1024; + + private static final class HostRange + { + private final byte[] min; + private final byte[] max; + + private HostRange( byte[] min, byte[] max ) + { + this.min = min; + this.max = max; + } + + public boolean contains( InetAddress address ) + { + byte[] entry = address.getAddress(); + if( entry.length != min.length ) return false; + + for( int i = 0; i < entry.length; i++ ) + { + int value = 0xFF & entry[i]; + if( value < (0xFF & min[i]) || value > (0xFF & max[i]) ) return false; + } + + return true; + } + } + + private final HostRange ip; + private final Pattern domainPattern; + private final PartialOptions partial; + + private AddressRule( @Nullable HostRange ip, @Nullable Pattern domainPattern, @Nonnull PartialOptions partial ) + { + this.ip = ip; + this.domainPattern = domainPattern; + this.partial = partial; + } + + @Nullable + public static AddressRule parse( String filter, @Nonnull PartialOptions partial ) + { + int cidr = filter.indexOf( '/' ); + if( cidr >= 0 ) + { + String addressStr = filter.substring( 0, cidr ); + String prefixSizeStr = filter.substring( cidr + 1 ); + + int prefixSize; + try + { + prefixSize = Integer.parseInt( prefixSizeStr ); + } + catch( NumberFormatException e ) + { + ComputerCraft.log.error( + "Malformed http whitelist/blacklist entry '{}': Cannot extract size of CIDR mask from '{}'.", + filter, prefixSizeStr + ); + return null; + } + + InetAddress address; + try + { + address = InetAddresses.forString( addressStr ); + } + catch( IllegalArgumentException e ) + { + ComputerCraft.log.error( + "Malformed http whitelist/blacklist entry '{}': Cannot extract IP address from '{}'.", + filter, prefixSizeStr + ); + return null; + } + + // Mask the bytes of the IP address. + byte[] minBytes = address.getAddress(), maxBytes = address.getAddress(); + int size = prefixSize; + for( int i = 0; i < minBytes.length; i++ ) + { + if( size <= 0 ) + { + minBytes[i] &= 0; + maxBytes[i] |= 0xFF; + } + else if( size < 8 ) + { + minBytes[i] &= 0xFF << (8 - size); + maxBytes[i] |= ~(0xFF << (8 - size)); + } + + size -= 8; + } + + return new AddressRule( new HostRange( minBytes, maxBytes ), null, partial ); + } + else + { + Pattern pattern = Pattern.compile( "^\\Q" + filter.replaceAll( "\\*", "\\\\E.*\\\\Q" ) + "\\E$" ); + return new AddressRule( null, pattern, partial ); + } + } + + /** + * Determine whether the given address matches a series of patterns. + * + * @param domain The domain to match + * @param address The address to check. + * @return Whether it matches any of these patterns. + */ + private boolean matches( String domain, InetAddress address ) + { + if( domainPattern != null ) + { + if( domainPattern.matcher( domain ).matches() ) return true; + if( domainPattern.matcher( address.getHostName() ).matches() ) return true; + } + + // Match the normal address + if( matchesAddress( address ) ) return true; + + // If we're an IPv4 address in disguise then let's check that. + return address instanceof Inet6Address && InetAddresses.is6to4Address( (Inet6Address) address ) + && matchesAddress( InetAddresses.get6to4IPv4Address( (Inet6Address) address ) ); + } + + private boolean matchesAddress( InetAddress address ) + { + if( domainPattern != null && domainPattern.matcher( address.getHostAddress() ).matches() ) return true; + return ip != null && ip.contains( address ); + } + + public static Options apply( Iterable rules, String domain, InetAddress address ) + { + PartialOptions options = null; + boolean hasMany = false; + + for( AddressRule rule : rules ) + { + if( !rule.matches( domain, address ) ) continue; + + if( options == null ) + { + options = rule.partial; + } + else + { + + if( !hasMany ) + { + options = options.copy(); + hasMany = true; + } + + options.merge( rule.partial ); + } + } + + return (options == null ? PartialOptions.DEFAULT : options).toOptions(); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/options/AddressRuleConfig.java b/src/main/java/dan200/computercraft/core/apis/http/options/AddressRuleConfig.java new file mode 100644 index 000000000..4f0bc5647 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/options/AddressRuleConfig.java @@ -0,0 +1,132 @@ +/* + * 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.apis.http.options; + +import com.electronwill.nightconfig.core.CommentedConfig; +import com.electronwill.nightconfig.core.Config; +import com.electronwill.nightconfig.core.InMemoryCommentedFormat; +import com.electronwill.nightconfig.core.UnmodifiableConfig; +import dan200.computercraft.ComputerCraft; + +import javax.annotation.Nullable; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Parses, checks and generates {@link Config}s for {@link AddressRule}. + */ +public class AddressRuleConfig +{ + public static UnmodifiableConfig makeRule( String host, Action action ) + { + CommentedConfig config = InMemoryCommentedFormat.defaultInstance().createConfig( ConcurrentHashMap::new ); + config.add( "host", host ); + config.add( "action", action.name().toLowerCase( Locale.ROOT ) ); + + if( host.equals( "*" ) && action == Action.ALLOW ) + { + config.setComment( "timeout", "The period of time (in milliseconds) to wait before a HTTP request times out. Set to 0 for unlimited." ); + config.add( "timeout", AddressRule.TIMEOUT ); + + config.setComment( "max_download", "The maximum size (in bytes) that a computer can download in a single request. Note that responses may receive more data than allowed, but this data will not be returned to the client." ); + config.set( "max_download", AddressRule.MAX_DOWNLOAD ); + + config.setComment( "max_upload", "The maximum size (in bytes) that a computer can upload in a single request. This includes headers and POST text." ); + config.set( "max_upload", AddressRule.MAX_UPLOAD ); + + config.setComment( "max_websocket_message", "The maximum size (in bytes) that a computer can send or receive in one websocket packet." ); + config.set( "max_websocket_message", AddressRule.WEBSOCKET_MESSAGE ); + } + + return config; + } + + public static boolean checkRule( UnmodifiableConfig builder ) + { + String hostObj = get( builder, "host", String.class ).orElse( null ); + return hostObj != null && checkEnum( builder, "action", Action.class ) + && check( builder, "timeout", Number.class ) + && check( builder, "max_upload", Number.class ) + && check( builder, "max_download", Number.class ) + && check( builder, "websocket_message", Number.class ) + && AddressRule.parse( hostObj, PartialOptions.DEFAULT ) != null; + } + + @Nullable + public static AddressRule parseRule( UnmodifiableConfig builder ) + { + String hostObj = get( builder, "host", String.class ).orElse( null ); + if( hostObj == null ) return null; + + Action action = getEnum( builder, "action", Action.class ).orElse( null ); + Integer timeout = get( builder, "timeout", Number.class ).map( Number::intValue ).orElse( null ); + Long maxUpload = get( builder, "max_upload", Number.class ).map( Number::longValue ).orElse( null ); + Long maxDownload = get( builder, "max_download", Number.class ).map( Number::longValue ).orElse( null ); + Integer websocketMessage = get( builder, "websocket_message", Number.class ).map( Number::intValue ).orElse( null ); + + PartialOptions options = new PartialOptions( + action, + maxUpload, + maxDownload, + timeout, + websocketMessage + ); + + return AddressRule.parse( hostObj, options ); + } + + private static boolean check( UnmodifiableConfig config, String field, Class klass ) + { + Object value = config.get( field ); + if( value == null || klass.isInstance( value ) ) return true; + + ComputerCraft.log.warn( "HTTP rule's {} is not a {}.", field, klass.getSimpleName() ); + return false; + } + + private static > boolean checkEnum( UnmodifiableConfig config, String field, Class klass ) + { + Object value = config.get( field ); + if( value == null ) return true; + + if( !(value instanceof String) ) + { + ComputerCraft.log.warn( "HTTP rule's {} is not a string", field ); + return false; + } + + if( parseEnum( klass, (String) value ) == null ) + { + ComputerCraft.log.warn( "HTTP rule's {} is not a known option", field ); + return false; + } + + return true; + } + + private static Optional get( UnmodifiableConfig config, String field, Class klass ) + { + Object value = config.get( field ); + return klass.isInstance( value ) ? Optional.of( klass.cast( value ) ) : Optional.empty(); + } + + private static > Optional getEnum( UnmodifiableConfig config, String field, Class klass ) + { + return get( config, field, String.class ).map( x -> parseEnum( klass, x ) ); + } + + @Nullable + private static > T parseEnum( Class klass, String x ) + { + for( T value : klass.getEnumConstants() ) + { + if( value.name().equalsIgnoreCase( x ) ) return value; + } + return null; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/options/Options.java b/src/main/java/dan200/computercraft/core/apis/http/options/Options.java new file mode 100644 index 000000000..5dd0e78d1 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/options/Options.java @@ -0,0 +1,31 @@ +/* + * 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.apis.http.options; + +import javax.annotation.Nonnull; + +/** + * Options about a specific domain. + */ +public final class Options +{ + @Nonnull + public final Action action; + public final long maxUpload; + public final long maxDownload; + public final int timeout; + public final int websocketMessage; + + Options( @Nonnull Action action, long maxUpload, long maxDownload, int timeout, int websocketMessage ) + { + this.action = action; + this.maxUpload = maxUpload; + this.maxDownload = maxDownload; + this.timeout = timeout; + this.websocketMessage = websocketMessage; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/options/PartialOptions.java b/src/main/java/dan200/computercraft/core/apis/http/options/PartialOptions.java new file mode 100644 index 000000000..fb7150dd5 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/options/PartialOptions.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.core.apis.http.options; + +import javax.annotation.Nonnull; + +public final class PartialOptions +{ + static final PartialOptions DEFAULT = new PartialOptions( null, null, null, null, null ); + + Action action; + Long maxUpload; + Long maxDownload; + Integer timeout; + Integer websocketMessage; + + Options options; + + PartialOptions( Action action, Long maxUpload, Long maxDownload, Integer timeout, Integer websocketMessage ) + { + this.action = action; + this.maxUpload = maxUpload; + this.maxDownload = maxDownload; + this.timeout = timeout; + this.websocketMessage = websocketMessage; + } + + @Nonnull + Options toOptions() + { + if( options != null ) return options; + + return options = new Options( + action == null ? Action.DENY : action, + maxUpload == null ? AddressRule.MAX_UPLOAD : maxUpload, + maxDownload == null ? AddressRule.MAX_DOWNLOAD : maxDownload, + timeout == null ? AddressRule.TIMEOUT : timeout, + websocketMessage == null ? AddressRule.WEBSOCKET_MESSAGE : websocketMessage + ); + } + + void merge( @Nonnull PartialOptions other ) + { + if( action == null && other.action != null ) action = other.action; + if( maxUpload == null && other.maxUpload != null ) maxUpload = other.maxUpload; + if( maxDownload == null && other.maxDownload != null ) maxDownload = other.maxDownload; + if( timeout == null && other.timeout != null ) timeout = other.timeout; + if( websocketMessage == null && other.websocketMessage != null ) websocketMessage = other.websocketMessage; + } + + PartialOptions copy() + { + return new PartialOptions( action, maxUpload, maxDownload, timeout, websocketMessage ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java new file mode 100644 index 000000000..ebd21a655 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java @@ -0,0 +1,283 @@ +/* + * 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.apis.http.request; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.apis.http.HTTPRequestException; +import dan200.computercraft.core.apis.http.NetworkUtils; +import dan200.computercraft.core.apis.http.Resource; +import dan200.computercraft.core.apis.http.ResourceGroup; +import dan200.computercraft.core.apis.http.options.Options; +import dan200.computercraft.core.tracking.TrackingField; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ConnectTimeoutException; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.TooLongFrameException; +import io.netty.handler.codec.http.*; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.timeout.ReadTimeoutException; +import io.netty.handler.timeout.ReadTimeoutHandler; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Represents an in-progress HTTP request. + */ +public class HttpRequest extends Resource +{ + private static final String SUCCESS_EVENT = "http_success"; + private static final String FAILURE_EVENT = "http_failure"; + + private static final int MAX_REDIRECTS = 16; + + private Future executorFuture; + private ChannelFuture connectFuture; + private HttpRequestHandler currentRequest; + + private final IAPIEnvironment environment; + + private final String address; + private final ByteBuf postBuffer; + private final HttpHeaders headers; + private final boolean binary; + + final AtomicInteger redirects; + + public HttpRequest( ResourceGroup limiter, IAPIEnvironment environment, String address, String postText, HttpHeaders headers, boolean binary, boolean followRedirects ) + { + super( limiter ); + this.environment = environment; + this.address = address; + postBuffer = postText != null + ? Unpooled.wrappedBuffer( postText.getBytes( StandardCharsets.UTF_8 ) ) + : Unpooled.buffer( 0 ); + this.headers = headers; + this.binary = binary; + redirects = new AtomicInteger( followRedirects ? MAX_REDIRECTS : 0 ); + + if( postText != null ) + { + if( !headers.contains( HttpHeaderNames.CONTENT_TYPE ) ) + { + headers.set( HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8" ); + } + + if( !headers.contains( HttpHeaderNames.CONTENT_LENGTH ) ) + { + headers.set( HttpHeaderNames.CONTENT_LENGTH, postBuffer.readableBytes() ); + } + } + } + + public IAPIEnvironment environment() + { + return environment; + } + + public static URI checkUri( String address ) throws HTTPRequestException + { + URI url; + try + { + url = new URI( address ); + } + catch( URISyntaxException e ) + { + throw new HTTPRequestException( "URL malformed" ); + } + + checkUri( url ); + return url; + } + + public static void checkUri( URI url ) throws HTTPRequestException + { + // Validate the URL + if( url.getScheme() == null ) throw new HTTPRequestException( "Must specify http or https" ); + if( url.getHost() == null ) throw new HTTPRequestException( "URL malformed" ); + + String scheme = url.getScheme().toLowerCase( Locale.ROOT ); + if( !scheme.equalsIgnoreCase( "http" ) && !scheme.equalsIgnoreCase( "https" ) ) + { + throw new HTTPRequestException( "Invalid protocol '" + scheme + "'" ); + } + } + + public void request( URI uri, HttpMethod method ) + { + if( isClosed() ) return; + executorFuture = NetworkUtils.EXECUTOR.submit( () -> doRequest( uri, method ) ); + checkClosed(); + } + + private void doRequest( URI uri, HttpMethod method ) + { + // If we're cancelled, abort. + if( isClosed() ) return; + + try + { + boolean ssl = uri.getScheme().equalsIgnoreCase( "https" ); + InetSocketAddress socketAddress = NetworkUtils.getAddress( uri.getHost(), uri.getPort(), ssl ); + Options options = NetworkUtils.getOptions( uri.getHost(), socketAddress ); + SslContext sslContext = ssl ? NetworkUtils.getSslContext() : null; + + // getAddress may have a slight delay, so let's perform another cancellation check. + if( isClosed() ) return; + + long requestBody = getHeaderSize( headers ) + postBuffer.capacity(); + if( options.maxUpload != 0 && requestBody > options.maxUpload ) + { + failure( "Request body is too large" ); + return; + } + + // Add request size to the tracker before opening the connection + environment.addTrackingChange( TrackingField.HTTP_REQUESTS, 1 ); + environment.addTrackingChange( TrackingField.HTTP_UPLOAD, requestBody ); + + HttpRequestHandler handler = currentRequest = new HttpRequestHandler( this, uri, method, options ); + connectFuture = new Bootstrap() + .group( NetworkUtils.LOOP_GROUP ) + .channelFactory( NioSocketChannel::new ) + .handler( new ChannelInitializer() + { + @Override + protected void initChannel( SocketChannel ch ) + { + + if( options.timeout > 0 ) + { + ch.config().setConnectTimeoutMillis( options.timeout ); + } + + ChannelPipeline p = ch.pipeline(); + if( sslContext != null ) + { + p.addLast( sslContext.newHandler( ch.alloc(), uri.getHost(), socketAddress.getPort() ) ); + } + + if( options.timeout > 0 ) + { + p.addLast( new ReadTimeoutHandler( options.timeout, TimeUnit.MILLISECONDS ) ); + } + + p.addLast( + new HttpClientCodec(), + new HttpContentDecompressor(), + handler + ); + } + } ) + .remoteAddress( socketAddress ) + .connect() + .addListener( c -> { + if( !c.isSuccess() ) failure( c.cause() ); + } ); + + // Do an additional check for cancellation + checkClosed(); + } + catch( HTTPRequestException e ) + { + failure( e.getMessage() ); + } + catch( Exception e ) + { + failure( "Could not connect" ); + if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error in HTTP request", e ); + } + } + + void failure( String message ) + { + if( tryClose() ) environment.queueEvent( FAILURE_EVENT, address, message ); + } + + void failure( Throwable cause ) + { + String message; + if( cause instanceof HTTPRequestException ) + { + message = cause.getMessage(); + } + else if( cause instanceof TooLongFrameException ) + { + message = "Response is too large"; + } + else if( cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException ) + { + message = "Timed out"; + } + else + { + message = "Could not connect"; + } + + failure( message ); + } + + void failure( String message, HttpResponseHandle object ) + { + if( tryClose() ) environment.queueEvent( FAILURE_EVENT, address, message, object ); + } + + void success( HttpResponseHandle object ) + { + if( tryClose() ) environment.queueEvent( SUCCESS_EVENT, address, object ); + } + + @Override + protected void dispose() + { + super.dispose(); + + executorFuture = closeFuture( executorFuture ); + connectFuture = closeChannel( connectFuture ); + currentRequest = closeCloseable( currentRequest ); + } + + public static long getHeaderSize( HttpHeaders headers ) + { + long size = 0; + for( Map.Entry header : headers ) + { + size += header.getKey() == null ? 0 : header.getKey().length(); + size += header.getValue() == null ? 0 : header.getValue().length() + 1; + } + return size; + } + + public ByteBuf body() + { + return postBuffer; + } + + public HttpHeaders headers() + { + return headers; + } + + public boolean isBinary() + { + return binary; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java new file mode 100644 index 000000000..fbd102be6 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java @@ -0,0 +1,265 @@ +/* + * 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.apis.http.request; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.core.apis.handles.ArrayByteChannel; +import dan200.computercraft.core.apis.handles.BinaryReadableHandle; +import dan200.computercraft.core.apis.handles.EncodedReadableHandle; +import dan200.computercraft.core.apis.handles.HandleGeneric; +import dan200.computercraft.core.apis.http.HTTPRequestException; +import dan200.computercraft.core.apis.http.NetworkUtils; +import dan200.computercraft.core.apis.http.options.Options; +import dan200.computercraft.core.tracking.TrackingField; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.*; + +import java.io.Closeable; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static dan200.computercraft.core.apis.http.request.HttpRequest.getHeaderSize; + +public final class HttpRequestHandler extends SimpleChannelInboundHandler implements Closeable +{ + /** + * Same as {@link io.netty.handler.codec.MessageAggregator}. + */ + private static final int DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS = 1024; + + private static final byte[] EMPTY_BYTES = new byte[0]; + + private final HttpRequest request; + private boolean closed = false; + + private final URI uri; + private final HttpMethod method; + private final Options options; + + private Charset responseCharset; + private final HttpHeaders responseHeaders = new DefaultHttpHeaders(); + private HttpResponseStatus responseStatus; + private CompositeByteBuf responseBody; + + HttpRequestHandler( HttpRequest request, URI uri, HttpMethod method, Options options ) + { + this.request = request; + + this.uri = uri; + this.method = method; + this.options = options; + } + + @Override + public void channelActive( ChannelHandlerContext ctx ) throws Exception + { + if( request.checkClosed() ) return; + + ByteBuf body = request.body(); + body.resetReaderIndex().retain(); + + String requestUri = uri.getRawPath(); + if( uri.getRawQuery() != null ) requestUri += "?" + uri.getRawQuery(); + + FullHttpRequest request = new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, requestUri, body ); + request.setMethod( method ); + request.headers().set( this.request.headers() ); + + // We force some headers to be always applied + if( !request.headers().contains( HttpHeaderNames.ACCEPT_CHARSET ) ) + { + request.headers().set( HttpHeaderNames.ACCEPT_CHARSET, "UTF-8" ); + } + if( !request.headers().contains( HttpHeaderNames.USER_AGENT ) ) + { + request.headers().set( HttpHeaderNames.USER_AGENT, this.request.environment().getComputerEnvironment().getUserAgent() ); + } + request.headers().set( HttpHeaderNames.HOST, uri.getPort() < 0 ? uri.getHost() : uri.getHost() + ":" + uri.getPort() ); + request.headers().set( HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE ); + + ctx.channel().writeAndFlush( request ); + + super.channelActive( ctx ); + } + + @Override + public void channelInactive( ChannelHandlerContext ctx ) throws Exception + { + if( !closed ) request.failure( "Could not connect" ); + super.channelInactive( ctx ); + } + + @Override + public void channelRead0( ChannelHandlerContext ctx, HttpObject message ) + { + if( closed || request.checkClosed() ) return; + + if( message instanceof HttpResponse ) + { + HttpResponse response = (HttpResponse) message; + + if( request.redirects.get() > 0 ) + { + URI redirect = getRedirect( response.status(), response.headers() ); + if( redirect != null && !uri.equals( redirect ) && request.redirects.getAndDecrement() > 0 ) + { + // If we have a redirect, and don't end up at the same place, then follow it. + + // We mark ourselves as disposed first though, to avoid firing events when the channel + // becomes inactive or disposed. + closed = true; + ctx.close(); + + try + { + HttpRequest.checkUri( redirect ); + } + catch( HTTPRequestException e ) + { + // If we cannot visit this uri, then fail. + request.failure( e.getMessage() ); + return; + } + + request.request( redirect, response.status().code() == 303 ? HttpMethod.GET : method ); + return; + } + } + + responseCharset = HttpUtil.getCharset( response, StandardCharsets.UTF_8 ); + responseStatus = response.status(); + responseHeaders.add( response.headers() ); + } + + if( message instanceof HttpContent ) + { + HttpContent content = (HttpContent) message; + + if( responseBody == null ) + { + responseBody = ctx.alloc().compositeBuffer( DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS ); + } + + ByteBuf partial = content.content(); + if( partial.isReadable() ) + { + // If we've read more than we're allowed to handle, abort as soon as possible. + if( options.maxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload ) + { + closed = true; + ctx.close(); + + request.failure( "Response is too large" ); + return; + } + + responseBody.addComponent( true, partial.retain() ); + } + + if( message instanceof LastHttpContent ) + { + LastHttpContent last = (LastHttpContent) message; + responseHeaders.add( last.trailingHeaders() ); + + // Set the content length, if not already given. + if( responseHeaders.contains( HttpHeaderNames.CONTENT_LENGTH ) ) + { + responseHeaders.set( HttpHeaderNames.CONTENT_LENGTH, responseBody.readableBytes() ); + } + + ctx.close(); + sendResponse(); + } + } + } + + @Override + public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) + { + if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error handling HTTP response", cause ); + request.failure( cause ); + } + + private void sendResponse() + { + // Read the ByteBuf into a channel. + CompositeByteBuf body = responseBody; + byte[] bytes = body == null ? EMPTY_BYTES : NetworkUtils.toBytes( body ); + + // Decode the headers + HttpResponseStatus status = responseStatus; + Map headers = new HashMap<>(); + for( Map.Entry header : responseHeaders ) + { + String existing = headers.get( header.getKey() ); + headers.put( header.getKey(), existing == null ? header.getValue() : existing + "," + header.getValue() ); + } + + // Fire off a stats event + request.environment().addTrackingChange( TrackingField.HTTP_DOWNLOAD, getHeaderSize( responseHeaders ) + bytes.length ); + + // Prepare to queue an event + ArrayByteChannel contents = new ArrayByteChannel( bytes ); + HandleGeneric reader = request.isBinary() + ? BinaryReadableHandle.of( contents ) + : new EncodedReadableHandle( EncodedReadableHandle.open( contents, responseCharset ) ); + HttpResponseHandle stream = new HttpResponseHandle( reader, status.code(), status.reasonPhrase(), headers ); + + if( status.code() >= 200 && status.code() < 400 ) + { + request.success( stream ); + } + else + { + request.failure( status.reasonPhrase(), stream ); + } + } + + /** + * Determine the redirect from this response. + * + * @param status The status of the HTTP response. + * @param headers The headers of the HTTP response. + * @return The URI to redirect to, or {@code null} if no redirect should occur. + */ + private URI getRedirect( HttpResponseStatus status, HttpHeaders headers ) + { + int code = status.code(); + if( code < 300 || code > 307 || code == 304 || code == 306 ) return null; + + String location = headers.get( HttpHeaderNames.LOCATION ); + if( location == null ) return null; + + try + { + return uri.resolve( new URI( URLDecoder.decode( location, "UTF-8" ) ) ); + } + catch( UnsupportedEncodingException | IllegalArgumentException | URISyntaxException e ) + { + return null; + } + } + + @Override + public void close() + { + closed = true; + if( responseBody != null ) + { + responseBody.release(); + responseBody = null; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java new file mode 100644 index 000000000..89038f99c --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.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.core.apis.http.request; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.apis.HTTPAPI; +import dan200.computercraft.core.apis.handles.BinaryReadableHandle; +import dan200.computercraft.core.apis.handles.EncodedReadableHandle; +import dan200.computercraft.core.apis.handles.HandleGeneric; +import dan200.computercraft.core.asm.ObjectSource; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Map; + +/** + * A http response. This provides the same methods as a {@link EncodedReadableHandle file} (or + * {@link BinaryReadableHandle binary file} if the request used binary mode), though provides several request specific + * methods. + * + * @cc.module http.Response + * @see HTTPAPI#request(IArguments) On how to make a http request. + */ +public class HttpResponseHandle implements ObjectSource +{ + private final Object reader; + private final int responseCode; + private final String responseStatus; + private final Map responseHeaders; + + public HttpResponseHandle( @Nonnull HandleGeneric reader, int responseCode, String responseStatus, @Nonnull Map responseHeaders ) + { + this.reader = reader; + this.responseCode = responseCode; + this.responseStatus = responseStatus; + this.responseHeaders = responseHeaders; + } + + /** + * Returns the response code and response message returned by the server. + * + * @return The response code and message. + * @cc.treturn number The response code (i.e. 200) + * @cc.treturn string The response message (i.e. "OK") + */ + @LuaFunction + public final Object[] getResponseCode() + { + return new Object[] { responseCode, responseStatus }; + } + + /** + * Get a table containing the response's headers, in a format similar to that required by {@link HTTPAPI#request}. + * If multiple headers are sent with the same name, they will be combined with a comma. + * + * @return The response's headers. + * @cc.usage Make a request to [example.computercraft.cc](https://example.computercraft.cc), and print the + * returned headers. + *
{@code
+     * local request = http.get("https://example.computercraft.cc")
+     * print(textutils.serialize(request.getResponseHeaders()))
+     * -- => {
+     * --  [ "Content-Type" ] = "text/plain; charset=utf8",
+     * --  [ "content-length" ] = 17,
+     * --  ...
+     * -- }
+     * request.close()
+     * }
+ */ + @LuaFunction + public final Map getResponseHeaders() + { + return responseHeaders; + } + + @Override + public Iterable getExtra() + { + return Collections.singletonList( reader ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java new file mode 100644 index 000000000..1841a3df9 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java @@ -0,0 +1,236 @@ +/* + * 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.apis.http.websocket; + +import com.google.common.base.Strings; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.apis.http.HTTPRequestException; +import dan200.computercraft.core.apis.http.NetworkUtils; +import dan200.computercraft.core.apis.http.Resource; +import dan200.computercraft.core.apis.http.ResourceGroup; +import dan200.computercraft.core.apis.http.options.Options; +import dan200.computercraft.shared.util.IoUtil; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory; +import io.netty.handler.codec.http.websocketx.WebSocketVersion; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; +import io.netty.handler.ssl.SslContext; + +import java.lang.ref.WeakReference; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.Future; + +/** + * Provides functionality to verify and connect to a remote websocket. + */ +public class Websocket extends Resource +{ + /** + * We declare the maximum size to be 2^30 bytes. While messages can be much longer, we set an arbitrary limit as + * working with larger messages (especially within a Lua VM) is absurd. + */ + public static final int MAX_MESSAGE_SIZE = 1 << 30; + + static final String SUCCESS_EVENT = "websocket_success"; + static final String FAILURE_EVENT = "websocket_failure"; + static final String CLOSE_EVENT = "websocket_closed"; + static final String MESSAGE_EVENT = "websocket_message"; + + private Future executorFuture; + private ChannelFuture connectFuture; + private WeakReference websocketHandle; + + private final IAPIEnvironment environment; + private final URI uri; + private final String address; + private final HttpHeaders headers; + + public Websocket( ResourceGroup limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers ) + { + super( limiter ); + this.environment = environment; + this.uri = uri; + this.address = address; + this.headers = headers; + } + + public static URI checkUri( String address ) throws HTTPRequestException + { + URI uri = null; + try + { + uri = new URI( address ); + } + catch( URISyntaxException ignored ) + { + } + + if( uri == null || uri.getHost() == null ) + { + try + { + uri = new URI( "ws://" + address ); + } + catch( URISyntaxException ignored ) + { + } + } + + if( uri == null || uri.getHost() == null ) throw new HTTPRequestException( "URL malformed" ); + + String scheme = uri.getScheme(); + if( scheme == null ) + { + try + { + uri = new URI( "ws://" + uri ); + } + catch( URISyntaxException e ) + { + throw new HTTPRequestException( "URL malformed" ); + } + } + else if( !scheme.equalsIgnoreCase( "wss" ) && !scheme.equalsIgnoreCase( "ws" ) ) + { + throw new HTTPRequestException( "Invalid scheme '" + scheme + "'" ); + } + + return uri; + } + + public void connect() + { + if( isClosed() ) return; + executorFuture = NetworkUtils.EXECUTOR.submit( this::doConnect ); + checkClosed(); + } + + private void doConnect() + { + // If we're cancelled, abort. + if( isClosed() ) return; + + try + { + boolean ssl = uri.getScheme().equalsIgnoreCase( "wss" ); + + InetSocketAddress socketAddress = NetworkUtils.getAddress( uri.getHost(), uri.getPort(), ssl ); + Options options = NetworkUtils.getOptions( uri.getHost(), socketAddress ); + SslContext sslContext = ssl ? NetworkUtils.getSslContext() : null; + + // getAddress may have a slight delay, so let's perform another cancellation check. + if( isClosed() ) return; + + connectFuture = new Bootstrap() + .group( NetworkUtils.LOOP_GROUP ) + .channel( NioSocketChannel.class ) + .handler( new ChannelInitializer() + { + @Override + protected void initChannel( SocketChannel ch ) + { + ChannelPipeline p = ch.pipeline(); + if( sslContext != null ) + { + p.addLast( sslContext.newHandler( ch.alloc(), uri.getHost(), socketAddress.getPort() ) ); + } + + WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker( + uri, WebSocketVersion.V13, null, true, headers, + options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage + ); + + p.addLast( + new HttpClientCodec(), + new HttpObjectAggregator( 8192 ), + WebSocketClientCompressionHandler.INSTANCE, + new WebsocketHandler( Websocket.this, handshaker, options ) + ); + } + } ) + .remoteAddress( socketAddress ) + .connect() + .addListener( c -> { + if( !c.isSuccess() ) failure( c.cause().getMessage() ); + } ); + + // Do an additional check for cancellation + checkClosed(); + } + catch( HTTPRequestException e ) + { + failure( e.getMessage() ); + } + catch( Exception e ) + { + failure( "Could not connect" ); + if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error in websocket", e ); + } + } + + void success( Channel channel, Options options ) + { + if( isClosed() ) return; + + WebsocketHandle handle = new WebsocketHandle( this, options, channel ); + environment().queueEvent( SUCCESS_EVENT, address, handle ); + websocketHandle = createOwnerReference( handle ); + + checkClosed(); + } + + void failure( String message ) + { + if( tryClose() ) environment.queueEvent( FAILURE_EVENT, address, message ); + } + + void close( int status, String reason ) + { + if( tryClose() ) + { + environment.queueEvent( CLOSE_EVENT, address, + Strings.isNullOrEmpty( reason ) ? null : reason, + status < 0 ? null : status ); + } + } + + @Override + protected void dispose() + { + super.dispose(); + + executorFuture = closeFuture( executorFuture ); + connectFuture = closeChannel( connectFuture ); + + WeakReference websocketHandleRef = websocketHandle; + WebsocketHandle websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get(); + IoUtil.closeQuietly( websocketHandle ); + this.websocketHandle = null; + } + + public IAPIEnvironment environment() + { + return environment; + } + + public String address() + { + return address; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java new file mode 100644 index 000000000..ce8684002 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java @@ -0,0 +1,162 @@ +/* + * 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.apis.http.websocket; + +import com.google.common.base.Objects; +import dan200.computercraft.api.lua.*; +import dan200.computercraft.core.apis.http.options.Options; +import dan200.computercraft.core.tracking.TrackingField; +import dan200.computercraft.shared.util.StringUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; + +import javax.annotation.Nonnull; +import java.io.Closeable; +import java.util.Arrays; +import java.util.Optional; + +import static dan200.computercraft.api.lua.LuaValues.checkFinite; +import static dan200.computercraft.core.apis.IAPIEnvironment.TIMER_EVENT; +import static dan200.computercraft.core.apis.http.websocket.Websocket.CLOSE_EVENT; +import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT; + +/** + * A websocket, which can be used to send an receive messages with a web server. + * + * @cc.module http.Websocket + * @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket. + */ +public class WebsocketHandle implements Closeable +{ + private final Websocket websocket; + private final Options options; + private boolean closed = false; + + private Channel channel; + + public WebsocketHandle( Websocket websocket, Options options, Channel channel ) + { + this.websocket = websocket; + this.options = options; + this.channel = channel; + } + + /** + * Wait for a message from the server. + * + * @param timeout The number of seconds to wait if no message is received. + * @return The result of receiving. + * @throws LuaException If the websocket has been closed. + * @cc.treturn [1] string The received message. + * @cc.treturn boolean If this was a binary message. + * @cc.treturn [2] nil If the websocket was closed while waiting, or if we timed out. + */ + @LuaFunction + public final MethodResult receive( Optional timeout ) throws LuaException + { + checkOpen(); + int timeoutId = timeout.isPresent() + ? websocket.environment().startTimer( Math.round( checkFinite( 0, timeout.get() ) / 0.05 ) ) + : -1; + + return new ReceiveCallback( timeoutId ).pull; + } + + /** + * Send a websocket message to the connected server. + * + * @param message The message to send. + * @param binary Whether this message should be treated as a + * @throws LuaException If the message is too large. + * @throws LuaException If the websocket has been closed. + */ + @LuaFunction + public final void send( Object message, Optional binary ) throws LuaException + { + checkOpen(); + + String text = StringUtil.toString( message ); + if( options.websocketMessage != 0 && text.length() > options.websocketMessage ) + { + throw new LuaException( "Message is too large" ); + } + + websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_OUTGOING, text.length() ); + + Channel channel = this.channel; + if( channel != null ) + { + channel.writeAndFlush( binary.orElse( false ) + ? new BinaryWebSocketFrame( Unpooled.wrappedBuffer( LuaValues.encode( text ) ) ) + : new TextWebSocketFrame( text ) ); + } + } + + /** + * Close this websocket. This will terminate the connection, meaning messages can no longer be sent or received + * along it. + */ + @LuaFunction( "close" ) + public final void doClose() + { + close(); + websocket.close(); + } + + private void checkOpen() throws LuaException + { + if( closed ) throw new LuaException( "attempt to use a closed file" ); + } + + @Override + public void close() + { + closed = true; + + Channel channel = this.channel; + if( channel != null ) + { + channel.close(); + this.channel = null; + } + } + + private final class ReceiveCallback implements ILuaCallback + { + final MethodResult pull = MethodResult.pullEvent( null, this ); + private final int timeoutId; + + ReceiveCallback( int timeoutId ) + { + this.timeoutId = timeoutId; + } + + @Nonnull + @Override + public MethodResult resume( Object[] event ) + { + if( event.length >= 3 && Objects.equal( event[0], MESSAGE_EVENT ) && Objects.equal( event[1], websocket.address() ) ) + { + return MethodResult.of( Arrays.copyOfRange( event, 2, event.length ) ); + } + else if( event.length >= 2 && Objects.equal( event[0], CLOSE_EVENT ) && Objects.equal( event[1], websocket.address() ) && closed ) + { + // If the socket is closed abort. + return MethodResult.of(); + } + else if( event.length >= 2 && timeoutId != -1 && Objects.equal( event[0], TIMER_EVENT ) + && event[1] instanceof Number && ((Number) event[1]).intValue() == timeoutId ) + { + // If we received a matching timer event then abort. + return MethodResult.of(); + } + + return pull; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java new file mode 100644 index 000000000..52c27b97c --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java @@ -0,0 +1,127 @@ +/* + * 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.apis.http.websocket; + +import dan200.computercraft.core.apis.http.HTTPRequestException; +import dan200.computercraft.core.apis.http.NetworkUtils; +import dan200.computercraft.core.apis.http.options.Options; +import dan200.computercraft.core.tracking.TrackingField; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ConnectTimeoutException; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.TooLongFrameException; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.websocketx.*; +import io.netty.handler.timeout.ReadTimeoutException; +import io.netty.util.CharsetUtil; + +import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT; + +public class WebsocketHandler extends SimpleChannelInboundHandler +{ + private final Websocket websocket; + private final WebSocketClientHandshaker handshaker; + private final Options options; + + public WebsocketHandler( Websocket websocket, WebSocketClientHandshaker handshaker, Options options ) + { + this.handshaker = handshaker; + this.websocket = websocket; + this.options = options; + } + + @Override + public void channelActive( ChannelHandlerContext ctx ) throws Exception + { + handshaker.handshake( ctx.channel() ); + super.channelActive( ctx ); + } + + @Override + public void channelInactive( ChannelHandlerContext ctx ) throws Exception + { + websocket.close( -1, "Websocket is inactive" ); + super.channelInactive( ctx ); + } + + @Override + public void channelRead0( ChannelHandlerContext ctx, Object msg ) + { + if( websocket.isClosed() ) return; + + if( !handshaker.isHandshakeComplete() ) + { + handshaker.finishHandshake( ctx.channel(), (FullHttpResponse) msg ); + websocket.success( ctx.channel(), options ); + return; + } + + if( msg instanceof FullHttpResponse ) + { + FullHttpResponse response = (FullHttpResponse) msg; + throw new IllegalStateException( "Unexpected FullHttpResponse (getStatus=" + response.status() + ", content=" + response.content().toString( CharsetUtil.UTF_8 ) + ')' ); + } + + WebSocketFrame frame = (WebSocketFrame) msg; + if( frame instanceof TextWebSocketFrame ) + { + String data = ((TextWebSocketFrame) frame).text(); + + websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_INCOMING, data.length() ); + websocket.environment().queueEvent( MESSAGE_EVENT, websocket.address(), data, false ); + } + else if( frame instanceof BinaryWebSocketFrame ) + { + byte[] converted = NetworkUtils.toBytes( frame.content() ); + + websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_INCOMING, converted.length ); + websocket.environment().queueEvent( MESSAGE_EVENT, websocket.address(), converted, true ); + } + else if( frame instanceof CloseWebSocketFrame ) + { + CloseWebSocketFrame closeFrame = (CloseWebSocketFrame) frame; + websocket.close( closeFrame.statusCode(), closeFrame.reasonText() ); + } + else if( frame instanceof PingWebSocketFrame ) + { + frame.content().retain(); + ctx.channel().writeAndFlush( new PongWebSocketFrame( frame.content() ) ); + } + } + + @Override + public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) + { + ctx.close(); + + String message; + if( cause instanceof WebSocketHandshakeException || cause instanceof HTTPRequestException ) + { + message = cause.getMessage(); + } + else if( cause instanceof TooLongFrameException ) + { + message = "Message is too large"; + } + else if( cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException ) + { + message = "Timed out"; + } + else + { + message = "Could not connect"; + } + + if( handshaker.isHandshakeComplete() ) + { + websocket.close( -1, message ); + } + else + { + websocket.failure( message ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/DeclaringClassLoader.java b/src/main/java/dan200/computercraft/core/asm/DeclaringClassLoader.java new file mode 100644 index 000000000..009a8840e --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/DeclaringClassLoader.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.core.asm; + +import java.security.ProtectionDomain; + +final class DeclaringClassLoader extends ClassLoader +{ + static final DeclaringClassLoader INSTANCE = new DeclaringClassLoader(); + + private DeclaringClassLoader() + { + super( DeclaringClassLoader.class.getClassLoader() ); + } + + Class define( String name, byte[] bytes, ProtectionDomain protectionDomain ) throws ClassFormatError + { + return defineClass( name, bytes, 0, bytes.length, protectionDomain ); + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/Generator.java b/src/main/java/dan200/computercraft/core/asm/Generator.java new file mode 100644 index 000000000..2e80cd86c --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -0,0 +1,362 @@ +/* + * 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 com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; +import dan200.computercraft.ComputerCraft; +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 org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import static org.objectweb.asm.Opcodes.*; + +public final class Generator +{ + private static final AtomicInteger METHOD_ID = new AtomicInteger(); + + private static final String METHOD_NAME = "apply"; + private static final String[] EXCEPTIONS = new String[] { Type.getInternalName( LuaException.class ) }; + + private static final String INTERNAL_METHOD_RESULT = Type.getInternalName( MethodResult.class ); + private static final String DESC_METHOD_RESULT = Type.getDescriptor( MethodResult.class ); + + private static final String INTERNAL_ARGUMENTS = Type.getInternalName( IArguments.class ); + private static final String DESC_ARGUMENTS = Type.getDescriptor( IArguments.class ); + + private final Class base; + private final List> context; + + private final String[] interfaces; + private final String methodDesc; + + private final Function wrap; + + private final LoadingCache, List>> classCache = CacheBuilder + .newBuilder() + .build( CacheLoader.from( this::build ) ); + + private final LoadingCache> methodCache = CacheBuilder + .newBuilder() + .build( CacheLoader.from( this::build ) ); + + Generator( Class base, List> context, Function wrap ) + { + this.base = base; + this.context = context; + this.interfaces = new String[] { Type.getInternalName( base ) }; + this.wrap = wrap; + + StringBuilder methodDesc = new StringBuilder().append( "(Ljava/lang/Object;" ); + for( Class klass : context ) methodDesc.append( Type.getDescriptor( klass ) ); + methodDesc.append( DESC_ARGUMENTS ).append( ")" ).append( DESC_METHOD_RESULT ); + this.methodDesc = methodDesc.toString(); + } + + @Nonnull + public List> getMethods( @Nonnull Class klass ) + { + try + { + return classCache.get( klass ); + } + catch( ExecutionException e ) + { + ComputerCraft.log.error( "Error getting methods for {}.", klass.getName(), e.getCause() ); + return Collections.emptyList(); + } + } + + @Nonnull + private List> build( Class klass ) + { + ArrayList> methods = null; + for( Method method : klass.getMethods() ) + { + 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 ); + } + + for( GenericSource.GenericMethod method : GenericSource.GenericMethod.all() ) + { + if( !method.target.isAssignableFrom( klass ) ) continue; + + 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(); + methods.trimToSize(); + 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(); + + // 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.isPublic( modifiers ) ) + { + ComputerCraft.log.error( "Lua Method {} should be a public method.", name ); + return Optional.empty(); + } + + if( !Modifier.isPublic( method.getDeclaringClass().getModifiers() ) ) + { + ComputerCraft.log.error( "Lua Method {} should be on a public class.", name ); + return Optional.empty(); + } + + ComputerCraft.log.debug( "Generating method wrapper for {}.", name ); + + Class[] exceptions = method.getExceptionTypes(); + for( Class exception : exceptions ) + { + if( exception != LuaException.class ) + { + ComputerCraft.log.error( "Lua Method {} cannot throw {}.", name, exception.getName() ); + return Optional.empty(); + } + } + + // 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, target, method ); + if( bytes == null ) return Optional.empty(); + + Class klass = DeclaringClassLoader.INSTANCE.define( className, bytes, method.getDeclaringClass().getProtectionDomain() ); + return Optional.of( klass.asSubclass( base ).getDeclaredConstructor().newInstance() ); + } + catch( ReflectiveOperationException | ClassFormatError | RuntimeException e ) + { + ComputerCraft.log.error( "Error generating wrapper for {}.", name, e ); + return Optional.empty(); + } + + } + + @Nullable + private byte[] generate( String className, Class target, Method method ) + { + String internalName = className.replace( ".", "/" ); + + // Construct a public final class which extends Object and implements MethodInstance.Delegate + ClassWriter cw = new ClassWriter( ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS ); + cw.visit( V1_8, ACC_PUBLIC | ACC_FINAL, internalName, null, "java/lang/Object", interfaces ); + cw.visitSource( "CC generated method", null ); + + { // Constructor just invokes super. + MethodVisitor mw = cw.visitMethod( ACC_PUBLIC, "", "()V", null, null ); + mw.visitCode(); + mw.visitVarInsn( ALOAD, 0 ); + mw.visitMethodInsn( INVOKESPECIAL, "java/lang/Object", "", "()V", false ); + mw.visitInsn( RETURN ); + mw.visitMaxs( 0, 0 ); + mw.visitEnd(); + } + + { + MethodVisitor mw = cw.visitMethod( ACC_PUBLIC, METHOD_NAME, methodDesc, null, EXCEPTIONS ); + mw.visitCode(); + + // 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, target, method, genericArg, argIndex ); + if( loadedArg == null ) return null; + if( loadedArg ) argIndex++; + } + + 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. + Class ret = method.getReturnType(); + if( ret != MethodResult.class ) + { + if( ret == void.class ) + { + mw.visitMethodInsn( INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "()" + DESC_METHOD_RESULT, false ); + } + else if( ret.isPrimitive() ) + { + Class boxed = Primitives.wrap( ret ); + mw.visitMethodInsn( INVOKESTATIC, Type.getInternalName( boxed ), "valueOf", "(" + Type.getDescriptor( ret ) + ")" + Type.getDescriptor( boxed ), false ); + mw.visitMethodInsn( INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT, false ); + } + else if( ret == Object[].class ) + { + mw.visitMethodInsn( INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "([Ljava/lang/Object;)" + DESC_METHOD_RESULT, false ); + } + else + { + mw.visitMethodInsn( INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT, false ); + } + } + + mw.visitInsn( ARETURN ); + + mw.visitMaxs( 0, 0 ); + mw.visitEnd(); + } + + cw.visitEnd(); + + return cw.toByteArray(); + } + + 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; + + if( arg == IArguments.class ) + { + mw.visitVarInsn( ALOAD, 2 + context.size() ); + return false; + } + + int idx = context.indexOf( arg ); + if( idx >= 0 ) + { + mw.visitVarInsn( ALOAD, 2 + idx ); + return false; + } + + if( arg == Optional.class ) + { + Class klass = Reflect.getRawType( method, TypeToken.of( genericArg ).resolveType( Reflect.OPTIONAL_IN ).getType(), false ); + if( klass == null ) return null; + + if( Enum.class.isAssignableFrom( klass ) && klass != Enum.class ) + { + mw.visitVarInsn( ALOAD, 2 + context.size() ); + Reflect.loadInt( mw, argIndex ); + mw.visitLdcInsn( Type.getType( klass ) ); + mw.visitMethodInsn( INVOKEINTERFACE, INTERNAL_ARGUMENTS, "optEnum", "(ILjava/lang/Class;)Ljava/util/Optional;", true ); + return true; + } + + String name = Reflect.getLuaName( Primitives.unwrap( klass ) ); + if( name != null ) + { + mw.visitVarInsn( ALOAD, 2 + context.size() ); + Reflect.loadInt( mw, argIndex ); + mw.visitMethodInsn( INVOKEINTERFACE, INTERNAL_ARGUMENTS, "opt" + name, "(I)Ljava/util/Optional;", true ); + return true; + } + } + + if( Enum.class.isAssignableFrom( arg ) && arg != Enum.class ) + { + mw.visitVarInsn( ALOAD, 2 + context.size() ); + Reflect.loadInt( mw, argIndex ); + mw.visitLdcInsn( Type.getType( arg ) ); + mw.visitMethodInsn( INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getEnum", "(ILjava/lang/Class;)Ljava/lang/Enum;", true ); + mw.visitTypeInsn( CHECKCAST, Type.getInternalName( arg ) ); + return true; + } + + String name = arg == Object.class ? "" : Reflect.getLuaName( arg ); + if( name != null ) + { + if( Reflect.getRawType( method, genericArg, false ) == null ) return null; + + mw.visitVarInsn( ALOAD, 2 + context.size() ); + Reflect.loadInt( mw, argIndex ); + mw.visitMethodInsn( INVOKEINTERFACE, INTERNAL_ARGUMENTS, "get" + name, "(I)" + Type.getDescriptor( arg ), true ); + return true; + } + + ComputerCraft.log.error( "Unknown parameter type {} for method {}.{}.", + arg.getName(), method.getDeclaringClass().getName(), method.getName() ); + 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..e84fc2ab4 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/GenericSource.java @@ -0,0 +1,120 @@ +/* + * 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 dan200.computercraft.shared.util.ServiceUtil; +import javax.annotation.Nonnull; +import net.minecraft.util.Identifier; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 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 + Identifier id(); + + /** + * Register a stream of generic sources. + * + * @param sources The source of generic methods. + * @see ServiceUtil For ways to load this. Sadly {@link java.util.ServiceLoader} is broken under Forge, but we don't + * want to add a hard-dep on Forge within core either. + */ + static void setup( Supplier> sources ) + { + GenericMethod.sources = sources; + } + + /** + * 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; + + static Supplier> sources; + 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; + if( sources == null ) + { + ComputerCraft.log.warn( "Getting GenericMethods without a provider" ); + return cache = Collections.emptyList(); + } + + return cache = sources.get() + .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/IntCache.java b/src/main/java/dan200/computercraft/core/asm/IntCache.java new file mode 100644 index 000000000..3652a530d --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/IntCache.java @@ -0,0 +1,41 @@ +/* + * 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 java.util.Arrays; +import java.util.function.IntFunction; + +public final class IntCache +{ + private final IntFunction factory; + private volatile Object[] cache = new Object[16]; + + IntCache( IntFunction factory ) + { + this.factory = factory; + } + + @SuppressWarnings( "unchecked" ) + public T get( int index ) + { + if( index < 0 ) throw new IllegalArgumentException( "index < 0" ); + + if( index < cache.length ) + { + T current = (T) cache[index]; + if( current != null ) return current; + } + + synchronized( this ) + { + if( index >= cache.length ) cache = Arrays.copyOf( cache, Math.max( cache.length * 2, index + 1 ) ); + T current = (T) cache[index]; + if( current == null ) cache[index] = current = factory.apply( index ); + return current; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/LuaMethod.java b/src/main/java/dan200/computercraft/core/asm/LuaMethod.java new file mode 100644 index 000000000..4bf142903 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/LuaMethod.java @@ -0,0 +1,28 @@ +/* + * 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.api.lua.*; + +import javax.annotation.Nonnull; +import java.util.Collections; + +public interface LuaMethod +{ + Generator GENERATOR = new Generator<>( LuaMethod.class, Collections.singletonList( ILuaContext.class ), + 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 ) + ); + + String[] EMPTY_METHODS = new String[0]; + + @Nonnull + MethodResult apply( @Nonnull Object target, @Nonnull ILuaContext context, @Nonnull IArguments args ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/core/asm/NamedMethod.java b/src/main/java/dan200/computercraft/core/asm/NamedMethod.java new file mode 100644 index 000000000..f7d0ffef4 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/NamedMethod.java @@ -0,0 +1,40 @@ +/* + * 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 javax.annotation.Nonnull; + +public final class NamedMethod +{ + private final String name; + private final T method; + private final boolean nonYielding; + + NamedMethod( String name, T method, boolean nonYielding ) + { + this.name = name; + this.method = method; + this.nonYielding = nonYielding; + } + + @Nonnull + public String getName() + { + return name; + } + + @Nonnull + public T getMethod() + { + return method; + } + + public boolean nonYielding() + { + return nonYielding; + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/ObjectSource.java b/src/main/java/dan200/computercraft/core/asm/ObjectSource.java new file mode 100644 index 000000000..f05ac7070 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/ObjectSource.java @@ -0,0 +1,33 @@ +/* + * 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 java.util.function.BiConsumer; + +/** + * A Lua object which exposes additional methods. + * + * This can be used to merge multiple objects together into one. Ideally this'd be part of the API, but I'm not entirely + * happy with the interface - something I'd like to think about first. + */ +public interface ObjectSource +{ + Iterable getExtra(); + + static void allMethods( Generator generator, Object object, BiConsumer> accept ) + { + for( NamedMethod method : generator.getMethods( object.getClass() ) ) accept.accept( object, method ); + + if( object instanceof ObjectSource ) + { + for( Object extra : ((ObjectSource) object).getExtra() ) + { + for( NamedMethod method : generator.getMethods( extra.getClass() ) ) accept.accept( extra, method ); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java b/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java new file mode 100644 index 000000000..f92a988f0 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java @@ -0,0 +1,31 @@ +/* + * 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.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 javax.annotation.Nonnull; +import java.util.Arrays; + +public interface PeripheralMethod +{ + Generator GENERATOR = new Generator<>( PeripheralMethod.class, Arrays.asList( ILuaContext.class, IComputerAccess.class ), + 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 ) + ); + + @Nonnull + MethodResult apply( @Nonnull Object target, @Nonnull ILuaContext context, @Nonnull IComputerAccess computer, @Nonnull IArguments args ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/core/asm/Reflect.java b/src/main/java/dan200/computercraft/core/asm/Reflect.java new file mode 100644 index 000000000..429402206 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/Reflect.java @@ -0,0 +1,95 @@ +/* + * 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 org.objectweb.asm.MethodVisitor; + +import javax.annotation.Nullable; +import java.lang.reflect.*; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Optional; + +import static org.objectweb.asm.Opcodes.ICONST_0; + +final class Reflect +{ + static final java.lang.reflect.Type OPTIONAL_IN = Optional.class.getTypeParameters()[0]; + + private Reflect() + { + } + + @Nullable + static String getLuaName( Class klass ) + { + if( klass.isPrimitive() ) + { + if( klass == int.class ) return "Int"; + if( klass == boolean.class ) return "Boolean"; + if( klass == double.class ) return "Double"; + if( klass == long.class ) return "Long"; + } + else + { + if( klass == Map.class ) return "Table"; + if( klass == String.class ) return "String"; + if( klass == ByteBuffer.class ) return "Bytes"; + } + + return null; + } + + @Nullable + static Class getRawType( Method method, Type root, boolean allowParameter ) + { + Type underlying = root; + while( true ) + { + if( underlying instanceof Class ) return (Class) underlying; + + if( underlying instanceof ParameterizedType ) + { + ParameterizedType type = (ParameterizedType) underlying; + if( !allowParameter ) + { + for( java.lang.reflect.Type arg : type.getActualTypeArguments() ) + { + if( arg instanceof WildcardType ) continue; + if( arg instanceof TypeVariable && ((TypeVariable) arg).getName().startsWith( "capture#" ) ) + { + continue; + } + + ComputerCraft.log.error( "Method {}.{} has generic type {} with non-wildcard argument {}.", method.getDeclaringClass(), method.getName(), root, arg ); + return null; + } + } + + // Continue to extract from this child + underlying = type.getRawType(); + continue; + } + + ComputerCraft.log.error( "Method {}.{} has unknown generic type {}.", method.getDeclaringClass(), method.getName(), root ); + return null; + } + } + + static void loadInt( MethodVisitor visitor, int value ) + { + if( value >= -1 && value <= 5 ) + { + visitor.visitInsn( ICONST_0 + value ); + } + else + { + visitor.visitLdcInsn( value ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/TaskCallback.java b/src/main/java/dan200/computercraft/core/asm/TaskCallback.java new file mode 100644 index 000000000..af8d8814d --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/TaskCallback.java @@ -0,0 +1,68 @@ +/* + * 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.api.lua.*; + +import javax.annotation.Nonnull; +import java.util.Arrays; + +public final class TaskCallback implements ILuaCallback +{ + private final MethodResult pull = MethodResult.pullEvent( "task_complete", this ); + private final long task; + + private TaskCallback( long task ) + { + this.task = task; + } + + @Nonnull + @Override + public MethodResult resume( Object[] response ) throws LuaException + { + if( response.length < 3 || !(response[1] instanceof Number) || !(response[2] instanceof Boolean) ) + { + return pull; + } + + if( ((Number) response[1]).longValue() != task ) return pull; + + if( (Boolean) response[2] ) + { + // Extract the return values from the event and return them + return MethodResult.of( Arrays.copyOfRange( response, 3, response.length ) ); + } + else if( response.length >= 4 && response[3] instanceof String ) + { + // Extract the error message from the event and raise it + throw new LuaException( (String) response[3] ); + } + else + { + throw new LuaException( "error" ); + } + } + + static Object[] checkUnwrap( MethodResult result ) + { + if( result.getCallback() != null ) + { + // Due to how tasks are implemented, we can't currently return a MethodResult. This is an + // entirely artificial limitation - we can remove it if it ever becomes an issue. + throw new IllegalStateException( "Cannot return MethodResult for mainThread task." ); + } + + 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/core/computer/ApiWrapper.java b/src/main/java/dan200/computercraft/core/computer/ApiWrapper.java new file mode 100644 index 000000000..693045dfe --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/ApiWrapper.java @@ -0,0 +1,53 @@ +/* + * 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.computer; + +import dan200.computercraft.api.lua.ILuaAPI; + +/** + * A wrapper for {@link ILuaAPI}s which cleans up after a {@link ComputerSystem} when the computer is shutdown. + */ +final class ApiWrapper implements ILuaAPI +{ + private final ILuaAPI delegate; + private final ComputerSystem system; + + ApiWrapper( ILuaAPI delegate, ComputerSystem system ) + { + this.delegate = delegate; + this.system = system; + } + + @Override + public String[] getNames() + { + return delegate.getNames(); + } + + @Override + public void startup() + { + delegate.startup(); + } + + @Override + public void update() + { + delegate.update(); + } + + @Override + public void shutdown() + { + delegate.shutdown(); + system.unmountAll(); + } + + public ILuaAPI getDelegate() + { + return delegate; + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/Computer.java b/src/main/java/dan200/computercraft/core/computer/Computer.java new file mode 100644 index 000000000..8affb0f01 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/Computer.java @@ -0,0 +1,219 @@ +/* + * 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.computer; + +import com.google.common.base.Objects; +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.peripheral.IWorkMonitor; +import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.filesystem.FileSystem; +import dan200.computercraft.core.terminal.Terminal; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Represents a computer which may exist in-world or elsewhere. + * + * Note, this class has several (read: far, far too many) responsibilities, so can get a little unwieldy at times. + * + *
    + *
  • Updates the {@link Environment}.
  • + *
  • Keeps track of whether the computer is on and blinking.
  • + *
  • Monitors whether the computer's visible state (redstone, on/off/blinking) has changed.
  • + *
  • Passes commands and events to the {@link ComputerExecutor}.
  • + *
  • Passes main thread tasks to the {@link MainThreadExecutor}.
  • + *
+ */ +public class Computer +{ + private static final int START_DELAY = 50; + + // Various properties of the computer + private int m_id; + private String m_label = null; + + // Read-only fields about the computer + private final IComputerEnvironment m_environment; + private final Terminal m_terminal; + private final ComputerExecutor executor; + private final MainThreadExecutor serverExecutor; + + // Additional state about the computer and its environment. + private boolean m_blinking = false; + private final Environment internalEnvironment = new Environment( this ); + private final AtomicBoolean externalOutputChanged = new AtomicBoolean(); + + private boolean startRequested; + private int m_ticksSinceStart = -1; + + public Computer( IComputerEnvironment environment, Terminal terminal, int id ) + { + m_id = id; + m_environment = environment; + m_terminal = terminal; + + executor = new ComputerExecutor( this ); + serverExecutor = new MainThreadExecutor( this ); + } + + IComputerEnvironment getComputerEnvironment() + { + return m_environment; + } + + FileSystem getFileSystem() + { + return executor.getFileSystem(); + } + + Terminal getTerminal() + { + return m_terminal; + } + + public Environment getEnvironment() + { + return internalEnvironment; + } + + public IAPIEnvironment getAPIEnvironment() + { + return internalEnvironment; + } + + public boolean isOn() + { + return executor.isOn(); + } + + public void turnOn() + { + startRequested = true; + } + + public void shutdown() + { + executor.queueStop( false, false ); + } + + public void reboot() + { + executor.queueStop( true, false ); + } + + public void unload() + { + executor.queueStop( false, true ); + } + + public void queueEvent( String event, Object[] args ) + { + executor.queueEvent( event, args ); + } + + /** + * Queue a task to be run on the main thread, using {@link MainThread}. + * + * @param runnable The task to run + * @return If the task was successfully queued (namely, whether there is space on it). + */ + public boolean queueMainThread( Runnable runnable ) + { + return serverExecutor.enqueue( runnable ); + } + + public IWorkMonitor getMainThreadMonitor() + { + return serverExecutor; + } + + public int getID() + { + return m_id; + } + + public int assignID() + { + if( m_id < 0 ) + { + m_id = m_environment.assignNewID(); + } + return m_id; + } + + public void setID( int id ) + { + m_id = id; + } + + public String getLabel() + { + return m_label; + } + + public void setLabel( String label ) + { + if( !Objects.equal( label, m_label ) ) + { + m_label = label; + externalOutputChanged.set( true ); + } + } + + public void tick() + { + // We keep track of the number of ticks since the last start, only + if( m_ticksSinceStart >= 0 && m_ticksSinceStart <= START_DELAY ) m_ticksSinceStart++; + + if( startRequested && (m_ticksSinceStart < 0 || m_ticksSinceStart > START_DELAY) ) + { + startRequested = false; + if( !executor.isOn() ) + { + m_ticksSinceStart = 0; + executor.queueStart(); + } + } + + executor.tick(); + + // Update the environment's internal state. + internalEnvironment.tick(); + + // Propagate the environment's output to the world. + if( internalEnvironment.updateOutput() ) externalOutputChanged.set( true ); + + // Set output changed if the terminal has changed from blinking to not + boolean blinking = m_terminal.getCursorBlink() && + m_terminal.getCursorX() >= 0 && m_terminal.getCursorX() < m_terminal.getWidth() && + m_terminal.getCursorY() >= 0 && m_terminal.getCursorY() < m_terminal.getHeight(); + if( blinking != m_blinking ) + { + m_blinking = blinking; + externalOutputChanged.set( true ); + } + } + + void markChanged() + { + externalOutputChanged.set( true ); + } + + public boolean pollAndResetChanged() + { + return externalOutputChanged.getAndSet( false ); + } + + public boolean isBlinking() + { + return isOn() && m_blinking; + } + + public void addApi( ILuaAPI api ) + { + executor.addApi( api ); + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java b/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java new file mode 100644 index 000000000..021c4b473 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java @@ -0,0 +1,660 @@ +/* + * 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.computer; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.ILuaAPIFactory; +import dan200.computercraft.core.apis.*; +import dan200.computercraft.core.filesystem.FileSystem; +import dan200.computercraft.core.filesystem.FileSystemException; +import dan200.computercraft.core.lua.CobaltLuaMachine; +import dan200.computercraft.core.lua.ILuaMachine; +import dan200.computercraft.core.lua.MachineResult; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.tracking.Tracking; +import dan200.computercraft.shared.util.Colour; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.InputStream; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +/** + * The main task queue and executor for a single computer. This handles turning on and off a computer, as well as + * running events. + * + * When the computer is instructed to turn on or off, or handle an event, we queue a task and register this to be + * executed on the {@link ComputerThread}. Note, as we may be starting many events in a single tick, the external + * cannot lock on anything which may be held for a long time. + * + * The executor is effectively composed of two separate queues. Firstly, we have a "single element" queue + * {@link #command} which determines which state the computer should transition too. This is set by + * {@link #queueStart()} and {@link #queueStop(boolean, boolean)}. + * + * When a computer is on, we simply push any events onto to the {@link #eventQueue}. + * + * Both queues are run from the {@link #work()} method, which tries to execute a command if one exists, or resumes the + * machine with an event otherwise. + * + * One final responsibility for the executor is calling {@link ILuaAPI#update()} every tick, via the {@link #tick()} + * method. This should only be called when the computer is actually on ({@link #isOn}). + */ +final class ComputerExecutor +{ + private static final int QUEUE_LIMIT = 256; + + private final Computer computer; + private final List apis = new ArrayList<>(); + final TimeoutState timeout = new TimeoutState(); + + private FileSystem fileSystem; + + private ILuaMachine machine; + + /** + * Whether the computer is currently on. This is set to false when a shutdown starts, or when turning on completes + * (but just before the Lua machine is started). + * + * @see #isOnLock + */ + private volatile boolean isOn = false; + + /** + * The lock to acquire when you need to modify the "on state" of a computer. + * + * We hold this lock when running any command, and attempt to hold it when updating APIs. This ensures you don't + * update APIs while also starting/stopping them. + * + * @see #isOn + * @see #tick() + * @see #turnOn() + * @see #shutdown() + */ + private final ReentrantLock isOnLock = new ReentrantLock(); + + /** + * A lock used for any changes to {@link #eventQueue}, {@link #command} or {@link #onComputerQueue}. This will be + * used on the main thread, so locks should be kept as brief as possible. + */ + private final Object queueLock = new Object(); + + /** + * Determines if this executor is present within {@link ComputerThread}. + * + * @see #queueLock + * @see #enqueue() + * @see #afterWork() + */ + volatile boolean onComputerQueue = false; + + /** + * The amount of time this computer has used on a theoretical machine which shares work evenly amongst computers. + * + * @see ComputerThread + */ + long virtualRuntime = 0; + + /** + * The last time at which we updated {@link #virtualRuntime}. + * + * @see ComputerThread + */ + long vRuntimeStart; + + /** + * The command that {@link #work()} should execute on the computer thread. + * + * One sets the command with {@link #queueStart()} and {@link #queueStop(boolean, boolean)}. Neither of these will + * queue a new event if there is an existing one in the queue. + * + * Note, if command is not {@code null}, then some command is scheduled to be executed. Otherwise it is not + * currently in the queue (or is currently being executed). + */ + private volatile StateCommand command; + + /** + * The queue of events which should be executed when this computer is on. + * + * Note, this should be empty if this computer is off - it is cleared on shutdown and when turning on again. + */ + private final Queue eventQueue = new ArrayDeque<>( 4 ); + + /** + * Whether we interrupted an event and so should resume it instead of executing another task. + * + * @see #work() + * @see #resumeMachine(String, Object[]) + */ + private boolean interruptedEvent = false; + + /** + * Whether this executor has been closed, and will no longer accept any incoming commands or events. + * + * @see #queueStop(boolean, boolean) + */ + private boolean closed; + + private IWritableMount rootMount; + + /** + * The thread the executor is running on. This is non-null when performing work. We use this to ensure we're only + * doing one bit of work at one time. + * + * @see ComputerThread + */ + final AtomicReference executingThread = new AtomicReference<>(); + + ComputerExecutor( Computer computer ) + { + // Ensure the computer thread is running as required. + ComputerThread.start(); + + this.computer = computer; + + Environment environment = computer.getEnvironment(); + + // Add all default APIs to the loaded list. + apis.add( new TermAPI( environment ) ); + apis.add( new RedstoneAPI( environment ) ); + apis.add( new FSAPI( environment ) ); + apis.add( new PeripheralAPI( environment ) ); + apis.add( new OSAPI( environment ) ); + if( ComputerCraft.httpEnabled ) apis.add( new HTTPAPI( environment ) ); + + // Load in the externally registered APIs. + for( ILuaAPIFactory factory : ApiFactories.getAll() ) + { + ComputerSystem system = new ComputerSystem( environment ); + ILuaAPI api = factory.create( system ); + if( api != null ) apis.add( new ApiWrapper( api, system ) ); + } + } + + boolean isOn() + { + return isOn; + } + + FileSystem getFileSystem() + { + return fileSystem; + } + + Computer getComputer() + { + return computer; + } + + void addApi( ILuaAPI api ) + { + apis.add( api ); + } + + /** + * Schedule this computer to be started if not already on. + */ + void queueStart() + { + synchronized( queueLock ) + { + // We should only schedule a start if we're not currently on and there's turn on. + if( closed || isOn || command != null ) return; + + command = StateCommand.TURN_ON; + enqueue(); + } + } + + /** + * Schedule this computer to be stopped if not already on. + * + * @param reboot Reboot the computer after stopping + * @param close Close the computer after stopping. + * @see #closed + */ + void queueStop( boolean reboot, boolean close ) + { + synchronized( queueLock ) + { + if( closed ) return; + closed = close; + + StateCommand newCommand = reboot ? StateCommand.REBOOT : StateCommand.SHUTDOWN; + + // We should only schedule a stop if we're currently on and there's no shutdown pending. + if( !isOn || command != null ) + { + // If we're closing, set the command just in case. + if( close ) command = newCommand; + return; + } + + command = newCommand; + enqueue(); + } + } + + /** + * Abort this whole computer due to a timeout. This will immediately destroy the Lua machine, + * and then schedule a shutdown. + */ + void abort() + { + ILuaMachine machine = this.machine; + if( machine != null ) machine.close(); + + synchronized( queueLock ) + { + if( closed ) return; + command = StateCommand.ABORT; + if( isOn ) enqueue(); + } + } + + /** + * Queue an event if the computer is on. + * + * @param event The event's name + * @param args The event's arguments + */ + void queueEvent( @Nonnull String event, @Nullable Object[] args ) + { + // Events should be skipped if we're not on. + if( !isOn ) return; + + synchronized( queueLock ) + { + // And if we've got some command in the pipeline, then don't queue events - they'll + // probably be disposed of anyway. + // We also limit the number of events which can be queued. + if( closed || command != null || eventQueue.size() >= QUEUE_LIMIT ) return; + + eventQueue.offer( new Event( event, args ) ); + enqueue(); + } + } + + /** + * Add this executor to the {@link ComputerThread} if not already there. + */ + private void enqueue() + { + synchronized( queueLock ) + { + if( !onComputerQueue ) ComputerThread.queue( this ); + } + } + + /** + * Update the internals of the executor. + */ + void tick() + { + if( isOn && isOnLock.tryLock() ) + { + // This horrific structure means we don't try to update APIs while the state is being changed + // (and so they may be running startup/shutdown). + // We use tryLock here, as it has minimal delay, and it doesn't matter if we miss an advance at the + // beginning or end of a computer's lifetime. + try + { + if( isOn ) + { + // Advance our APIs. + for( ILuaAPI api : apis ) api.update(); + } + } + finally + { + isOnLock.unlock(); + } + } + } + + private IMount getRomMount() + { + return computer.getComputerEnvironment().createResourceMount( "computercraft", "lua/rom" ); + } + + private IWritableMount getRootMount() + { + if( rootMount == null ) + { + rootMount = computer.getComputerEnvironment().createSaveDirMount( + "computer/" + computer.assignID(), + computer.getComputerEnvironment().getComputerSpaceLimit() + ); + } + return rootMount; + } + + private FileSystem createFileSystem() + { + FileSystem filesystem = null; + try + { + filesystem = new FileSystem( "hdd", getRootMount() ); + + IMount romMount = getRomMount(); + if( romMount == null ) + { + displayFailure( "Cannot mount ROM", null ); + return null; + } + + filesystem.mount( "rom", "rom", romMount ); + return filesystem; + } + catch( FileSystemException e ) + { + if( filesystem != null ) filesystem.close(); + ComputerCraft.log.error( "Cannot mount computer filesystem", e ); + + displayFailure( "Cannot mount computer system", null ); + return null; + } + } + + private ILuaMachine createLuaMachine() + { + // Load the bios resource + InputStream biosStream = null; + try + { + biosStream = computer.getComputerEnvironment().createResourceFile( "computercraft", "lua/bios.lua" ); + } + catch( Exception ignored ) + { + } + + if( biosStream == null ) + { + displayFailure( "Error loading bios.lua", null ); + return null; + } + + // Create the lua machine + ILuaMachine machine = new CobaltLuaMachine( computer, timeout ); + + // Add the APIs. We unwrap them (yes, this is horrible) to get access to the underlying object. + for( ILuaAPI api : apis ) machine.addAPI( api instanceof ApiWrapper ? ((ApiWrapper) api).getDelegate() : api ); + + // Start the machine running the bios resource + MachineResult result = machine.loadBios( biosStream ); + IoUtil.closeQuietly( biosStream ); + + if( result.isError() ) + { + machine.close(); + displayFailure( "Error loading bios.lua", result.getMessage() ); + return null; + } + + return machine; + } + + private void turnOn() throws InterruptedException + { + isOnLock.lockInterruptibly(); + try + { + // Reset the terminal and event queue + computer.getTerminal().reset(); + interruptedEvent = false; + synchronized( queueLock ) + { + eventQueue.clear(); + } + + // Init filesystem + if( (fileSystem = createFileSystem()) == null ) + { + shutdown(); + return; + } + + // Init APIs + computer.getEnvironment().reset(); + for( ILuaAPI api : apis ) api.startup(); + + // Init lua + if( (machine = createLuaMachine()) == null ) + { + shutdown(); + return; + } + + // Initialisation has finished, so let's mark ourselves as on. + isOn = true; + computer.markChanged(); + } + finally + { + isOnLock.unlock(); + } + + // Now actually start the computer, now that everything is set up. + resumeMachine( null, null ); + } + + private void shutdown() throws InterruptedException + { + isOnLock.lockInterruptibly(); + try + { + isOn = false; + interruptedEvent = false; + synchronized( queueLock ) + { + eventQueue.clear(); + } + + // Shutdown Lua machine + if( machine != null ) + { + machine.close(); + machine = null; + } + + // Shutdown our APIs + for( ILuaAPI api : apis ) api.shutdown(); + computer.getEnvironment().reset(); + + // Unload filesystem + if( fileSystem != null ) + { + fileSystem.close(); + fileSystem = null; + } + + computer.getEnvironment().resetOutput(); + computer.markChanged(); + } + finally + { + isOnLock.unlock(); + } + } + + /** + * Called before calling {@link #work()}, setting up any important state. + */ + void beforeWork() + { + vRuntimeStart = System.nanoTime(); + timeout.startTimer(); + } + + /** + * Called after executing {@link #work()}. + * + * @return If we have more work to do. + */ + boolean afterWork() + { + if( interruptedEvent ) + { + timeout.pauseTimer(); + } + else + { + timeout.stopTimer(); + } + + Tracking.addTaskTiming( getComputer(), timeout.nanoCurrent() ); + + if( interruptedEvent ) return true; + + synchronized( queueLock ) + { + if( eventQueue.isEmpty() && command == null ) return onComputerQueue = false; + return true; + } + } + + /** + * The main worker function, called by {@link ComputerThread}. + * + * This either executes a {@link StateCommand} or attempts to run an event + * + * @throws InterruptedException If various locks could not be acquired. + * @see #command + * @see #eventQueue + */ + void work() throws InterruptedException + { + if( interruptedEvent ) + { + interruptedEvent = false; + if( machine != null ) + { + resumeMachine( null, null ); + return; + } + } + + StateCommand command; + Event event = null; + synchronized( queueLock ) + { + command = this.command; + this.command = null; + + // If we've no command, pull something from the event queue instead. + if( command == null ) + { + if( !isOn ) + { + // We're not on and had no command, but we had work queued. This should never happen, so clear + // the event queue just in case. + eventQueue.clear(); + return; + } + + event = eventQueue.poll(); + } + } + + if( command != null ) + { + switch( command ) + { + case TURN_ON: + if( isOn ) return; + turnOn(); + break; + + case SHUTDOWN: + + if( !isOn ) return; + computer.getTerminal().reset(); + shutdown(); + break; + + case REBOOT: + if( !isOn ) return; + computer.getTerminal().reset(); + shutdown(); + + computer.turnOn(); + break; + + case ABORT: + if( !isOn ) return; + displayFailure( "Error running computer", TimeoutState.ABORT_MESSAGE ); + shutdown(); + break; + } + } + else if( event != null ) + { + resumeMachine( event.name, event.args ); + } + } + + private void displayFailure( String message, String extra ) + { + Terminal terminal = computer.getTerminal(); + boolean colour = computer.getComputerEnvironment().isColour(); + terminal.reset(); + + // Display our primary error message + if( colour ) terminal.setTextColour( 15 - Colour.RED.ordinal() ); + terminal.write( message ); + + if( extra != null ) + { + // Display any additional information. This generally comes from the Lua Machine, such as compilation or + // runtime errors. + terminal.setCursorPos( 0, terminal.getCursorY() + 1 ); + terminal.write( extra ); + } + + // And display our generic "CC may be installed incorrectly" message. + terminal.setCursorPos( 0, terminal.getCursorY() + 1 ); + if( colour ) terminal.setTextColour( 15 - Colour.WHITE.ordinal() ); + terminal.write( "ComputerCraft may be installed incorrectly" ); + } + + private void resumeMachine( String event, Object[] args ) throws InterruptedException + { + MachineResult result = machine.handleEvent( event, args ); + interruptedEvent = result.isPause(); + if( !result.isError() ) return; + + displayFailure( "Error running computer", result.getMessage() ); + shutdown(); + } + + private enum StateCommand + { + TURN_ON, + SHUTDOWN, + REBOOT, + ABORT, + } + + private static final class Event + { + final String name; + final Object[] args; + + private Event( String name, Object[] args ) + { + this.name = name; + this.args = args; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerSide.java b/src/main/java/dan200/computercraft/core/computer/ComputerSide.java new file mode 100644 index 000000000..0f3cfa63e --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/ComputerSide.java @@ -0,0 +1,58 @@ +/* + * 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.computer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A side on a computer. Unlike {@link Direction}, this is relative to the direction the computer is + * facing.. + */ +public enum ComputerSide +{ + BOTTOM( "bottom" ), + TOP( "top" ), + BACK( "back" ), + FRONT( "front" ), + RIGHT( "right" ), + LEFT( "left" ); + + public static final String[] NAMES = new String[] { "bottom", "top", "back", "front", "right", "left" }; + + public static final int COUNT = 6; + + private static final ComputerSide[] VALUES = values(); + + private final String name; + + ComputerSide( String name ) + { + this.name = name; + } + + @Nonnull + public static ComputerSide valueOf( int side ) + { + return VALUES[side]; + } + + @Nullable + public static ComputerSide valueOfInsensitive( @Nonnull String name ) + { + for( ComputerSide side : VALUES ) + { + if( side.name.equalsIgnoreCase( name ) ) return side; + } + + return null; + } + + public String getName() + { + return name; + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerSystem.java b/src/main/java/dan200/computercraft/core/computer/ComputerSystem.java new file mode 100644 index 000000000..ee4917afe --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/ComputerSystem.java @@ -0,0 +1,75 @@ +/* + * 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.computer; + +import dan200.computercraft.api.filesystem.IFileSystem; +import dan200.computercraft.api.lua.IComputerSystem; +import dan200.computercraft.api.lua.ILuaAPIFactory; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.apis.ComputerAccess; +import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.filesystem.FileSystem; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.Map; + +/** + * Implementation of {@link IComputerAccess}/{@link IComputerSystem} for usage by externally registered APIs. + * + * @see dan200.computercraft.api.ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory) + * @see ILuaAPIFactory + * @see ApiWrapper + */ +public class ComputerSystem extends ComputerAccess implements IComputerSystem +{ + private final IAPIEnvironment environment; + + ComputerSystem( IAPIEnvironment environment ) + { + super( environment ); + this.environment = environment; + } + + @Nonnull + @Override + public String getAttachmentName() + { + return "computer"; + } + + @Nullable + @Override + public IFileSystem getFileSystem() + { + FileSystem fs = environment.getFileSystem(); + return fs == null ? null : fs.getMountWrapper(); + } + + @Nullable + @Override + public String getLabel() + { + return environment.getLabel(); + } + + @Nonnull + @Override + public Map getAvailablePeripherals() + { + // TODO: Should this return peripherals on the current computer? + return Collections.emptyMap(); + } + + @Nullable + @Override + public IPeripheral getAvailablePeripheral( @Nonnull String name ) + { + return null; + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerThread.java b/src/main/java/dan200/computercraft/core/computer/ComputerThread.java new file mode 100644 index 000000000..66ea9e644 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/ComputerThread.java @@ -0,0 +1,540 @@ +/* + * 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.computer; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.util.ThreadUtils; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.TreeSet; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.LockSupport; +import java.util.concurrent.locks.ReentrantLock; + +import static dan200.computercraft.core.computer.TimeoutState.ABORT_TIMEOUT; +import static dan200.computercraft.core.computer.TimeoutState.TIMEOUT; + +/** + * Responsible for running all tasks from a {@link Computer}. + * + * This is split into two components: the {@link TaskRunner}s, which pull an executor from the queue and execute it, and + * a single {@link Monitor} which observes all runners and kills them if they have not been terminated by + * {@link TimeoutState#isSoftAborted()}. + * + * Computers are executed using a priority system, with those who have spent less time executing having a higher + * priority than those hogging the thread. This, combined with {@link TimeoutState#isPaused()} means we can reduce the + * risk of badly behaved computers stalling execution for everyone else. + * + * This is done using an implementation of Linux's Completely Fair Scheduler. When a computer executes, we compute what + * share of execution time it has used (time executed/number of tasks). We then pick the computer who has the least + * "virtual execution time" (aka {@link ComputerExecutor#virtualRuntime}). + * + * When adding a computer to the queue, we make sure its "virtual runtime" is at least as big as the smallest runtime. + * This means that adding computers which have slept a lot do not then have massive priority over everyone else. See + * {@link #queue(ComputerExecutor)} for how this is implemented. + * + * In reality, it's unlikely that more than a few computers are waiting to execute at once, so this will not have much + * effect unless you have a computer hogging execution time. However, it is pretty effective in those situations. + * + * @see TimeoutState For how hard timeouts are handled. + * @see ComputerExecutor For how computers actually do execution. + */ +public final class ComputerThread +{ + /** + * How often the computer thread monitor should run, in milliseconds. + * + * @see Monitor + */ + private static final int MONITOR_WAKEUP = 100; + + /** + * The target latency between executing two tasks on a single machine. + * + * An average tick takes 50ms, and so we ideally need to have handled a couple of events within that window in order + * to have a perceived low latency. + */ + private static final long DEFAULT_LATENCY = TimeUnit.MILLISECONDS.toNanos( 50 ); + + /** + * The minimum value that {@link #DEFAULT_LATENCY} can have when scaled. + * + * From statistics gathered on SwitchCraft, almost all machines will execute under 15ms, 75% under 1.5ms, with the + * mean being about 3ms. Most computers shouldn't be too impacted with having such a short period to execute in. + */ + private static final long DEFAULT_MIN_PERIOD = TimeUnit.MILLISECONDS.toNanos( 5 ); + + /** + * The maximum number of tasks before we have to start scaling latency linearly. + */ + private static final long LATENCY_MAX_TASKS = DEFAULT_LATENCY / DEFAULT_MIN_PERIOD; + + /** + * Lock used for modifications to the array of current threads. + */ + private static final Object threadLock = new Object(); + + /** + * Whether the computer thread system is currently running. + */ + private static volatile boolean running = false; + + /** + * The current task manager. + */ + private static Thread monitor; + + /** + * The array of current runners, and their owning threads. + */ + private static TaskRunner[] runners; + + private static long latency; + private static long minPeriod; + + private static final ReentrantLock computerLock = new ReentrantLock(); + + private static final Condition hasWork = computerLock.newCondition(); + + /** + * Active queues to execute. + */ + private static final TreeSet computerQueue = new TreeSet<>( ( a, b ) -> { + if( a == b ) return 0; // Should never happen, but let's be consistent here + + long at = a.virtualRuntime, bt = b.virtualRuntime; + if( at == bt ) return Integer.compare( a.hashCode(), b.hashCode() ); + return at < bt ? -1 : 1; + } ); + + /** + * The minimum {@link ComputerExecutor#virtualRuntime} time on the tree. + */ + private static long minimumVirtualRuntime = 0; + + private static final ThreadFactory monitorFactory = ThreadUtils.factory( "Computer-Monitor" ); + private static final ThreadFactory runnerFactory = ThreadUtils.factory( "Computer-Runner" ); + + private ComputerThread() {} + + /** + * Start the computer thread. + */ + static void start() + { + synchronized( threadLock ) + { + running = true; + + if( runners == null ) + { + // TODO: Change the runners length on config reloads + runners = new TaskRunner[ComputerCraft.computerThreads]; + + // latency and minPeriod are scaled by 1 + floor(log2(threads)). We can afford to execute tasks for + // longer when executing on more than one thread. + long factor = 64 - Long.numberOfLeadingZeros( runners.length ); + latency = DEFAULT_LATENCY * factor; + minPeriod = DEFAULT_MIN_PERIOD * factor; + } + + for( int i = 0; i < runners.length; i++ ) + { + TaskRunner runner = runners[i]; + if( runner == null || runner.owner == null || !runner.owner.isAlive() ) + { + // Mark the old runner as dead, just in case. + if( runner != null ) runner.running = false; + // And start a new runner + runnerFactory.newThread( runners[i] = new TaskRunner() ).start(); + } + } + + if( monitor == null || !monitor.isAlive() ) (monitor = monitorFactory.newThread( new Monitor() )).start(); + } + } + + /** + * Attempt to stop the computer thread. This interrupts each runner, and clears the task queue. + */ + public static void stop() + { + synchronized( threadLock ) + { + running = false; + if( runners != null ) + { + for( TaskRunner runner : runners ) + { + if( runner == null ) continue; + + runner.running = false; + if( runner.owner != null ) runner.owner.interrupt(); + } + } + } + + computerLock.lock(); + try + { + computerQueue.clear(); + } + finally + { + computerLock.unlock(); + } + } + + /** + * Mark a computer as having work, enqueuing it on the thread. + * + * You must be holding {@link ComputerExecutor}'s {@code queueLock} when calling this method - it should only + * be called from {@code enqueue}. + * + * @param executor The computer to execute work on. + */ + static void queue( @Nonnull ComputerExecutor executor ) + { + computerLock.lock(); + try + { + if( executor.onComputerQueue ) throw new IllegalStateException( "Cannot queue already queued executor" ); + executor.onComputerQueue = true; + + updateRuntimes( null ); + + // We're not currently on the queue, so update its current execution time to + // ensure its at least as high as the minimum. + long newRuntime = minimumVirtualRuntime; + + if( executor.virtualRuntime == 0 ) + { + // Slow down new computers a little bit. + newRuntime += scaledPeriod(); + } + else + { + // Give a small boost to computers which have slept a little. + newRuntime -= latency / 2; + } + + executor.virtualRuntime = Math.max( newRuntime, executor.virtualRuntime ); + + // Add to the queue, and signal the workers. + computerQueue.add( executor ); + hasWork.signal(); + } + finally + { + computerLock.unlock(); + } + } + + + /** + * Update the {@link ComputerExecutor#virtualRuntime}s of all running tasks, and then update the + * {@link #minimumVirtualRuntime} based on the current tasks. + * + * This is called before queueing tasks, to ensure that {@link #minimumVirtualRuntime} is up-to-date. + * + * @param current The machine which we updating runtimes from. + */ + private static void updateRuntimes( @Nullable ComputerExecutor current ) + { + long minRuntime = Long.MAX_VALUE; + + // If we've a task on the queue, use that as our base time. + if( !computerQueue.isEmpty() ) minRuntime = computerQueue.first().virtualRuntime; + + // Update all the currently executing tasks + long now = System.nanoTime(); + int tasks = 1 + computerQueue.size(); + TaskRunner[] currentRunners = runners; + if( currentRunners != null ) + { + for( TaskRunner runner : currentRunners ) + { + if( runner == null ) continue; + ComputerExecutor executor = runner.currentExecutor.get(); + if( executor == null ) continue; + + // We do two things here: first we update the task's virtual runtime based on when we + // last checked, and then we check the minimum. + minRuntime = Math.min( minRuntime, executor.virtualRuntime += (now - executor.vRuntimeStart) / tasks ); + executor.vRuntimeStart = now; + } + } + + // And update the most recently executed one (if set). + if( current != null ) + { + minRuntime = Math.min( minRuntime, current.virtualRuntime += (now - current.vRuntimeStart) / tasks ); + } + + if( minRuntime > minimumVirtualRuntime && minRuntime < Long.MAX_VALUE ) + { + minimumVirtualRuntime = minRuntime; + } + } + + /** + * Ensure the "currently working" state of the executor is reset, the timings are updated, and then requeue the + * executor if needed. + * + * @param runner The runner this task was on. + * @param executor The executor to requeue + */ + private static void afterWork( TaskRunner runner, ComputerExecutor executor ) + { + // Clear the executor's thread. + Thread currentThread = executor.executingThread.getAndSet( null ); + if( currentThread != runner.owner ) + { + ComputerCraft.log.error( + "Expected computer #{} to be running on {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.", + executor.getComputer().getID(), runner.owner.getName(), currentThread == null ? "nothing" : currentThread.getName() + ); + } + + computerLock.lock(); + try + { + updateRuntimes( executor ); + + // If we've no more tasks, just return. + if( !executor.afterWork() ) return; + + // Otherwise, add to the queue, and signal any waiting workers. + computerQueue.add( executor ); + hasWork.signal(); + } + finally + { + computerLock.unlock(); + } + } + + /** + * The scaled period for a single task. + * + * @return The scaled period for the task + * @see #DEFAULT_LATENCY + * @see #DEFAULT_MIN_PERIOD + * @see #LATENCY_MAX_TASKS + */ + static long scaledPeriod() + { + // +1 to include the current task + int count = 1 + computerQueue.size(); + return count < LATENCY_MAX_TASKS ? latency / count : minPeriod; + } + + /** + * Determine if the thread has computers queued up. + * + * @return If we have work queued up. + */ + static boolean hasPendingWork() + { + return !computerQueue.isEmpty(); + } + + /** + * Observes all currently active {@link TaskRunner}s and terminates their tasks once they have exceeded the hard + * abort limit. + * + * @see TimeoutState + */ + private static final class Monitor implements Runnable + { + @Override + public void run() + { + try + { + while( true ) + { + Thread.sleep( MONITOR_WAKEUP ); + + TaskRunner[] currentRunners = ComputerThread.runners; + if( currentRunners != null ) + { + for( int i = 0; i < currentRunners.length; i++ ) + { + TaskRunner runner = currentRunners[i]; + // If we've no runner, skip. + if( runner == null || runner.owner == null || !runner.owner.isAlive() ) + { + if( !running ) continue; + + // Mark the old runner as dead and start a new one. + ComputerCraft.log.warn( "Previous runner ({}) has crashed, restarting!", + runner != null && runner.owner != null ? runner.owner.getName() : runner ); + if( runner != null ) runner.running = false; + runnerFactory.newThread( runners[i] = new TaskRunner() ).start(); + } + + // If the runner has no work, skip + ComputerExecutor executor = runner.currentExecutor.get(); + if( executor == null ) continue; + + // If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT), + // then we can let the Lua machine do its work. + long afterStart = executor.timeout.nanoCumulative(); + long afterHardAbort = afterStart - TIMEOUT - ABORT_TIMEOUT; + if( afterHardAbort < 0 ) continue; + + // Set the hard abort flag. + executor.timeout.hardAbort(); + executor.abort(); + + if( afterHardAbort >= ABORT_TIMEOUT * 2 ) + { + // If we've hard aborted and interrupted, and we're still not dead, then mark the runner + // as dead, finish off the task, and spawn a new runner. + timeoutTask( executor, runner.owner, afterStart ); + runner.running = false; + runner.owner.interrupt(); + + ComputerExecutor thisExecutor = runner.currentExecutor.getAndSet( null ); + if( thisExecutor != null ) afterWork( runner, executor ); + + synchronized( threadLock ) + { + if( running && runners.length > i && runners[i] == runner ) + { + runnerFactory.newThread( currentRunners[i] = new TaskRunner() ).start(); + } + } + } + else if( afterHardAbort >= ABORT_TIMEOUT ) + { + // If we've hard aborted but we're still not dead, dump the stack trace and interrupt + // the task. + timeoutTask( executor, runner.owner, afterStart ); + runner.owner.interrupt(); + } + } + } + } + } + catch( InterruptedException ignored ) + { + } + } + } + + /** + * Pulls tasks from the {@link #computerQueue} queue and runs them. + * + * This is responsible for running the {@link ComputerExecutor#work()}, {@link ComputerExecutor#beforeWork()} and + * {@link ComputerExecutor#afterWork()} functions. Everything else is either handled by the executor, timeout + * state or monitor. + */ + private static final class TaskRunner implements Runnable + { + Thread owner; + volatile boolean running = true; + + final AtomicReference currentExecutor = new AtomicReference<>(); + + @Override + public void run() + { + owner = Thread.currentThread(); + + tasks: + while( running && ComputerThread.running ) + { + // Wait for an active queue to execute + ComputerExecutor executor; + try + { + computerLock.lockInterruptibly(); + try + { + while( computerQueue.isEmpty() ) hasWork.await(); + executor = computerQueue.pollFirst(); + assert executor != null : "hasWork should ensure we never receive null work"; + } + finally + { + computerLock.unlock(); + } + } + catch( InterruptedException ignored ) + { + // If we've been interrupted, our running flag has probably been reset, so we'll + // just jump into the next iteration. + continue; + } + + // If we're trying to executing some task on this computer while someone else is doing work, something + // is seriously wrong. + while( !executor.executingThread.compareAndSet( null, owner ) ) + { + Thread existing = executor.executingThread.get(); + if( existing != null ) + { + ComputerCraft.log.error( + "Trying to run computer #{} on thread {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.", + executor.getComputer().getID(), owner.getName(), existing.getName() + ); + continue tasks; + } + } + + // Reset the timers + executor.beforeWork(); + + // And then set the current executor. It's important to do it afterwards, as otherwise we introduce + // race conditions with the monitor. + currentExecutor.set( executor ); + + // Execute the task + try + { + executor.work(); + } + catch( Exception | LinkageError | VirtualMachineError e ) + { + ComputerCraft.log.error( "Error running task on computer #" + executor.getComputer().getID(), e ); + } + finally + { + ComputerExecutor thisExecutor = currentExecutor.getAndSet( null ); + if( thisExecutor != null ) afterWork( this, executor ); + } + } + } + } + + private static void timeoutTask( ComputerExecutor executor, Thread thread, long time ) + { + if( !ComputerCraft.logComputerErrors ) return; + + StringBuilder builder = new StringBuilder() + .append( "Terminating computer #" ).append( executor.getComputer().getID() ) + .append( " due to timeout (running for " ).append( time * 1e-9 ) + .append( " seconds). This is NOT a bug, but may mean a computer is misbehaving. " ) + .append( thread.getName() ) + .append( " is currently " ) + .append( thread.getState() ); + Object blocking = LockSupport.getBlocker( thread ); + if( blocking != null ) builder.append( "\n on " ).append( blocking ); + + for( StackTraceElement element : thread.getStackTrace() ) + { + builder.append( "\n at " ).append( element ); + } + + ComputerCraft.log.warn( builder.toString() ); + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/Environment.java b/src/main/java/dan200/computercraft/core/computer/Environment.java new file mode 100644 index 000000000..041973fef --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/Environment.java @@ -0,0 +1,377 @@ +/* + * 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.computer; + +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IWorkMonitor; +import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.filesystem.FileSystem; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.tracking.Tracking; +import dan200.computercraft.core.tracking.TrackingField; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import javax.annotation.Nonnull; +import java.util.Arrays; +import java.util.Iterator; + +/** + * Represents the "environment" that a {@link Computer} exists in. + * + * This handles storing and updating of peripherals and redstone. + * + *

Redstone

+ * We holds three kinds of arrays for redstone, in normal and bundled versions: + *
    + *
  • {@link #internalOutput} is the redstone output which the computer has currently set. This is read on both + * threads, and written on the computer thread.
  • + *
  • {@link #externalOutput} is the redstone output currently propagated to the world. This is only read and written + * on the main thread.
  • + *
  • {@link #input} is the redstone input from external sources. This is read on both threads, and written on the main + * thread.
  • + *
+ * + *

Peripheral

+ * We also keep track of peripherals. These are read on both threads, and only written on the main thread. + */ +public final class Environment implements IAPIEnvironment +{ + private final Computer computer; + + private boolean internalOutputChanged = false; + private final int[] internalOutput = new int[ComputerSide.COUNT]; + private final int[] internalBundledOutput = new int[ComputerSide.COUNT]; + + private final int[] externalOutput = new int[ComputerSide.COUNT]; + private final int[] externalBundledOutput = new int[ComputerSide.COUNT]; + + private boolean inputChanged = false; + private final int[] input = new int[ComputerSide.COUNT]; + private final int[] bundledInput = new int[ComputerSide.COUNT]; + + private final IPeripheral[] peripherals = new IPeripheral[ComputerSide.COUNT]; + private IPeripheralChangeListener peripheralListener = null; + + private final Int2ObjectMap timers = new Int2ObjectOpenHashMap<>(); + private int nextTimerToken = 0; + + Environment( Computer computer ) + { + this.computer = computer; + } + + @Override + public int getComputerID() + { + return computer.assignID(); + } + + @Nonnull + @Override + public IComputerEnvironment getComputerEnvironment() + { + return computer.getComputerEnvironment(); + } + + @Nonnull + @Override + public IWorkMonitor getMainThreadMonitor() + { + return computer.getMainThreadMonitor(); + } + + @Nonnull + @Override + public Terminal getTerminal() + { + return computer.getTerminal(); + } + + @Override + public FileSystem getFileSystem() + { + return computer.getFileSystem(); + } + + @Override + public void shutdown() + { + computer.shutdown(); + } + + @Override + public void reboot() + { + computer.reboot(); + } + + @Override + public void queueEvent( String event, Object... args ) + { + computer.queueEvent( event, args ); + } + + @Override + public int getInput( ComputerSide side ) + { + return input[side.ordinal()]; + } + + @Override + public int getBundledInput( ComputerSide side ) + { + return bundledInput[side.ordinal()]; + } + + @Override + public void setOutput( ComputerSide side, int output ) + { + int index = side.ordinal(); + synchronized( internalOutput ) + { + if( internalOutput[index] != output ) + { + internalOutput[index] = output; + internalOutputChanged = true; + } + } + } + + @Override + public int getOutput( ComputerSide side ) + { + synchronized( internalOutput ) + { + return computer.isOn() ? internalOutput[side.ordinal()] : 0; + } + } + + @Override + public void setBundledOutput( ComputerSide side, int output ) + { + int index = side.ordinal(); + synchronized( internalOutput ) + { + if( internalBundledOutput[index] != output ) + { + internalBundledOutput[index] = output; + internalOutputChanged = true; + } + } + } + + @Override + public int getBundledOutput( ComputerSide side ) + { + synchronized( internalOutput ) + { + return computer.isOn() ? internalBundledOutput[side.ordinal()] : 0; + } + } + + public int getExternalRedstoneOutput( ComputerSide side ) + { + return computer.isOn() ? externalOutput[side.ordinal()] : 0; + } + + public int getExternalBundledRedstoneOutput( ComputerSide side ) + { + return computer.isOn() ? externalBundledOutput[side.ordinal()] : 0; + } + + public void setRedstoneInput( ComputerSide side, int level ) + { + int index = side.ordinal(); + if( input[index] != level ) + { + input[index] = level; + inputChanged = true; + } + } + + public void setBundledRedstoneInput( ComputerSide side, int combination ) + { + int index = side.ordinal(); + if( bundledInput[index] != combination ) + { + bundledInput[index] = combination; + inputChanged = true; + } + } + + /** + * Called when the computer starts up or shuts down, to reset any internal state. + * + * @see ILuaAPI#startup() + * @see ILuaAPI#shutdown() + */ + void reset() + { + synchronized( timers ) + { + timers.clear(); + } + } + + /** + * Called on the main thread to update the internal state of the computer. + */ + void tick() + { + if( inputChanged ) + { + inputChanged = false; + queueEvent( "redstone" ); + } + + synchronized( timers ) + { + // Countdown all of our active timers + Iterator> it = timers.int2ObjectEntrySet().iterator(); + while( it.hasNext() ) + { + Int2ObjectMap.Entry entry = it.next(); + Timer timer = entry.getValue(); + timer.ticksLeft--; + if( timer.ticksLeft <= 0 ) + { + // Queue the "timer" event + queueEvent( TIMER_EVENT, entry.getIntKey() ); + it.remove(); + } + } + } + } + + /** + * Called on the main thread to propagate the internal outputs to the external ones. + * + * @return If the outputs have changed. + */ + boolean updateOutput() + { + // Mark output as changed if the internal redstone has changed + synchronized( internalOutput ) + { + if( !internalOutputChanged ) return false; + + boolean changed = false; + + for( int i = 0; i < ComputerSide.COUNT; i++ ) + { + if( externalOutput[i] != internalOutput[i] ) + { + externalOutput[i] = internalOutput[i]; + changed = true; + } + + if( externalBundledOutput[i] != internalBundledOutput[i] ) + { + externalBundledOutput[i] = internalBundledOutput[i]; + changed = true; + } + } + + internalOutputChanged = false; + + return changed; + } + } + + void resetOutput() + { + // Reset redstone output + synchronized( internalOutput ) + { + Arrays.fill( internalOutput, 0 ); + Arrays.fill( internalBundledOutput, 0 ); + internalOutputChanged = true; + } + } + + @Override + public IPeripheral getPeripheral( ComputerSide side ) + { + synchronized( peripherals ) + { + return peripherals[side.ordinal()]; + } + } + + public void setPeripheral( ComputerSide side, IPeripheral peripheral ) + { + synchronized( peripherals ) + { + int index = side.ordinal(); + IPeripheral existing = peripherals[index]; + if( (existing == null && peripheral != null) || + (existing != null && peripheral == null) || + (existing != null && !existing.equals( peripheral )) ) + { + peripherals[index] = peripheral; + if( peripheralListener != null ) peripheralListener.onPeripheralChanged( side, peripheral ); + } + } + } + + @Override + public void setPeripheralChangeListener( IPeripheralChangeListener listener ) + { + synchronized( peripherals ) + { + peripheralListener = listener; + } + } + + @Override + public String getLabel() + { + return computer.getLabel(); + } + + @Override + public void setLabel( String label ) + { + computer.setLabel( label ); + } + + @Override + public int startTimer( long ticks ) + { + synchronized( timers ) + { + timers.put( nextTimerToken, new Timer( ticks ) ); + return nextTimerToken++; + } + } + + @Override + public void cancelTimer( int id ) + { + synchronized( timers ) + { + timers.remove( id ); + } + } + + @Override + public void addTrackingChange( @Nonnull TrackingField field, long change ) + { + Tracking.addValue( computer, field, change ); + } + + private static class Timer + { + long ticksLeft; + + Timer( long ticksLeft ) + { + this.ticksLeft = ticksLeft; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/IComputerEnvironment.java b/src/main/java/dan200/computercraft/core/computer/IComputerEnvironment.java new file mode 100644 index 000000000..e51054768 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/IComputerEnvironment.java @@ -0,0 +1,41 @@ +/* + * 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.computer; + +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.InputStream; + +public interface IComputerEnvironment +{ + int getDay(); + + double getTimeOfDay(); + + boolean isColour(); + + long getComputerSpaceLimit(); + + @Nonnull + String getHostString(); + + @Nonnull + String getUserAgent(); + + int assignNewID(); + + @Nullable + IWritableMount createSaveDirMount( String subPath, long capacity ); + + @Nullable + IMount createResourceMount( String domain, String subPath ); + + @Nullable + InputStream createResourceFile( String domain, String subPath ); +} diff --git a/src/main/java/dan200/computercraft/core/computer/MainThread.java b/src/main/java/dan200/computercraft/core/computer/MainThread.java new file mode 100644 index 000000000..e2874dadc --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/MainThread.java @@ -0,0 +1,194 @@ +/* + * 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.computer; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.ILuaTask; + +import javax.annotation.Nonnull; +import java.util.HashSet; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Runs tasks on the main (server) thread, ticks {@link MainThreadExecutor}s, and limits how much time is used this + * tick. + * + * Similar to {@link MainThreadExecutor}, the {@link MainThread} can be in one of three states: cool, hot and cooling. + * However, the implementation here is a little different: + * + * {@link MainThread} starts cool, and runs as many tasks as it can in the current {@link #budget}ns. Any external tasks + * (those run by tile entities, etc...) will also consume the budget + * + * Next tick, we put {@link ComputerCraft#maxMainGlobalTime} into our budget (and clamp it to that value to). If we're + * still over budget, then we should not execute any work (either as part of {@link MainThread} or externally). + */ +public final class MainThread +{ + /** + * An internal counter for {@link ILuaTask} ids. + * + * @see dan200.computercraft.api.lua.ILuaContext#issueMainThreadTask(ILuaTask) + * @see #getUniqueTaskID() + */ + private static final AtomicLong lastTaskId = new AtomicLong(); + + /** + * The queue of {@link MainThreadExecutor}s with tasks to perform. + */ + private static final TreeSet executors = new TreeSet<>( ( a, b ) -> { + if( a == b ) return 0; // Should never happen, but let's be consistent here + + long at = a.virtualTime, bt = b.virtualTime; + if( at == bt ) return Integer.compare( a.hashCode(), b.hashCode() ); + return at < bt ? -1 : 1; + } ); + + /** + * The set of executors which went over budget in a previous tick, and are waiting for their time to run down. + * + * @see MainThreadExecutor#tickCooling() + * @see #cooling(MainThreadExecutor) + */ + private static final HashSet cooling = new HashSet<>(); + + /** + * The current tick number. This is used by {@link MainThreadExecutor} to determine when to reset its own time + * counter. + * + * @see #currentTick() + */ + private static int currentTick; + + /** + * The remaining budgeted time for this tick. This may be negative, in the case that we've gone over budget. + */ + private static long budget; + + /** + * Whether we should be executing any work this tick. + * + * This is true iff {@code MAX_TICK_TIME - currentTime} was true at the beginning of the tick. + */ + private static boolean canExecute = true; + + private static long minimumTime = 0; + + private MainThread() {} + + public static long getUniqueTaskID() + { + return lastTaskId.incrementAndGet(); + } + + static void queue( @Nonnull MainThreadExecutor executor, boolean sleeper ) + { + synchronized( executors ) + { + if( executor.onQueue ) throw new IllegalStateException( "Cannot queue already queued executor" ); + executor.onQueue = true; + executor.updateTime(); + + // We're not currently on the queue, so update its current execution time to + // ensure its at least as high as the minimum. + long newRuntime = minimumTime; + + // Slow down new computers a little bit. + if( executor.virtualTime == 0 ) newRuntime += ComputerCraft.maxMainComputerTime; + + executor.virtualTime = Math.max( newRuntime, executor.virtualTime ); + + executors.add( executor ); + } + } + + static void cooling( @Nonnull MainThreadExecutor executor ) + { + cooling.add( executor ); + } + + static void consumeTime( long time ) + { + budget -= time; + } + + static boolean canExecute() + { + return canExecute; + } + + static int currentTick() + { + return currentTick; + } + + public static void executePendingTasks() + { + // Move onto the next tick and cool down the global executor. We're allowed to execute if we have _any_ time + // allocated for this tick. This means we'll stick much closer to doing MAX_TICK_TIME work every tick. + // + // Of course, we'll go over the MAX_TICK_TIME most of the time, but eventually that overrun will accumulate + // and we'll skip a whole tick - bringing the average back down again. + currentTick++; + budget = Math.min( budget + ComputerCraft.maxMainGlobalTime, ComputerCraft.maxMainGlobalTime ); + canExecute = budget > 0; + + // Cool down any warm computers. + cooling.removeIf( MainThreadExecutor::tickCooling ); + + if( !canExecute ) return; + + // Run until we meet the deadline. + long start = System.nanoTime(); + long deadline = start + budget; + while( true ) + { + MainThreadExecutor executor; + synchronized( executors ) + { + executor = executors.pollFirst(); + } + if( executor == null ) break; + + long taskStart = System.nanoTime(); + executor.execute(); + + long taskStop = System.nanoTime(); + synchronized( executors ) + { + if( executor.afterExecute( taskStop - taskStart ) ) executors.add( executor ); + + // Compute the new minimum time (including the next task on the queue too). Note that this may also include + // time spent in external tasks. + long newMinimum = executor.virtualTime; + if( !executors.isEmpty() ) + { + MainThreadExecutor next = executors.first(); + if( next.virtualTime < newMinimum ) newMinimum = next.virtualTime; + } + minimumTime = Math.max( minimumTime, newMinimum ); + } + + if( taskStop >= deadline ) break; + } + + consumeTime( System.nanoTime() - start ); + } + + public static void reset() + { + currentTick = 0; + budget = 0; + canExecute = true; + minimumTime = 0; + lastTaskId.set( 0 ); + cooling.clear(); + synchronized( executors ) + { + executors.clear(); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java b/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java new file mode 100644 index 000000000..de19920f2 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java @@ -0,0 +1,247 @@ +/* + * 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.computer; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IWorkMonitor; +import dan200.computercraft.core.tracking.Tracking; +import dan200.computercraft.shared.turtle.core.TurtleBrain; +import javax.annotation.Nonnull; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.TimeUnit; + +/** + * Keeps track of tasks that a {@link Computer} should run on the main thread and how long that has been spent executing + * them. + * + * This provides rate-limiting mechanism for tasks enqueued with {@link Computer#queueMainThread(Runnable)}, but also + * those run elsewhere (such as during the turtle's tick - see {@link TurtleBrain#update()}). In order to handle this, + * the executor goes through three stages: + * + * When {@link State#COOL}, the computer is allocated {@link ComputerCraft#maxMainComputerTime}ns to execute any work + * this tick. At the beginning of the tick, we execute as many {@link MainThread} tasks as possible, until our + * time-frame or the global time frame has expired. + * + * Then, when other objects (such as {@link TileEntity}) are ticked, we update how much time we've used using + * {@link IWorkMonitor#trackWork(long, TimeUnit)}. + * + * Now, if anywhere during this period, we use more than our allocated time slice, the executor is marked as + * {@link State#HOT}. This means it will no longer be able to execute {@link MainThread} tasks (though will still + * execute tile entity tasks, in order to prevent the main thread from exhausting work every tick). + * + * At the beginning of the next tick, we increment the budget e by {@link ComputerCraft#maxMainComputerTime} and any + * {@link State#HOT} executors are marked as {@link State#COOLING}. They will remain cooling until their budget is + * fully replenished (is equal to {@link ComputerCraft#maxMainComputerTime}). Note, this is different to + * {@link MainThread}, which allows running when it has any budget left. When cooling, no tasks are executed - + * be they on the tile entity or main thread. + * + * This mechanism means that, on average, computers will use at most {@link ComputerCraft#maxMainComputerTime}ns per + * second, but one task source will not prevent others from executing. + * + * @see MainThread + * @see IWorkMonitor + * @see Computer#getMainThreadMonitor() + * @see Computer#queueMainThread(Runnable) + */ +final class MainThreadExecutor implements IWorkMonitor +{ + /** + * The maximum number of {@link MainThread} tasks allowed on the queue. + */ + private static final int MAX_TASKS = 5000; + + private final Computer computer; + + /** + * A lock used for any changes to {@link #tasks}, or {@link #onQueue}. This will be + * used on the main thread, so locks should be kept as brief as possible. + */ + private final Object queueLock = new Object(); + + /** + * The queue of tasks which should be executed. + * + * @see #queueLock + */ + private final Queue tasks = new ArrayDeque<>( 4 ); + + /** + * Determines if this executor is currently present on the queue. + * + * This should be true iff {@link #tasks} is non-empty. + * + * @see #queueLock + * @see #enqueue(Runnable) + * @see #afterExecute(long) + */ + volatile boolean onQueue; + + /** + * The remaining budgeted time for this tick. This may be negative, in the case that we've gone over budget. + * + * @see #tickCooling() + * @see #consumeTime(long) + */ + private long budget = 0; + + /** + * The last tick that {@link #budget} was updated. + * + * @see #tickCooling() + * @see #consumeTime(long) + */ + private int currentTick = -1; + + /** + * The current state of this executor. + * + * @see #canWork() + */ + private State state = State.COOL; + + private long pendingTime; + + long virtualTime; + + MainThreadExecutor( Computer computer ) + { + this.computer = computer; + } + + /** + * Push a task onto this executor's queue, pushing it onto the {@link MainThread} if needed. + * + * @param runnable The task to run on the main thread. + * @return Whether this task was enqueued (namely, was there space). + */ + boolean enqueue( Runnable runnable ) + { + synchronized( queueLock ) + { + if( tasks.size() >= MAX_TASKS || !tasks.offer( runnable ) ) return false; + if( !onQueue && state == State.COOL ) MainThread.queue( this, true ); + return true; + } + } + + void execute() + { + if( state != State.COOL ) return; + + Runnable task; + synchronized( queueLock ) + { + task = tasks.poll(); + } + + if( task != null ) task.run(); + } + + /** + * Update the time taken to run an {@link #enqueue(Runnable)} task. + * + * @param time The time some task took to run. + * @return Whether this should be added back to the queue. + */ + boolean afterExecute( long time ) + { + consumeTime( time ); + + synchronized( queueLock ) + { + virtualTime += time; + updateTime(); + if( state != State.COOL || tasks.isEmpty() ) return onQueue = false; + return true; + } + } + + /** + * Whether we should execute "external" tasks (ones not part of {@link #tasks}). + * + * @return Whether we can execute external tasks. + */ + @Override + public boolean canWork() + { + return state != State.COOLING && MainThread.canExecute(); + } + + @Override + public boolean shouldWork() + { + return state == State.COOL && MainThread.canExecute(); + } + + @Override + public void trackWork( long time, @Nonnull TimeUnit unit ) + { + long nanoTime = unit.toNanos( time ); + synchronized( queueLock ) + { + pendingTime += nanoTime; + } + + consumeTime( nanoTime ); + MainThread.consumeTime( nanoTime ); + } + + private void consumeTime( long time ) + { + Tracking.addServerTiming( computer, time ); + + // Reset the budget if moving onto a new tick. We know this is safe, as this will only have happened if + // #tickCooling() isn't called, and so we didn't overrun the previous tick. + if( currentTick != MainThread.currentTick() ) + { + currentTick = MainThread.currentTick(); + budget = ComputerCraft.maxMainComputerTime; + } + + budget -= time; + + // If we've gone over our limit, mark us as having to cool down. + if( budget < 0 && state == State.COOL ) + { + state = State.HOT; + MainThread.cooling( this ); + } + } + + /** + * Move this executor forward one tick, replenishing the budget by {@link ComputerCraft#maxMainComputerTime}. + * + * @return Whether this executor has cooled down, and so is safe to run again. + */ + boolean tickCooling() + { + state = State.COOLING; + currentTick = MainThread.currentTick(); + budget = Math.min( budget + ComputerCraft.maxMainComputerTime, ComputerCraft.maxMainComputerTime ); + if( budget < ComputerCraft.maxMainComputerTime ) return false; + + state = State.COOL; + synchronized( queueLock ) + { + if( !tasks.isEmpty() && !onQueue ) MainThread.queue( this, false ); + } + return true; + } + + void updateTime() + { + virtualTime += pendingTime; + pendingTime = 0; + } + + private enum State + { + COOL, + HOT, + COOLING, + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/TimeoutState.java b/src/main/java/dan200/computercraft/core/computer/TimeoutState.java new file mode 100644 index 000000000..6bfe804e4 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/TimeoutState.java @@ -0,0 +1,171 @@ +/* + * 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.computer; + +import dan200.computercraft.core.lua.ILuaMachine; +import dan200.computercraft.core.lua.MachineResult; + +import java.util.concurrent.TimeUnit; + +/** + * Used to measure how long a computer has executed for, and thus the relevant timeout states. + * + * Timeouts are mostly used for execution of Lua code: we should ideally never have a state where constructing the APIs + * or machines themselves takes more than a fraction of a second. + * + * When a computer runs, it is allowed to run for 7 seconds ({@link #TIMEOUT}). After that point, the "soft abort" flag + * is set ({@link #isSoftAborted()}). Here, the Lua machine will attempt to abort the program in some safe manner + * (namely, throwing a "Too long without yielding" error). + * + * Now, if a computer still does not stop after that period, they're behaving really badly. 1.5 seconds after a soft + * abort ({@link #ABORT_TIMEOUT}), we trigger a hard abort (note, this is done from the computer thread manager). This + * will destroy the entire Lua runtime and shut the computer down. + * + * The Lua runtime is also allowed to pause execution if there are other computers contesting for work. All computers + * are allowed to run for {@link ComputerThread#scaledPeriod()} nanoseconds (see {@link #currentDeadline}). After that + * period, if any computers are waiting to be executed then we'll set the paused flag to true ({@link #isPaused()}. + * + * @see ComputerThread + * @see ILuaMachine + * @see MachineResult#isPause() + */ +public final class TimeoutState +{ + /** + * The total time a task is allowed to run before aborting in nanoseconds. + */ + static final long TIMEOUT = TimeUnit.MILLISECONDS.toNanos( 7000 ); + + /** + * The time the task is allowed to run after each abort in nanoseconds. + */ + static final long ABORT_TIMEOUT = TimeUnit.MILLISECONDS.toNanos( 1500 ); + + /** + * The error message to display when we trigger an abort. + */ + public static final String ABORT_MESSAGE = "Too long without yielding"; + + private boolean paused; + private boolean softAbort; + private volatile boolean hardAbort; + + /** + * When the cumulative time would have started had the whole event been processed in one go. + */ + private long cumulativeStart; + + /** + * How much cumulative time has elapsed. This is effectively {@code cumulativeStart - currentStart}. + */ + private long cumulativeElapsed; + + /** + * When this execution round started. + */ + private long currentStart; + + /** + * When this execution round should look potentially be paused. + */ + private long currentDeadline; + + long nanoCumulative() + { + return System.nanoTime() - cumulativeStart; + } + + long nanoCurrent() + { + return System.nanoTime() - currentStart; + } + + /** + * Recompute the {@link #isSoftAborted()} and {@link #isPaused()} flags. + */ + public void refresh() + { + // Important: The weird arithmetic here is important, as nanoTime may return negative values, and so we + // need to handle overflow. + long now = System.nanoTime(); + if( !paused ) paused = currentDeadline - now <= 0 && ComputerThread.hasPendingWork(); // now >= currentDeadline + if( !softAbort ) softAbort = now - cumulativeStart - TIMEOUT >= 0; // now - cumulativeStart >= TIMEOUT + } + + /** + * Whether we should pause execution of this machine. + * + * This is determined by whether we've consumed our time slice, and if there are other computers waiting to perform + * work. + * + * @return Whether we should pause execution. + */ + public boolean isPaused() + { + return paused; + } + + /** + * If the machine should be passively aborted. + * + * @return {@code true} if we should throw a timeout error. + */ + public boolean isSoftAborted() + { + return softAbort; + } + + /** + * Determine if the machine should be forcibly aborted. + * + * @return {@code true} if the machine should be forcibly shut down. + */ + public boolean isHardAborted() + { + return hardAbort; + } + + /** + * If the machine should be forcibly aborted. + */ + void hardAbort() + { + softAbort = hardAbort = true; + } + + /** + * Start the current and cumulative timers again. + */ + void startTimer() + { + long now = System.nanoTime(); + currentStart = now; + currentDeadline = now + ComputerThread.scaledPeriod(); + // Compute the "nominal start time". + cumulativeStart = now - cumulativeElapsed; + } + + /** + * Pauses the cumulative time, to be resumed by {@link #startTimer()}. + * + * @see #nanoCumulative() + */ + void pauseTimer() + { + // We set the cumulative time to difference between current time and "nominal start time". + cumulativeElapsed = System.nanoTime() - cumulativeStart; + paused = false; + } + + /** + * Resets the cumulative time and resets the abort flags. + */ + void stopTimer() + { + cumulativeElapsed = 0; + paused = softAbort = hardAbort = false; + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java b/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java new file mode 100644 index 000000000..94cd47b19 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java @@ -0,0 +1,49 @@ +/* + * 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.filesystem; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.channels.Channel; + +/** + * Wraps some closeable object such as a buffered writer, and the underlying stream. + * + * When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown + * this causes us to release the channel, but not actually close it. This wrapper will attempt to close the wrapper (and + * so hopefully flush the channel), and then close the underlying channel. + * + * @param The type of the closeable object to write. + */ +class ChannelWrapper implements Closeable +{ + private final T wrapper; + private final Channel channel; + + ChannelWrapper( T wrapper, Channel channel ) + { + this.wrapper = wrapper; + this.channel = channel; + } + + @Override + public void close() throws IOException + { + try + { + wrapper.close(); + } + finally + { + channel.close(); + } + } + + public T get() + { + return wrapper; + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/ComboMount.java b/src/main/java/dan200/computercraft/core/filesystem/ComboMount.java new file mode 100644 index 000000000..ffd7db5cf --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/ComboMount.java @@ -0,0 +1,145 @@ +/* + * 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.filesystem; + +import dan200.computercraft.api.filesystem.FileOperationException; +import dan200.computercraft.api.filesystem.IMount; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ComboMount implements IMount +{ + private IMount[] m_parts; + + public ComboMount( IMount[] parts ) + { + m_parts = parts; + } + + // IMount implementation + + @Override + public boolean exists( @Nonnull String path ) throws IOException + { + for( int i = m_parts.length - 1; i >= 0; --i ) + { + IMount part = m_parts[i]; + if( part.exists( path ) ) + { + return true; + } + } + return false; + } + + @Override + public boolean isDirectory( @Nonnull String path ) throws IOException + { + for( int i = m_parts.length - 1; i >= 0; --i ) + { + IMount part = m_parts[i]; + if( part.isDirectory( path ) ) + { + return true; + } + } + return false; + } + + @Override + public void list( @Nonnull String path, @Nonnull List contents ) throws IOException + { + // Combine the lists from all the mounts + List foundFiles = null; + int foundDirs = 0; + for( int i = m_parts.length - 1; i >= 0; --i ) + { + IMount part = m_parts[i]; + if( part.exists( path ) && part.isDirectory( path ) ) + { + if( foundFiles == null ) + { + foundFiles = new ArrayList<>(); + } + part.list( path, foundFiles ); + foundDirs++; + } + } + + if( foundDirs == 1 ) + { + // We found one directory, so we know it already doesn't contain duplicates + contents.addAll( foundFiles ); + } + else if( foundDirs > 1 ) + { + // We found multiple directories, so filter for duplicates + Set seen = new HashSet<>(); + for( String file : foundFiles ) + { + if( seen.add( file ) ) + { + contents.add( file ); + } + } + } + else + { + throw new FileOperationException( path, "Not a directory" ); + } + } + + @Override + public long getSize( @Nonnull String path ) throws IOException + { + for( int i = m_parts.length - 1; i >= 0; --i ) + { + IMount part = m_parts[i]; + if( part.exists( path ) ) + { + return part.getSize( path ); + } + } + throw new FileOperationException( path, "No such file" ); + } + + @Nonnull + @Override + public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException + { + for( int i = m_parts.length - 1; i >= 0; --i ) + { + IMount part = m_parts[i]; + if( part.exists( path ) && !part.isDirectory( path ) ) + { + return part.openForRead( path ); + } + } + throw new FileOperationException( path, "No such file" ); + } + + @Nonnull + @Override + public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException + { + for( int i = m_parts.length - 1; i >= 0; --i ) + { + IMount part = m_parts[i]; + if( part.exists( path ) && !part.isDirectory( path ) ) + { + return part.getAttributes( path ); + } + } + throw new FileOperationException( path, "No such file" ); + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/EmptyMount.java b/src/main/java/dan200/computercraft/core/filesystem/EmptyMount.java new file mode 100644 index 000000000..c3ba89855 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/EmptyMount.java @@ -0,0 +1,47 @@ +/* + * 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.filesystem; + +import dan200.computercraft.api.filesystem.FileOperationException; +import dan200.computercraft.api.filesystem.IMount; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.util.List; + +public class EmptyMount implements IMount +{ + @Override + public boolean exists( @Nonnull String path ) + { + return path.isEmpty(); + } + + @Override + public boolean isDirectory( @Nonnull String path ) + { + return path.isEmpty(); + } + + @Override + public void list( @Nonnull String path, @Nonnull List contents ) + { + } + + @Override + public long getSize( @Nonnull String path ) + { + return 0; + } + + @Nonnull + @Override + public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException + { + throw new FileOperationException( path, "No such file" ); + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileMount.java b/src/main/java/dan200/computercraft/core/filesystem/FileMount.java new file mode 100644 index 000000000..c1d2a0af6 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/FileMount.java @@ -0,0 +1,419 @@ +/* + * 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.filesystem; + +import com.google.common.collect.Sets; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.filesystem.FileOperationException; +import dan200.computercraft.api.filesystem.IWritableMount; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.*; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collections; +import java.util.List; +import java.util.OptionalLong; +import java.util.Set; + +public class FileMount implements IWritableMount +{ + private static final int MINIMUM_FILE_SIZE = 500; + private static final Set READ_OPTIONS = Collections.singleton( StandardOpenOption.READ ); + private static final Set WRITE_OPTIONS = Sets.newHashSet( StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING ); + private static final Set APPEND_OPTIONS = Sets.newHashSet( StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND ); + + private class WritableCountingChannel implements WritableByteChannel + { + + private final WritableByteChannel m_inner; + long m_ignoredBytesLeft; + + WritableCountingChannel( WritableByteChannel inner, long bytesToIgnore ) + { + m_inner = inner; + m_ignoredBytesLeft = bytesToIgnore; + } + + @Override + public int write( @Nonnull ByteBuffer b ) throws IOException + { + count( b.remaining() ); + return m_inner.write( b ); + } + + void count( long n ) throws IOException + { + m_ignoredBytesLeft -= n; + if( m_ignoredBytesLeft < 0 ) + { + long newBytes = -m_ignoredBytesLeft; + m_ignoredBytesLeft = 0; + + long bytesLeft = m_capacity - m_usedSpace; + if( newBytes > bytesLeft ) throw new IOException( "Out of space" ); + m_usedSpace += newBytes; + } + } + + @Override + public boolean isOpen() + { + return m_inner.isOpen(); + } + + @Override + public void close() throws IOException + { + m_inner.close(); + } + } + + private class SeekableCountingChannel extends WritableCountingChannel implements SeekableByteChannel + { + private final SeekableByteChannel m_inner; + + SeekableCountingChannel( SeekableByteChannel inner, long bytesToIgnore ) + { + super( inner, bytesToIgnore ); + m_inner = inner; + } + + @Override + public SeekableByteChannel position( long newPosition ) throws IOException + { + if( !isOpen() ) throw new ClosedChannelException(); + if( newPosition < 0 ) + { + throw new IllegalArgumentException( "Cannot seek before the beginning of the stream" ); + } + + long delta = newPosition - m_inner.position(); + if( delta < 0 ) + { + m_ignoredBytesLeft -= delta; + } + else + { + count( delta ); + } + + return m_inner.position( newPosition ); + } + + @Override + public SeekableByteChannel truncate( long size ) throws IOException + { + throw new IOException( "Not yet implemented" ); + } + + @Override + public int read( ByteBuffer dst ) throws ClosedChannelException + { + if( !m_inner.isOpen() ) throw new ClosedChannelException(); + throw new NonReadableChannelException(); + } + + @Override + public long position() throws IOException + { + return m_inner.position(); + } + + @Override + public long size() throws IOException + { + return m_inner.size(); + } + } + + private File m_rootPath; + private long m_capacity; + private long m_usedSpace; + + public FileMount( File rootPath, long capacity ) + { + m_rootPath = rootPath; + m_capacity = capacity + MINIMUM_FILE_SIZE; + m_usedSpace = created() ? measureUsedSpace( m_rootPath ) : MINIMUM_FILE_SIZE; + } + + // IMount implementation + + @Override + public boolean exists( @Nonnull String path ) + { + if( !created() ) return path.isEmpty(); + + File file = getRealPath( path ); + return file.exists(); + } + + @Override + public boolean isDirectory( @Nonnull String path ) + { + if( !created() ) return path.isEmpty(); + + File file = getRealPath( path ); + return file.exists() && file.isDirectory(); + } + + @Override + public void list( @Nonnull String path, @Nonnull List contents ) throws IOException + { + if( !created() ) + { + if( !path.isEmpty() ) throw new FileOperationException( path, "Not a directory" ); + return; + } + + File file = getRealPath( path ); + if( !file.exists() || !file.isDirectory() ) throw new FileOperationException( path, "Not a directory" ); + + String[] paths = file.list(); + for( String subPath : paths ) + { + if( new File( file, subPath ).exists() ) contents.add( subPath ); + } + } + + @Override + public long getSize( @Nonnull String path ) throws IOException + { + if( !created() ) + { + if( path.isEmpty() ) return 0; + } + else + { + File file = getRealPath( path ); + if( file.exists() ) return file.isDirectory() ? 0 : file.length(); + } + + throw new FileOperationException( path, "No such file" ); + } + + @Nonnull + @Override + public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException + { + if( created() ) + { + File file = getRealPath( path ); + if( file.exists() && !file.isDirectory() ) return FileChannel.open( file.toPath(), READ_OPTIONS ); + } + + throw new FileOperationException( path, "No such file" ); + } + + @Nonnull + @Override + public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException + { + if( created() ) + { + File file = getRealPath( path ); + if( file.exists() ) return Files.readAttributes( file.toPath(), BasicFileAttributes.class ); + } + + throw new FileOperationException( path, "No such file" ); + } + + // IWritableMount implementation + + @Override + public void makeDirectory( @Nonnull String path ) throws IOException + { + create(); + File file = getRealPath( path ); + if( file.exists() ) + { + if( !file.isDirectory() ) throw new FileOperationException( path, "File exists" ); + return; + } + + int dirsToCreate = 1; + File parent = file.getParentFile(); + while( !parent.exists() ) + { + ++dirsToCreate; + parent = parent.getParentFile(); + } + + if( getRemainingSpace() < dirsToCreate * MINIMUM_FILE_SIZE ) + { + throw new FileOperationException( path, "Out of space" ); + } + + if( file.mkdirs() ) + { + m_usedSpace += dirsToCreate * MINIMUM_FILE_SIZE; + } + else + { + throw new FileOperationException( path, "Access denied" ); + } + } + + @Override + public void delete( @Nonnull String path ) throws IOException + { + if( path.isEmpty() ) throw new FileOperationException( path, "Access denied" ); + + if( created() ) + { + File file = getRealPath( path ); + if( file.exists() ) deleteRecursively( file ); + } + } + + private void deleteRecursively( File file ) throws IOException + { + // Empty directories first + if( file.isDirectory() ) + { + String[] children = file.list(); + for( String aChildren : children ) + { + deleteRecursively( new File( file, aChildren ) ); + } + } + + // Then delete + long fileSize = file.isDirectory() ? 0 : file.length(); + boolean success = file.delete(); + if( success ) + { + m_usedSpace -= Math.max( MINIMUM_FILE_SIZE, fileSize ); + } + else + { + throw new IOException( "Access denied" ); + } + } + + @Nonnull + @Override + public WritableByteChannel openForWrite( @Nonnull String path ) throws IOException + { + create(); + File file = getRealPath( path ); + if( file.exists() && file.isDirectory() ) throw new FileOperationException( path, "Cannot write to directory" ); + + if( file.exists() ) + { + m_usedSpace -= Math.max( file.length(), MINIMUM_FILE_SIZE ); + } + else if( getRemainingSpace() < MINIMUM_FILE_SIZE ) + { + throw new FileOperationException( path, "Out of space" ); + } + m_usedSpace += MINIMUM_FILE_SIZE; + + return new SeekableCountingChannel( Files.newByteChannel( file.toPath(), WRITE_OPTIONS ), MINIMUM_FILE_SIZE ); + } + + @Nonnull + @Override + public WritableByteChannel openForAppend( @Nonnull String path ) throws IOException + { + if( !created() ) + { + throw new FileOperationException( path, "No such file" ); + } + + File file = getRealPath( path ); + if( !file.exists() ) throw new FileOperationException( path, "No such file" ); + if( file.isDirectory() ) throw new FileOperationException( path, "Cannot write to directory" ); + + // Allowing seeking when appending is not recommended, so we use a separate channel. + return new WritableCountingChannel( + Files.newByteChannel( file.toPath(), APPEND_OPTIONS ), + Math.max( MINIMUM_FILE_SIZE - file.length(), 0 ) + ); + } + + @Override + public long getRemainingSpace() + { + return Math.max( m_capacity - m_usedSpace, 0 ); + } + + @Nonnull + @Override + public OptionalLong getCapacity() + { + return OptionalLong.of( m_capacity - MINIMUM_FILE_SIZE ); + } + + private File getRealPath( String path ) + { + return new File( m_rootPath, path ); + } + + private boolean created() + { + return m_rootPath.exists(); + } + + private void create() throws IOException + { + if( !m_rootPath.exists() ) + { + boolean success = m_rootPath.mkdirs(); + if( !success ) + { + throw new IOException( "Access denied" ); + } + } + } + + private static class Visitor extends SimpleFileVisitor + { + long size; + + @Override + public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attrs ) + { + size += MINIMUM_FILE_SIZE; + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile( Path file, BasicFileAttributes attrs ) + { + size += Math.max( attrs.size(), MINIMUM_FILE_SIZE ); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed( Path file, IOException exc ) + { + ComputerCraft.log.error( "Error computing file size for {}", file, exc ); + return FileVisitResult.CONTINUE; + } + } + + private static long measureUsedSpace( File file ) + { + if( !file.exists() ) return 0; + + try + { + Visitor visitor = new Visitor(); + Files.walkFileTree( file.toPath(), visitor ); + return visitor.size; + } + catch( IOException e ) + { + ComputerCraft.log.error( "Error computing file size for {}", file, e ); + return 0; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java new file mode 100644 index 000000000..644515e1c --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java @@ -0,0 +1,606 @@ +/* + * 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.filesystem; + +import com.google.common.io.ByteStreams; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.filesystem.IFileSystem; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; +import java.io.Closeable; +import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.nio.channels.Channel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.AccessDeniedException; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class FileSystem +{ + /** + * Maximum depth that {@link #copyRecursive(String, MountWrapper, String, MountWrapper, int)} will descend into. + * + * This is a pretty arbitrary value, though hopefully it is large enough that it'll never be normally hit. This + * exists to prevent it overflowing if it ever gets into an infinite loop. + */ + private static final int MAX_COPY_DEPTH = 128; + + private final FileSystemWrapperMount m_wrapper = new FileSystemWrapperMount( this ); + private final Map mounts = new HashMap<>(); + + private final HashMap>, ChannelWrapper> m_openFiles = new HashMap<>(); + private final ReferenceQueue> m_openFileQueue = new ReferenceQueue<>(); + + public FileSystem( String rootLabel, IMount rootMount ) throws FileSystemException + { + mount( rootLabel, "", rootMount ); + } + + public FileSystem( String rootLabel, IWritableMount rootMount ) throws FileSystemException + { + mountWritable( rootLabel, "", rootMount ); + } + + public void close() + { + // Close all dangling open files + synchronized( m_openFiles ) + { + for( Closeable file : m_openFiles.values() ) IoUtil.closeQuietly( file ); + m_openFiles.clear(); + while( m_openFileQueue.poll() != null ) ; + } + } + + public synchronized void mount( String label, String location, IMount mount ) throws FileSystemException + { + if( mount == null ) throw new NullPointerException(); + location = sanitizePath( location ); + if( location.contains( ".." ) ) throw new FileSystemException( "Cannot mount below the root" ); + mount( new MountWrapper( label, location, mount ) ); + } + + public synchronized void mountWritable( String label, String location, IWritableMount mount ) throws FileSystemException + { + if( mount == null ) + { + throw new NullPointerException(); + } + location = sanitizePath( location ); + if( location.contains( ".." ) ) + { + throw new FileSystemException( "Cannot mount below the root" ); + } + mount( new MountWrapper( label, location, mount ) ); + } + + private synchronized void mount( MountWrapper wrapper ) + { + String location = wrapper.getLocation(); + mounts.remove( location ); + mounts.put( location, wrapper ); + } + + public synchronized void unmount( String path ) + { + mounts.remove( sanitizePath( path ) ); + } + + public synchronized String combine( String path, String childPath ) + { + path = sanitizePath( path, true ); + childPath = sanitizePath( childPath, true ); + + if( path.isEmpty() ) + { + return childPath; + } + else if( childPath.isEmpty() ) + { + return path; + } + else + { + return sanitizePath( path + '/' + childPath, true ); + } + } + + public static String getDirectory( String path ) + { + path = sanitizePath( path, true ); + if( path.isEmpty() ) + { + return ".."; + } + + int lastSlash = path.lastIndexOf( '/' ); + if( lastSlash >= 0 ) + { + return path.substring( 0, lastSlash ); + } + else + { + return ""; + } + } + + public static String getName( String path ) + { + path = sanitizePath( path, true ); + if( path.isEmpty() ) return "root"; + + int lastSlash = path.lastIndexOf( '/' ); + return lastSlash >= 0 ? path.substring( lastSlash + 1 ) : path; + } + + public synchronized long getSize( String path ) throws FileSystemException + { + return getMount( sanitizePath( path ) ).getSize( sanitizePath( path ) ); + } + + public synchronized BasicFileAttributes getAttributes( String path ) throws FileSystemException + { + return getMount( sanitizePath( path ) ).getAttributes( sanitizePath( path ) ); + } + + public synchronized String[] list( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + + // Gets a list of the files in the mount + List list = new ArrayList<>(); + mount.list( path, list ); + + // Add any mounts that are mounted at this location + for( MountWrapper otherMount : mounts.values() ) + { + if( getDirectory( otherMount.getLocation() ).equals( path ) ) + { + list.add( getName( otherMount.getLocation() ) ); + } + } + + // Return list + String[] array = new String[list.size()]; + list.toArray( array ); + Arrays.sort( array ); + return array; + } + + private void findIn( String dir, List matches, Pattern wildPattern ) throws FileSystemException + { + String[] list = list( dir ); + for( String entry : list ) + { + String entryPath = dir.isEmpty() ? entry : dir + "/" + entry; + if( wildPattern.matcher( entryPath ).matches() ) + { + matches.add( entryPath ); + } + if( isDir( entryPath ) ) + { + findIn( entryPath, matches, wildPattern ); + } + } + } + + public synchronized String[] find( String wildPath ) throws FileSystemException + { + // Match all the files on the system + wildPath = sanitizePath( wildPath, true ); + + // If we don't have a wildcard at all just check the file exists + int starIndex = wildPath.indexOf( '*' ); + if( starIndex == -1 ) + { + return exists( wildPath ) ? new String[] { wildPath } : new String[0]; + } + + // Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar + int prevDir = wildPath.substring( 0, starIndex ).lastIndexOf( '/' ); + String startDir = prevDir == -1 ? "" : wildPath.substring( 0, prevDir ); + + // If this isn't a directory then just abort + if( !isDir( startDir ) ) return new String[0]; + + // Scan as normal, starting from this directory + Pattern wildPattern = Pattern.compile( "^\\Q" + wildPath.replaceAll( "\\*", "\\\\E[^\\\\/]*\\\\Q" ) + "\\E$" ); + List matches = new ArrayList<>(); + findIn( startDir, matches, wildPattern ); + + // Return matches + String[] array = new String[matches.size()]; + matches.toArray( array ); + return array; + } + + public synchronized boolean exists( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.exists( path ); + } + + public synchronized boolean isDir( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.isDirectory( path ); + } + + public synchronized boolean isReadOnly( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.isReadOnly( path ); + } + + public synchronized String getMountLabel( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.getLabel(); + } + + public synchronized void makeDir( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + mount.makeDirectory( path ); + } + + public synchronized void delete( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + mount.delete( path ); + } + + public synchronized void move( String sourcePath, String destPath ) throws FileSystemException + { + sourcePath = sanitizePath( sourcePath ); + destPath = sanitizePath( destPath ); + if( isReadOnly( sourcePath ) || isReadOnly( destPath ) ) + { + throw new FileSystemException( "Access denied" ); + } + if( !exists( sourcePath ) ) + { + throw new FileSystemException( "No such file" ); + } + if( exists( destPath ) ) + { + throw new FileSystemException( "File exists" ); + } + if( contains( sourcePath, destPath ) ) + { + throw new FileSystemException( "Can't move a directory inside itself" ); + } + copy( sourcePath, destPath ); + delete( sourcePath ); + } + + public synchronized void copy( String sourcePath, String destPath ) throws FileSystemException + { + sourcePath = sanitizePath( sourcePath ); + destPath = sanitizePath( destPath ); + if( isReadOnly( destPath ) ) + { + throw new FileSystemException( "/" + destPath + ": Access denied" ); + } + if( !exists( sourcePath ) ) + { + throw new FileSystemException( "/" + sourcePath + ": No such file" ); + } + if( exists( destPath ) ) + { + throw new FileSystemException( "/" + destPath + ": File exists" ); + } + if( contains( sourcePath, destPath ) ) + { + throw new FileSystemException( "/" + sourcePath + ": Can't copy a directory inside itself" ); + } + copyRecursive( sourcePath, getMount( sourcePath ), destPath, getMount( destPath ), 0 ); + } + + private synchronized void copyRecursive( String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, int depth ) throws FileSystemException + { + if( !sourceMount.exists( sourcePath ) ) return; + if( depth >= MAX_COPY_DEPTH ) throw new FileSystemException( "Too many directories to copy" ); + + if( sourceMount.isDirectory( sourcePath ) ) + { + // Copy a directory: + // Make the new directory + destinationMount.makeDirectory( destinationPath ); + + // Copy the source contents into it + List sourceChildren = new ArrayList<>(); + sourceMount.list( sourcePath, sourceChildren ); + for( String child : sourceChildren ) + { + copyRecursive( + combine( sourcePath, child ), sourceMount, + combine( destinationPath, child ), destinationMount, + depth + 1 + ); + } + } + else + { + // Copy a file: + try( ReadableByteChannel source = sourceMount.openForRead( sourcePath ); + WritableByteChannel destination = destinationMount.openForWrite( destinationPath ) ) + { + // Copy bytes as fast as we can + ByteStreams.copy( source, destination ); + } + catch( AccessDeniedException e ) + { + throw new FileSystemException( "Access denied" ); + } + catch( IOException e ) + { + throw new FileSystemException( e.getMessage() ); + } + } + } + + private void cleanup() + { + synchronized( m_openFiles ) + { + Reference ref; + while( (ref = m_openFileQueue.poll()) != null ) + { + IoUtil.closeQuietly( m_openFiles.remove( ref ) ); + } + } + } + + private synchronized FileSystemWrapper openFile( @Nonnull Channel channel, @Nonnull T file ) throws FileSystemException + { + synchronized( m_openFiles ) + { + if( ComputerCraft.maximumFilesOpen > 0 && + m_openFiles.size() >= ComputerCraft.maximumFilesOpen ) + { + IoUtil.closeQuietly( file ); + IoUtil.closeQuietly( channel ); + throw new FileSystemException( "Too many files already open" ); + } + + ChannelWrapper channelWrapper = new ChannelWrapper<>( file, channel ); + FileSystemWrapper fsWrapper = new FileSystemWrapper<>( this, channelWrapper, m_openFileQueue ); + m_openFiles.put( fsWrapper.self, channelWrapper ); + return fsWrapper; + } + } + + synchronized void removeFile( FileSystemWrapper handle ) + { + synchronized( m_openFiles ) + { + m_openFiles.remove( handle.self ); + } + } + + public synchronized FileSystemWrapper openForRead( String path, Function open ) throws FileSystemException + { + cleanup(); + + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + ReadableByteChannel channel = mount.openForRead( path ); + if( channel != null ) + { + return openFile( channel, open.apply( channel ) ); + } + return null; + } + + public synchronized FileSystemWrapper openForWrite( String path, boolean append, Function open ) throws FileSystemException + { + cleanup(); + + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + WritableByteChannel channel = append ? mount.openForAppend( path ) : mount.openForWrite( path ); + if( channel != null ) + { + return openFile( channel, open.apply( channel ) ); + } + return null; + } + + public synchronized long getFreeSpace( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.getFreeSpace(); + } + + @Nonnull + public synchronized OptionalLong getCapacity( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.getCapacity(); + } + + private synchronized MountWrapper getMount( String path ) throws FileSystemException + { + // Return the deepest mount that contains a given path + Iterator it = mounts.values().iterator(); + MountWrapper match = null; + int matchLength = 999; + while( it.hasNext() ) + { + MountWrapper mount = it.next(); + if( contains( mount.getLocation(), path ) ) + { + int len = toLocal( path, mount.getLocation() ).length(); + if( match == null || len < matchLength ) + { + match = mount; + matchLength = len; + } + } + } + if( match == null ) + { + throw new FileSystemException( "/" + path + ": Invalid Path" ); + } + return match; + } + + public IFileSystem getMountWrapper() + { + return m_wrapper; + } + + private static String sanitizePath( String path ) + { + return sanitizePath( path, false ); + } + + private static final Pattern threeDotsPattern = Pattern.compile( "^\\.{3,}$" ); + + private static String sanitizePath( String path, boolean allowWildcards ) + { + // Allow windowsy slashes + path = path.replace( '\\', '/' ); + + // Clean the path or illegal characters. + final char[] specialChars = new char[] { + '"', ':', '<', '>', '?', '|', // Sorted by ascii value (important) + }; + + StringBuilder cleanName = new StringBuilder(); + for( int i = 0; i < path.length(); i++ ) + { + char c = path.charAt( i ); + if( c >= 32 && Arrays.binarySearch( specialChars, c ) < 0 && (allowWildcards || c != '*') ) + { + cleanName.append( c ); + } + } + path = cleanName.toString(); + + // Collapse the string into its component parts, removing ..'s + String[] parts = path.split( "/" ); + Stack outputParts = new Stack<>(); + for( String part : parts ) + { + if( part.isEmpty() || part.equals( "." ) || threeDotsPattern.matcher( part ).matches() ) + { + // . is redundant + // ... and more are treated as . + continue; + } + + if( part.equals( ".." ) ) + { + // .. can cancel out the last folder entered + if( !outputParts.empty() ) + { + String top = outputParts.peek(); + if( !top.equals( ".." ) ) + { + outputParts.pop(); + } + else + { + outputParts.push( ".." ); + } + } + else + { + outputParts.push( ".." ); + } + } + else if( part.length() >= 255 ) + { + // If part length > 255 and it is the last part + outputParts.push( part.substring( 0, 255 ) ); + } + else + { + // Anything else we add to the stack + outputParts.push( part ); + } + } + + // Recombine the output parts into a new string + StringBuilder result = new StringBuilder(); + Iterator it = outputParts.iterator(); + while( it.hasNext() ) + { + String part = it.next(); + result.append( part ); + if( it.hasNext() ) + { + result.append( '/' ); + } + } + + return result.toString(); + } + + public static boolean contains( String pathA, String pathB ) + { + pathA = sanitizePath( pathA ).toLowerCase( Locale.ROOT ); + pathB = sanitizePath( pathB ).toLowerCase( Locale.ROOT ); + + if( pathB.equals( ".." ) ) + { + return false; + } + else if( pathB.startsWith( "../" ) ) + { + return false; + } + else if( pathB.equals( pathA ) ) + { + return true; + } + else if( pathA.isEmpty() ) + { + return true; + } + else + { + return pathB.startsWith( pathA + "/" ); + } + } + + public static String toLocal( String path, String location ) + { + path = sanitizePath( path ); + location = sanitizePath( location ); + + assert contains( location, path ); + String local = path.substring( location.length() ); + if( local.startsWith( "/" ) ) + { + return local.substring( 1 ); + } + else + { + return local; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystemException.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystemException.java new file mode 100644 index 000000000..2ff07ae5d --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystemException.java @@ -0,0 +1,16 @@ +/* + * 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.filesystem; + +public class FileSystemException extends Exception +{ + private static final long serialVersionUID = -2500631644868104029L; + + FileSystemException( String s ) + { + super( s ); + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java new file mode 100644 index 000000000..28841f295 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java @@ -0,0 +1,52 @@ +/* + * 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.filesystem; + +import javax.annotation.Nonnull; +import java.io.Closeable; +import java.io.IOException; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; + +/** + * An alternative closeable implementation that will free up resources in the filesystem. + * + * The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of + * (say, the Lua object referencing it has gone), then the wrapped object will be closed by the filesystem. + * + * Closing this will stop the filesystem tracking it, reducing the current descriptor count. + * + * In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks + * on the stream, it's not really possible as it'd require numerous instances. + * + * @param The type of writer or channel to wrap. + */ +public class FileSystemWrapper implements Closeable +{ + private final FileSystem fileSystem; + private final ChannelWrapper closeable; + final WeakReference> self; + + FileSystemWrapper( FileSystem fileSystem, ChannelWrapper closeable, ReferenceQueue> queue ) + { + this.fileSystem = fileSystem; + this.closeable = closeable; + self = new WeakReference<>( this, queue ); + } + + @Override + public void close() throws IOException + { + fileSystem.removeFile( this ); + closeable.close(); + } + + @Nonnull + public T get() + { + return closeable.get(); + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapperMount.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapperMount.java new file mode 100644 index 000000000..40e4634d8 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapperMount.java @@ -0,0 +1,192 @@ +/* + * 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.filesystem; + +import dan200.computercraft.api.filesystem.IFileSystem; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +public class FileSystemWrapperMount implements IFileSystem +{ + private final FileSystem m_filesystem; + + public FileSystemWrapperMount( FileSystem filesystem ) + { + this.m_filesystem = filesystem; + } + + @Override + public void makeDirectory( @Nonnull String path ) throws IOException + { + try + { + m_filesystem.makeDir( path ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public void delete( @Nonnull String path ) throws IOException + { + try + { + m_filesystem.delete( path ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Nonnull + @Override + public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException + { + try + { + // FIXME: Think of a better way of implementing this, so closing this will close on the computer. + return m_filesystem.openForRead( path, Function.identity() ).get(); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Nonnull + @Override + public WritableByteChannel openForWrite( @Nonnull String path ) throws IOException + { + try + { + return m_filesystem.openForWrite( path, false, Function.identity() ).get(); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Nonnull + @Override + public WritableByteChannel openForAppend( @Nonnull String path ) throws IOException + { + try + { + return m_filesystem.openForWrite( path, true, Function.identity() ).get(); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public long getRemainingSpace() throws IOException + { + try + { + return m_filesystem.getFreeSpace( "/" ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public boolean exists( @Nonnull String path ) throws IOException + { + try + { + return m_filesystem.exists( path ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public boolean isDirectory( @Nonnull String path ) throws IOException + { + try + { + return m_filesystem.exists( path ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public void list( @Nonnull String path, @Nonnull List contents ) throws IOException + { + try + { + Collections.addAll( contents, m_filesystem.list( path ) ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public long getSize( @Nonnull String path ) throws IOException + { + try + { + return m_filesystem.getSize( path ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public String combine( String path, String child ) + { + return m_filesystem.combine( path, child ); + } + + @Override + public void copy( String from, String to ) throws IOException + { + try + { + m_filesystem.copy( from, to ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public void move( String from, String to ) throws IOException + { + try + { + m_filesystem.move( from, to ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/JarMount.java b/src/main/java/dan200/computercraft/core/filesystem/JarMount.java new file mode 100644 index 000000000..d169e8717 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/JarMount.java @@ -0,0 +1,345 @@ +/* + * 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.filesystem; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.io.ByteStreams; +import dan200.computercraft.api.filesystem.FileOperationException; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.core.apis.handles.ArrayByteChannel; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class JarMount implements IMount +{ + /** + * Only cache files smaller than 1MiB. + */ + private static final int MAX_CACHED_SIZE = 1 << 20; + + /** + * Limit the entire cache to 64MiB. + */ + private static final int MAX_CACHE_SIZE = 64 << 20; + + /** + * We maintain a cache of the contents of all files in the mount. This allows us to allow + * seeking within ROM files, and reduces the amount we need to access disk for computer startup. + */ + private static final Cache CONTENTS_CACHE = CacheBuilder.newBuilder() + .concurrencyLevel( 4 ) + .expireAfterAccess( 60, TimeUnit.SECONDS ) + .maximumWeight( MAX_CACHE_SIZE ) + .weakKeys() + .weigher( ( k, v ) -> v.length ) + .build(); + + /** + * We have a {@link ReferenceQueue} of all mounts, a long with their corresponding {@link ZipFile}. If + * the mount has been destroyed, we clean up after it. + */ + private static final ReferenceQueue MOUNT_QUEUE = new ReferenceQueue<>(); + + private final ZipFile zip; + private final FileEntry root; + + public JarMount( File jarFile, String subPath ) throws IOException + { + // Cleanup any old mounts. It's unlikely that there will be any, but it's best to be safe. + cleanup(); + + if( !jarFile.exists() || jarFile.isDirectory() ) throw new FileNotFoundException( "Cannot find " + jarFile ); + + // Open the zip file + try + { + zip = new ZipFile( jarFile ); + } + catch( IOException e ) + { + throw new IOException( "Error loading zip file", e ); + } + + // Ensure the root entry exists. + if( zip.getEntry( subPath ) == null ) + { + zip.close(); + throw new FileNotFoundException( "Zip does not contain path" ); + } + + // We now create a weak reference to this mount. This is automatically added to the appropriate queue. + new MountReference( this ); + + // Read in all the entries + root = new FileEntry(); + Enumeration zipEntries = zip.entries(); + while( zipEntries.hasMoreElements() ) + { + ZipEntry entry = zipEntries.nextElement(); + + String entryPath = entry.getName(); + if( !entryPath.startsWith( subPath ) ) continue; + + String localPath = FileSystem.toLocal( entryPath, subPath ); + create( entry, localPath ); + } + } + + private FileEntry get( String path ) + { + FileEntry lastEntry = root; + int lastIndex = 0; + + while( lastEntry != null && lastIndex < path.length() ) + { + int nextIndex = path.indexOf( '/', lastIndex ); + if( nextIndex < 0 ) nextIndex = path.length(); + + lastEntry = lastEntry.children == null ? null : lastEntry.children.get( path.substring( lastIndex, nextIndex ) ); + lastIndex = nextIndex + 1; + } + + return lastEntry; + } + + private void create( ZipEntry entry, String localPath ) + { + FileEntry lastEntry = root; + + int lastIndex = 0; + while( lastIndex < localPath.length() ) + { + int nextIndex = localPath.indexOf( '/', lastIndex ); + if( nextIndex < 0 ) nextIndex = localPath.length(); + + String part = localPath.substring( lastIndex, nextIndex ); + if( lastEntry.children == null ) lastEntry.children = new HashMap<>( 0 ); + + FileEntry nextEntry = lastEntry.children.get( part ); + if( nextEntry == null || !nextEntry.isDirectory() ) + { + lastEntry.children.put( part, nextEntry = new FileEntry() ); + } + + lastEntry = nextEntry; + lastIndex = nextIndex + 1; + } + + lastEntry.setup( entry ); + } + + @Override + public boolean exists( @Nonnull String path ) + { + return get( path ) != null; + } + + @Override + public boolean isDirectory( @Nonnull String path ) + { + FileEntry file = get( path ); + return file != null && file.isDirectory(); + } + + @Override + public void list( @Nonnull String path, @Nonnull List contents ) throws IOException + { + FileEntry file = get( path ); + if( file == null || !file.isDirectory() ) throw new FileOperationException( path, "Not a directory" ); + + file.list( contents ); + } + + @Override + public long getSize( @Nonnull String path ) throws IOException + { + FileEntry file = get( path ); + if( file != null ) return file.size; + throw new FileOperationException( path, "No such file" ); + } + + @Nonnull + @Override + public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException + { + FileEntry file = get( path ); + if( file != null && !file.isDirectory() ) + { + byte[] contents = CONTENTS_CACHE.getIfPresent( file ); + if( contents != null ) return new ArrayByteChannel( contents ); + + try + { + ZipEntry entry = zip.getEntry( file.path ); + if( entry != null ) + { + try( InputStream stream = zip.getInputStream( entry ) ) + { + if( stream.available() > MAX_CACHED_SIZE ) return Channels.newChannel( stream ); + + contents = ByteStreams.toByteArray( stream ); + CONTENTS_CACHE.put( file, contents ); + return new ArrayByteChannel( contents ); + } + } + } + catch( IOException e ) + { + // Treat errors as non-existence of file + } + } + + throw new FileOperationException( path, "No such file" ); + } + + @Nonnull + @Override + public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException + { + FileEntry file = get( path ); + if( file != null ) + { + ZipEntry entry = zip.getEntry( file.path ); + if( entry != null ) return new ZipEntryAttributes( entry ); + } + + throw new FileOperationException( path, "No such file" ); + } + + private static class FileEntry + { + String path; + long size; + Map children; + + void setup( ZipEntry entry ) + { + path = entry.getName(); + size = entry.getSize(); + if( children == null && entry.isDirectory() ) children = new HashMap<>( 0 ); + } + + boolean isDirectory() + { + return children != null; + } + + void list( List contents ) + { + if( children != null ) contents.addAll( children.keySet() ); + } + } + + private static class MountReference extends WeakReference + { + final ZipFile file; + + MountReference( JarMount file ) + { + super( file, MOUNT_QUEUE ); + this.file = file.zip; + } + } + + private static void cleanup() + { + Reference next; + while( (next = MOUNT_QUEUE.poll()) != null ) IoUtil.closeQuietly( ((MountReference) next).file ); + } + + private static class ZipEntryAttributes implements BasicFileAttributes + { + private final ZipEntry entry; + + ZipEntryAttributes( ZipEntry entry ) + { + this.entry = entry; + } + + @Override + public FileTime lastModifiedTime() + { + return orEpoch( entry.getLastModifiedTime() ); + } + + @Override + public FileTime lastAccessTime() + { + return orEpoch( entry.getLastAccessTime() ); + } + + @Override + public FileTime creationTime() + { + FileTime time = entry.getCreationTime(); + return time == null ? lastModifiedTime() : time; + } + + @Override + public boolean isRegularFile() + { + return !entry.isDirectory(); + } + + @Override + public boolean isDirectory() + { + return entry.isDirectory(); + } + + @Override + public boolean isSymbolicLink() + { + return false; + } + + @Override + public boolean isOther() + { + return false; + } + + @Override + public long size() + { + return entry.getSize(); + } + + @Override + public Object fileKey() + { + return null; + } + + private static final FileTime EPOCH = FileTime.from( Instant.EPOCH ); + + private static FileTime orEpoch( FileTime time ) + { + return time == null ? EPOCH : time; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java b/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java new file mode 100644 index 000000000..86a97426d --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java @@ -0,0 +1,323 @@ +/* + * 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.filesystem; + +import dan200.computercraft.api.filesystem.FileOperationException; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.AccessDeniedException; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; +import java.util.OptionalLong; + +class MountWrapper +{ + private final String label; + private final String location; + + private final IMount mount; + private final IWritableMount writableMount; + + MountWrapper( String label, String location, IMount mount ) + { + this.label = label; + this.location = location; + this.mount = mount; + writableMount = null; + } + + MountWrapper( String label, String location, IWritableMount mount ) + { + this.label = label; + this.location = location; + this.mount = mount; + writableMount = mount; + } + + public String getLabel() + { + return label; + } + + public String getLocation() + { + return location; + } + + public long getFreeSpace() + { + if( writableMount == null ) return 0; + + try + { + return writableMount.getRemainingSpace(); + } + catch( IOException e ) + { + return 0; + } + } + + public OptionalLong getCapacity() + { + return writableMount == null ? OptionalLong.empty() : writableMount.getCapacity(); + } + + public boolean isReadOnly( String path ) + { + return writableMount == null; + } + + public boolean exists( String path ) throws FileSystemException + { + path = toLocal( path ); + try + { + return mount.exists( path ); + } + catch( IOException e ) + { + throw localExceptionOf( path, e ); + } + } + + public boolean isDirectory( String path ) throws FileSystemException + { + path = toLocal( path ); + try + { + return mount.exists( path ) && mount.isDirectory( path ); + } + catch( IOException e ) + { + throw localExceptionOf( path, e ); + } + } + + public void list( String path, List contents ) throws FileSystemException + { + path = toLocal( path ); + try + { + if( !mount.exists( path ) || !mount.isDirectory( path ) ) + { + throw localExceptionOf( path, "Not a directory" ); + } + + mount.list( path, contents ); + } + catch( IOException e ) + { + throw localExceptionOf( path, e ); + } + } + + public long getSize( String path ) throws FileSystemException + { + path = toLocal( path ); + try + { + if( !mount.exists( path ) ) throw localExceptionOf( path, "No such file" ); + return mount.isDirectory( path ) ? 0 : mount.getSize( path ); + } + catch( IOException e ) + { + throw localExceptionOf( path, e ); + } + } + + @Nonnull + public BasicFileAttributes getAttributes( String path ) throws FileSystemException + { + path = toLocal( path ); + try + { + if( !mount.exists( path ) ) throw localExceptionOf( path, "No such file" ); + return mount.getAttributes( path ); + } + catch( IOException e ) + { + throw localExceptionOf( path, e ); + } + } + + public ReadableByteChannel openForRead( String path ) throws FileSystemException + { + path = toLocal( path ); + try + { + if( mount.exists( path ) && !mount.isDirectory( path ) ) + { + return mount.openForRead( path ); + } + else + { + throw localExceptionOf( path, "No such file" ); + } + } + catch( IOException e ) + { + throw localExceptionOf( path, e ); + } + } + + public void makeDirectory( String path ) throws FileSystemException + { + if( writableMount == null ) throw exceptionOf( path, "Access denied" ); + + path = toLocal( path ); + try + { + if( mount.exists( path ) ) + { + if( !mount.isDirectory( path ) ) throw localExceptionOf( path, "File exists" ); + } + else + { + writableMount.makeDirectory( path ); + } + } + catch( IOException e ) + { + throw localExceptionOf( path, e ); + } + } + + public void delete( String path ) throws FileSystemException + { + if( writableMount == null ) throw exceptionOf( path, "Access denied" ); + + path = toLocal( path ); + try + { + if( mount.exists( path ) ) + { + writableMount.delete( path ); + } + } + catch( AccessDeniedException e ) + { + throw new FileSystemException( "Access denied" ); + } + catch( IOException e ) + { + throw localExceptionOf( path, e ); + } + } + + public WritableByteChannel openForWrite( String path ) throws FileSystemException + { + if( writableMount == null ) throw exceptionOf( path, "Access denied" ); + + path = toLocal( path ); + try + { + if( mount.exists( path ) && mount.isDirectory( path ) ) + { + throw localExceptionOf( path, "Cannot write to directory" ); + } + else + { + if( !path.isEmpty() ) + { + String dir = FileSystem.getDirectory( path ); + if( !dir.isEmpty() && !mount.exists( path ) ) + { + writableMount.makeDirectory( dir ); + } + } + return writableMount.openForWrite( path ); + } + } + catch( AccessDeniedException e ) + { + throw new FileSystemException( "Access denied" ); + } + catch( IOException e ) + { + throw localExceptionOf( path, e ); + } + } + + public WritableByteChannel openForAppend( String path ) throws FileSystemException + { + if( writableMount == null ) throw exceptionOf( path, "Access denied" ); + + path = toLocal( path ); + try + { + if( !mount.exists( path ) ) + { + if( !path.isEmpty() ) + { + String dir = FileSystem.getDirectory( path ); + if( !dir.isEmpty() && !mount.exists( path ) ) + { + writableMount.makeDirectory( dir ); + } + } + return writableMount.openForWrite( path ); + } + else if( mount.isDirectory( path ) ) + { + throw localExceptionOf( path, "Cannot write to directory" ); + } + else + { + return writableMount.openForAppend( path ); + } + } + catch( AccessDeniedException e ) + { + throw new FileSystemException( "Access denied" ); + } + catch( IOException e ) + { + throw localExceptionOf( path, e ); + } + } + + private String toLocal( String path ) + { + return FileSystem.toLocal( path, location ); + } + + private FileSystemException localExceptionOf( @Nullable String localPath, @Nonnull IOException e ) + { + if( !location.isEmpty() && e instanceof FileOperationException ) + { + FileOperationException ex = (FileOperationException) e; + if( ex.getFilename() != null ) return localExceptionOf( ex.getFilename(), ex.getMessage() ); + } + + if( e instanceof java.nio.file.FileSystemException ) + { + // This error will contain the absolute path, leaking information about where MC is installed. We drop that, + // just taking the reason. We assume that the error refers to the input path. + String message = ((java.nio.file.FileSystemException) e).getReason().trim(); + return localPath == null ? new FileSystemException( message ) : localExceptionOf( localPath, message ); + } + + return new FileSystemException( e.getMessage() ); + } + + private FileSystemException localExceptionOf( String path, String message ) + { + if( !location.isEmpty() ) path = path.isEmpty() ? location : location + "/" + path; + return exceptionOf( path, message ); + } + + private static FileSystemException exceptionOf( String path, String message ) + { + return new FileSystemException( "/" + path + ": " + message ); + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java new file mode 100644 index 000000000..18c012f98 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java @@ -0,0 +1,305 @@ +/* + * 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.filesystem; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.MapMaker; +import com.google.common.io.ByteStreams; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.core.apis.handles.ArrayByteChannel; +import dan200.computercraft.core.filesystem.ResourceMount.Listener; +import net.minecraft.resource.ReloadableResourceManager; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.util.Identifier; +import net.minecraft.util.InvalidIdentifierException; +import net.minecraftforge.resource.IResourceType; +import net.minecraftforge.resource.ISelectiveResourceReloadListener; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +public final class ResourceMount implements IMount +{ + /** + * Only cache files smaller than 1MiB. + */ + private static final int MAX_CACHED_SIZE = 1 << 20; + + /** + * Limit the entire cache to 64MiB. + */ + private static final int MAX_CACHE_SIZE = 64 << 20; + + private static final byte[] TEMP_BUFFER = new byte[8192]; + + /** + * We maintain a cache of the contents of all files in the mount. This allows us to allow + * seeking within ROM files, and reduces the amount we need to access disk for computer startup. + */ + private static final Cache CONTENTS_CACHE = CacheBuilder.newBuilder() + .concurrencyLevel( 4 ) + .expireAfterAccess( 60, TimeUnit.SECONDS ) + .maximumWeight( MAX_CACHE_SIZE ) + .weakKeys() + .weigher( ( k, v ) -> v.length ) + .build(); + + private static final MapMaker CACHE_TEMPLATE = new MapMaker().weakValues().concurrencyLevel( 1 ); + + /** + * Maintain a cache of currently loaded resource mounts. This cache is invalidated when currentManager changes. + */ + private static final Map> MOUNT_CACHE = new WeakHashMap<>( 2 ); + + private final String namespace; + private final String subPath; + private final ReloadableResourceManager manager; + + @Nullable + private FileEntry root; + + public static ResourceMount get( String namespace, String subPath, ReloadableResourceManager manager ) + { + Map cache; + + synchronized( MOUNT_CACHE ) + { + cache = MOUNT_CACHE.get( manager ); + if( cache == null ) MOUNT_CACHE.put( manager, cache = CACHE_TEMPLATE.makeMap() ); + } + + Identifier path = new Identifier( namespace, subPath ); + synchronized( cache ) + { + ResourceMount mount = cache.get( path ); + if( mount == null ) cache.put( path, mount = new ResourceMount( namespace, subPath, manager ) ); + return mount; + } + } + + private ResourceMount( String namespace, String subPath, ReloadableResourceManager manager ) + { + this.namespace = namespace; + this.subPath = subPath; + this.manager = manager; + + Listener.INSTANCE.add( manager, this ); + if( root == null ) load(); + } + + private void load() + { + boolean hasAny = false; + FileEntry newRoot = new FileEntry( new Identifier( namespace, subPath ) ); + for( Identifier file : manager.findResources( subPath, s -> true ) ) + { + if( !file.getNamespace().equals( namespace ) ) continue; + + String localPath = FileSystem.toLocal( file.getPath(), subPath ); + create( newRoot, localPath ); + hasAny = true; + } + + root = hasAny ? newRoot : null; + } + + private FileEntry get( String path ) + { + FileEntry lastEntry = root; + int lastIndex = 0; + + while( lastEntry != null && lastIndex < path.length() ) + { + int nextIndex = path.indexOf( '/', lastIndex ); + if( nextIndex < 0 ) nextIndex = path.length(); + + lastEntry = lastEntry.children == null ? null : lastEntry.children.get( path.substring( lastIndex, nextIndex ) ); + lastIndex = nextIndex + 1; + } + + return lastEntry; + } + + private void create( FileEntry lastEntry, String path ) + { + int lastIndex = 0; + while( lastIndex < path.length() ) + { + int nextIndex = path.indexOf( '/', lastIndex ); + if( nextIndex < 0 ) nextIndex = path.length(); + + String part = path.substring( lastIndex, nextIndex ); + if( lastEntry.children == null ) lastEntry.children = new HashMap<>(); + + FileEntry nextEntry = lastEntry.children.get( part ); + if( nextEntry == null ) + { + Identifier childPath; + try + { + childPath = new Identifier( namespace, subPath + "/" + path ); + } + catch( InvalidIdentifierException e ) + { + ComputerCraft.log.warn( "Cannot create resource location for {} ({})", part, e.getMessage() ); + return; + } + lastEntry.children.put( part, nextEntry = new FileEntry( childPath ) ); + } + + lastEntry = nextEntry; + lastIndex = nextIndex + 1; + } + } + + @Override + public boolean exists( @Nonnull String path ) + { + return get( path ) != null; + } + + @Override + public boolean isDirectory( @Nonnull String path ) + { + FileEntry file = get( path ); + return file != null && file.isDirectory(); + } + + @Override + public void list( @Nonnull String path, @Nonnull List contents ) throws IOException + { + FileEntry file = get( path ); + if( file == null || !file.isDirectory() ) throw new IOException( "/" + path + ": Not a directory" ); + + file.list( contents ); + } + + @Override + public long getSize( @Nonnull String path ) throws IOException + { + FileEntry file = get( path ); + if( file != null ) + { + if( file.size != -1 ) return file.size; + if( file.isDirectory() ) return file.size = 0; + + byte[] contents = CONTENTS_CACHE.getIfPresent( file ); + if( contents != null ) return file.size = contents.length; + + try + { + Resource resource = manager.getResource( file.identifier ); + InputStream s = resource.getInputStream(); + int total = 0, read = 0; + do + { + total += read; + read = s.read( TEMP_BUFFER ); + } while( read > 0 ); + + return file.size = total; + } + catch( IOException e ) + { + return file.size = 0; + } + } + + throw new IOException( "/" + path + ": No such file" ); + } + + @Nonnull + @Override + public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException + { + FileEntry file = get( path ); + if( file != null && !file.isDirectory() ) + { + byte[] contents = CONTENTS_CACHE.getIfPresent( file ); + if( contents != null ) return new ArrayByteChannel( contents ); + + try( InputStream stream = manager.getResource( file.identifier ).getInputStream() ) + { + if( stream.available() > MAX_CACHED_SIZE ) return Channels.newChannel( stream ); + + contents = ByteStreams.toByteArray( stream ); + CONTENTS_CACHE.put( file, contents ); + return new ArrayByteChannel( contents ); + } + catch( FileNotFoundException ignored ) + { + } + } + + throw new IOException( "/" + path + ": No such file" ); + } + + private static class FileEntry + { + final Identifier identifier; + Map children; + long size = -1; + + FileEntry( Identifier identifier ) + { + this.identifier = identifier; + } + + boolean isDirectory() + { + return children != null; + } + + void list( List contents ) + { + if( children != null ) contents.addAll( children.keySet() ); + } + } + + /** + * A {@link ISelectiveResourceReloadListener} which reloads any associated mounts. + * + * While people should really be keeping a permanent reference to this, some people construct it every + * method call, so let's make this as small as possible. + */ + static class Listener implements ISelectiveResourceReloadListener + { + private static final Listener INSTANCE = new Listener(); + + private final Set mounts = Collections.newSetFromMap( new WeakHashMap<>() ); + private final Set managers = Collections.newSetFromMap( new WeakHashMap<>() ); + + @Override + public void apply( @Nonnull ResourceManager manager ) + { + // FIXME: Remove this. We need this patch in order to prevent trying to load ReloadRequirements. + onResourceManagerReload( manager, x -> true ); + } + + @Override + public synchronized void onResourceManagerReload( @Nonnull ResourceManager manager, @Nonnull Predicate predicate ) + { + for( ResourceMount mount : mounts ) mount.load(); + } + + synchronized void add( ReloadableResourceManager manager, ResourceMount mount ) + { + if( managers.add( manager ) ) manager.registerListener( this ); + mounts.add( mount ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/SubMount.java b/src/main/java/dan200/computercraft/core/filesystem/SubMount.java new file mode 100644 index 000000000..68d2999ee --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/SubMount.java @@ -0,0 +1,69 @@ +/* + * 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.filesystem; + +import dan200.computercraft.api.filesystem.IMount; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; + +public class SubMount implements IMount +{ + private IMount parent; + private String subPath; + + public SubMount( IMount parent, String subPath ) + { + this.parent = parent; + this.subPath = subPath; + } + + @Override + public boolean exists( @Nonnull String path ) throws IOException + { + return parent.exists( getFullPath( path ) ); + } + + @Override + public boolean isDirectory( @Nonnull String path ) throws IOException + { + return parent.isDirectory( getFullPath( path ) ); + } + + @Override + public void list( @Nonnull String path, @Nonnull List contents ) throws IOException + { + parent.list( getFullPath( path ), contents ); + } + + @Override + public long getSize( @Nonnull String path ) throws IOException + { + return parent.getSize( getFullPath( path ) ); + } + + @Nonnull + @Override + public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException + { + return parent.openForRead( getFullPath( path ) ); + } + + @Nonnull + @Override + public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException + { + return parent.getAttributes( getFullPath( path ) ); + } + + private String getFullPath( String path ) + { + return path.isEmpty() ? subPath : subPath + "/" + path; + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/BasicFunction.java b/src/main/java/dan200/computercraft/core/lua/BasicFunction.java new file mode 100644 index 000000000..6f4b25652 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/BasicFunction.java @@ -0,0 +1,75 @@ +/* + * 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.lua; + +import dan200.computercraft.ComputerCraft; +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.core.asm.LuaMethod; +import org.squiddev.cobalt.LuaError; +import org.squiddev.cobalt.LuaState; +import org.squiddev.cobalt.Varargs; +import org.squiddev.cobalt.function.VarArgFunction; + +/** + * An "optimised" version of {@link ResultInterpreterFunction} which is guaranteed to never yield. + * + * As we never yield, we do not need to push a function to the stack, which removes a small amount of overhead. + */ +class BasicFunction extends VarArgFunction +{ + private final CobaltLuaMachine machine; + private final LuaMethod method; + private final Object instance; + private final ILuaContext context; + private final String name; + + BasicFunction( CobaltLuaMachine machine, LuaMethod method, Object instance, ILuaContext context, String name ) + { + this.machine = machine; + this.method = method; + this.instance = instance; + this.context = context; + this.name = name; + } + + @Override + public Varargs invoke( LuaState luaState, Varargs args ) throws LuaError + { + IArguments arguments = CobaltLuaMachine.toArguments( args ); + MethodResult results; + try + { + results = method.apply( instance, context, arguments ); + } + catch( LuaException e ) + { + throw wrap( e ); + } + catch( Throwable t ) + { + if( ComputerCraft.logComputerErrors ) + { + ComputerCraft.log.error( "Error calling " + name + " on " + instance, t ); + } + throw new LuaError( "Java Exception Thrown: " + t, 0 ); + } + + if( results.getCallback() != null ) + { + throw new IllegalStateException( "Cannot have a yielding non-yielding function" ); + } + return machine.toValues( results.getResult() ); + } + + public static LuaError wrap( LuaException exception ) + { + return exception.hasLevel() ? new LuaError( exception.getMessage() ) : new LuaError( exception.getMessage(), exception.getLevel() ); + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java new file mode 100644 index 000000000..e678667e1 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java @@ -0,0 +1,570 @@ +/* + * 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.lua; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.*; +import dan200.computercraft.core.asm.LuaMethod; +import dan200.computercraft.core.asm.ObjectSource; +import dan200.computercraft.core.computer.Computer; +import dan200.computercraft.core.computer.MainThread; +import dan200.computercraft.core.computer.TimeoutState; +import dan200.computercraft.core.tracking.Tracking; +import dan200.computercraft.core.tracking.TrackingField; +import dan200.computercraft.shared.util.ThreadUtils; +import org.squiddev.cobalt.*; +import org.squiddev.cobalt.compiler.CompileException; +import org.squiddev.cobalt.compiler.LoadState; +import org.squiddev.cobalt.debug.DebugFrame; +import org.squiddev.cobalt.debug.DebugHandler; +import org.squiddev.cobalt.debug.DebugState; +import org.squiddev.cobalt.function.LuaFunction; +import org.squiddev.cobalt.lib.*; +import org.squiddev.cobalt.lib.platform.VoidResourceManipulator; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static org.squiddev.cobalt.ValueFactory.valueOf; +import static org.squiddev.cobalt.ValueFactory.varargsOf; +import static org.squiddev.cobalt.debug.DebugFrame.FLAG_HOOKED; +import static org.squiddev.cobalt.debug.DebugFrame.FLAG_HOOKYIELD; + +public class CobaltLuaMachine implements ILuaMachine +{ + private static final ThreadPoolExecutor COROUTINES = new ThreadPoolExecutor( + 0, Integer.MAX_VALUE, + 5L, TimeUnit.MINUTES, + new SynchronousQueue<>(), + ThreadUtils.factory( "Coroutine" ) + ); + + private static final LuaMethod FUNCTION_METHOD = ( target, context, args ) -> ((ILuaFunction) target).call( args ); + + private final Computer m_computer; + private final TimeoutState timeout; + private final TimeoutDebugHandler debug; + private final ILuaContext context = new CobaltLuaContext(); + + private LuaState m_state; + private LuaTable m_globals; + + private LuaThread m_mainRoutine = null; + private String m_eventFilter = null; + + public CobaltLuaMachine( Computer computer, TimeoutState timeout ) + { + m_computer = computer; + this.timeout = timeout; + debug = new TimeoutDebugHandler(); + + // Create an environment to run in + LuaState state = m_state = LuaState.builder() + .resourceManipulator( new VoidResourceManipulator() ) + .debug( debug ) + .coroutineExecutor( command -> { + Tracking.addValue( m_computer, TrackingField.COROUTINES_CREATED, 1 ); + COROUTINES.execute( () -> { + try + { + command.run(); + } + finally + { + Tracking.addValue( m_computer, TrackingField.COROUTINES_DISPOSED, 1 ); + } + } ); + } ) + .build(); + + m_globals = new LuaTable(); + state.setupThread( m_globals ); + + // Add basic libraries + m_globals.load( state, new BaseLib() ); + m_globals.load( state, new TableLib() ); + m_globals.load( state, new StringLib() ); + m_globals.load( state, new MathLib() ); + m_globals.load( state, new CoroutineLib() ); + m_globals.load( state, new Bit32Lib() ); + m_globals.load( state, new Utf8Lib() ); + if( ComputerCraft.debugEnable ) m_globals.load( state, new DebugLib() ); + + // Remove globals we don't want to expose + m_globals.rawset( "collectgarbage", Constants.NIL ); + m_globals.rawset( "dofile", Constants.NIL ); + m_globals.rawset( "loadfile", Constants.NIL ); + m_globals.rawset( "print", Constants.NIL ); + + // Add version globals + m_globals.rawset( "_VERSION", valueOf( "Lua 5.1" ) ); + m_globals.rawset( "_HOST", valueOf( computer.getAPIEnvironment().getComputerEnvironment().getHostString() ) ); + m_globals.rawset( "_CC_DEFAULT_SETTINGS", valueOf( ComputerCraft.defaultComputerSettings ) ); + if( ComputerCraft.disableLua51Features ) + { + m_globals.rawset( "_CC_DISABLE_LUA51_FEATURES", Constants.TRUE ); + } + } + + @Override + public void addAPI( @Nonnull ILuaAPI api ) + { + // Add the methods of an API to the global table + LuaTable table = wrapLuaObject( api ); + if( table == null ) + { + ComputerCraft.log.warn( "API {} does not provide any methods", api ); + table = new LuaTable(); + } + + String[] names = api.getNames(); + for( String name : names ) m_globals.rawset( name, table ); + } + + @Override + public MachineResult loadBios( @Nonnull InputStream bios ) + { + // Begin executing a file (ie, the bios) + if( m_mainRoutine != null ) return MachineResult.OK; + + try + { + LuaFunction value = LoadState.load( m_state, bios, "@bios.lua", m_globals ); + m_mainRoutine = new LuaThread( m_state, value, m_globals ); + return MachineResult.OK; + } + catch( CompileException e ) + { + close(); + return MachineResult.error( e ); + } + catch( Exception e ) + { + ComputerCraft.log.warn( "Could not load bios.lua", e ); + close(); + return MachineResult.GENERIC_ERROR; + } + } + + @Override + public MachineResult handleEvent( String eventName, Object[] arguments ) + { + if( m_mainRoutine == null ) return MachineResult.OK; + + if( m_eventFilter != null && eventName != null && !eventName.equals( m_eventFilter ) && !eventName.equals( "terminate" ) ) + { + return MachineResult.OK; + } + + // If the soft abort has been cleared then we can reset our flag. + timeout.refresh(); + if( !timeout.isSoftAborted() ) debug.thrownSoftAbort = false; + + try + { + Varargs resumeArgs = Constants.NONE; + if( eventName != null ) + { + resumeArgs = varargsOf( valueOf( eventName ), toValues( arguments ) ); + } + + // Resume the current thread, or the main one when first starting off. + LuaThread thread = m_state.getCurrentThread(); + if( thread == null || thread == m_state.getMainThread() ) thread = m_mainRoutine; + + Varargs results = LuaThread.run( thread, resumeArgs ); + if( timeout.isHardAborted() ) throw HardAbortError.INSTANCE; + if( results == null ) return MachineResult.PAUSE; + + LuaValue filter = results.first(); + m_eventFilter = filter.isString() ? filter.toString() : null; + + if( m_mainRoutine.getStatus().equals( "dead" ) ) + { + close(); + return MachineResult.GENERIC_ERROR; + } + else + { + return MachineResult.OK; + } + } + catch( HardAbortError | InterruptedException e ) + { + close(); + return MachineResult.TIMEOUT; + } + catch( LuaError e ) + { + close(); + ComputerCraft.log.warn( "Top level coroutine errored", e ); + return MachineResult.error( e ); + } + } + + @Override + public void close() + { + LuaState state = m_state; + if( state == null ) return; + + state.abandon(); + m_mainRoutine = null; + m_state = null; + m_globals = null; + } + + @Nullable + private LuaTable wrapLuaObject( Object object ) + { + String[] dynamicMethods = object instanceof IDynamicLuaObject + ? Objects.requireNonNull( ((IDynamicLuaObject) object).getMethodNames(), "Methods cannot be null" ) + : LuaMethod.EMPTY_METHODS; + + LuaTable table = new LuaTable(); + for( int i = 0; i < dynamicMethods.length; i++ ) + { + String method = dynamicMethods[i]; + table.rawset( method, new ResultInterpreterFunction( this, LuaMethod.DYNAMIC.get( i ), object, context, method ) ); + } + + ObjectSource.allMethods( LuaMethod.GENERATOR, object, ( instance, method ) -> + table.rawset( method.getName(), method.nonYielding() + ? new BasicFunction( this, method.getMethod(), instance, context, method.getName() ) + : new ResultInterpreterFunction( this, method.getMethod(), instance, context, method.getName() ) ) ); + + try + { + if( table.keyCount() == 0 ) return null; + } + catch( LuaError ignored ) + { + } + + return table; + } + + @Nonnull + private LuaValue toValue( @Nullable Object object, @Nullable Map values ) + { + if( object == null ) return Constants.NIL; + if( object instanceof Number ) return valueOf( ((Number) object).doubleValue() ); + if( object instanceof Boolean ) return valueOf( (Boolean) object ); + if( object instanceof String ) return valueOf( object.toString() ); + if( object instanceof byte[] ) + { + byte[] b = (byte[]) object; + return valueOf( Arrays.copyOf( b, b.length ) ); + } + if( object instanceof ByteBuffer ) + { + ByteBuffer b = (ByteBuffer) object; + byte[] bytes = new byte[b.remaining()]; + b.get( bytes ); + return valueOf( bytes ); + } + + if( values == null ) values = new IdentityHashMap<>( 1 ); + LuaValue result = values.get( object ); + if( result != null ) return result; + + if( object instanceof ILuaFunction ) + { + return new ResultInterpreterFunction( this, FUNCTION_METHOD, object, context, object.toString() ); + } + + if( object instanceof IDynamicLuaObject ) + { + LuaValue wrapped = wrapLuaObject( object ); + if( wrapped == null ) wrapped = new LuaTable(); + values.put( object, wrapped ); + return wrapped; + } + + if( object instanceof Map ) + { + LuaTable table = new LuaTable(); + values.put( object, table ); + + for( Map.Entry pair : ((Map) object).entrySet() ) + { + LuaValue key = toValue( pair.getKey(), values ); + LuaValue value = toValue( pair.getValue(), values ); + if( !key.isNil() && !value.isNil() ) table.rawset( key, value ); + } + return table; + } + + if( object instanceof Collection ) + { + Collection objects = (Collection) object; + LuaTable table = new LuaTable( objects.size(), 0 ); + values.put( object, table ); + int i = 0; + for( Object child : objects ) table.rawset( ++i, toValue( child, values ) ); + return table; + } + + if( object instanceof Object[] ) + { + Object[] objects = (Object[]) object; + LuaTable table = new LuaTable( objects.length, 0 ); + values.put( object, table ); + for( int i = 0; i < objects.length; i++ ) table.rawset( i + 1, toValue( objects[i], values ) ); + return table; + } + + LuaTable wrapped = wrapLuaObject( object ); + if( wrapped != null ) + { + values.put( object, wrapped ); + return wrapped; + } + + if( ComputerCraft.logComputerErrors ) + { + ComputerCraft.log.warn( "Received unknown type '{}', returning nil.", object.getClass().getName() ); + } + return Constants.NIL; + } + + Varargs toValues( Object[] objects ) + { + if( objects == null || objects.length == 0 ) return Constants.NONE; + if( objects.length == 1 ) return toValue( objects[0], null ); + + Map result = new IdentityHashMap<>( 0 ); + LuaValue[] values = new LuaValue[objects.length]; + for( int i = 0; i < values.length; i++ ) + { + Object object = objects[i]; + values[i] = toValue( object, result ); + } + return varargsOf( values ); + } + + static Object toObject( LuaValue value, Map objects ) + { + switch( value.type() ) + { + case Constants.TNIL: + case Constants.TNONE: + return null; + case Constants.TINT: + case Constants.TNUMBER: + return value.toDouble(); + case Constants.TBOOLEAN: + return value.toBoolean(); + case Constants.TSTRING: + return value.toString(); + case Constants.TTABLE: + { + // Table: + // Start remembering stuff + if( objects == null ) + { + objects = new IdentityHashMap<>( 1 ); + } + else + { + Object existing = objects.get( value ); + if( existing != null ) return existing; + } + Map table = new HashMap<>(); + objects.put( value, table ); + + LuaTable luaTable = (LuaTable) value; + + // Convert all keys + LuaValue k = Constants.NIL; + while( true ) + { + Varargs keyValue; + try + { + keyValue = luaTable.next( k ); + } + catch( LuaError luaError ) + { + break; + } + k = keyValue.first(); + if( k.isNil() ) break; + + LuaValue v = keyValue.arg( 2 ); + Object keyObject = toObject( k, objects ); + Object valueObject = toObject( v, objects ); + if( keyObject != null && valueObject != null ) + { + table.put( keyObject, valueObject ); + } + } + return table; + } + default: + return null; + } + } + + static Object[] toObjects( Varargs values ) + { + int count = values.count(); + Object[] objects = new Object[count]; + for( int i = 0; i < count; i++ ) objects[i] = toObject( values.arg( i + 1 ), null ); + return objects; + } + + static IArguments toArguments( Varargs values ) + { + return values == Constants.NONE ? VarargArguments.EMPTY : new VarargArguments( values ); + } + + /** + * A {@link DebugHandler} which observes the {@link TimeoutState} and responds accordingly. + */ + private class TimeoutDebugHandler extends DebugHandler + { + private final TimeoutState timeout; + private int count = 0; + boolean thrownSoftAbort; + + private boolean isPaused; + private int oldFlags; + private boolean oldInHook; + + TimeoutDebugHandler() + { + timeout = CobaltLuaMachine.this.timeout; + } + + @Override + public void onInstruction( DebugState ds, DebugFrame di, int pc ) throws LuaError, UnwindThrowable + { + di.pc = pc; + + if( isPaused ) resetPaused( ds, di ); + + // We check our current pause/abort state every 128 instructions. + if( (count = (count + 1) & 127) == 0 ) + { + // If we've been hard aborted or closed then abort. + if( timeout.isHardAborted() || m_state == null ) throw HardAbortError.INSTANCE; + + timeout.refresh(); + if( timeout.isPaused() ) + { + // Preserve the current state + isPaused = true; + oldInHook = ds.inhook; + oldFlags = di.flags; + + // Suspend the state. This will probably throw, but we need to handle the case where it won't. + di.flags |= FLAG_HOOKYIELD | FLAG_HOOKED; + LuaThread.suspend( ds.getLuaState() ); + resetPaused( ds, di ); + } + + handleSoftAbort(); + } + + super.onInstruction( ds, di, pc ); + } + + @Override + public void poll() throws LuaError + { + // If we've been hard aborted or closed then abort. + LuaState state = m_state; + if( timeout.isHardAborted() || state == null ) throw HardAbortError.INSTANCE; + + timeout.refresh(); + if( timeout.isPaused() ) LuaThread.suspendBlocking( state ); + handleSoftAbort(); + } + + private void resetPaused( DebugState ds, DebugFrame di ) + { + // Restore the previous paused state + isPaused = false; + ds.inhook = oldInHook; + di.flags = oldFlags; + } + + private void handleSoftAbort() throws LuaError + { + // If we already thrown our soft abort error then don't do it again. + if( !timeout.isSoftAborted() || thrownSoftAbort ) return; + + thrownSoftAbort = true; + throw new LuaError( TimeoutState.ABORT_MESSAGE ); + } + } + + private class CobaltLuaContext implements ILuaContext + { + @Override + public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException + { + // Issue command + final long taskID = MainThread.getUniqueTaskID(); + final Runnable iTask = () -> { + try + { + Object[] results = task.execute(); + if( results != null ) + { + Object[] eventArguments = new Object[results.length + 2]; + eventArguments[0] = taskID; + eventArguments[1] = true; + System.arraycopy( results, 0, eventArguments, 2, results.length ); + m_computer.queueEvent( "task_complete", eventArguments ); + } + else + { + m_computer.queueEvent( "task_complete", new Object[] { taskID, true } ); + } + } + catch( LuaException e ) + { + m_computer.queueEvent( "task_complete", new Object[] { taskID, false, e.getMessage() } ); + } + catch( Throwable t ) + { + if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error running task", t ); + m_computer.queueEvent( "task_complete", new Object[] { + taskID, false, "Java Exception Thrown: " + t, + } ); + } + }; + if( m_computer.queueMainThread( iTask ) ) + { + return taskID; + } + else + { + throw new LuaException( "Task limit exceeded" ); + } + } + } + + private static final class HardAbortError extends Error + { + private static final long serialVersionUID = 7954092008586367501L; + + static final HardAbortError INSTANCE = new HardAbortError(); + + private HardAbortError() + { + super( "Hard Abort", null, true, false ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java b/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java new file mode 100644 index 000000000..5c9de9cb3 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/ILuaMachine.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.core.lua; + +import dan200.computercraft.api.lua.IDynamicLuaObject; +import dan200.computercraft.api.lua.ILuaAPI; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.InputStream; + +/** + * Represents a machine which will execute Lua code. Technically this API is flexible enough to support many languages, + * but you'd need a way to provide alternative ROMs, BIOSes, etc... + * + * There should only be one concrete implementation at any one time, which is currently {@link CobaltLuaMachine}. If + * external mod authors are interested in registering their own machines, we can look into how we can provide some + * mechanism for registering these. + * + * This should provide implementations of {@link dan200.computercraft.api.lua.ILuaContext}, and the ability to convert + * {@link IDynamicLuaObject}s into something the VM understands, as well as handling method calls. + */ +public interface ILuaMachine +{ + /** + * Inject an API into the global environment of this machine. This should construct an object, as it would for any + * {@link IDynamicLuaObject} and set it to all names in {@link ILuaAPI#getNames()}. + * + * Called before {@link #loadBios(InputStream)}. + * + * @param api The API to register. + */ + void addAPI( @Nonnull ILuaAPI api ); + + /** + * Create a function from the provided program, and set it up to run when {@link #handleEvent(String, Object[])} is + * called. + * + * This should destroy the machine if it failed to load the bios. + * + * @param bios The stream containing the boot program. + * @return The result of loading this machine. Will either be OK, or the error message when loading the bios. + */ + MachineResult loadBios( @Nonnull InputStream bios ); + + /** + * Resume the machine, either starting or resuming the coroutine. + * + * This should destroy the machine if it failed to execute successfully. + * + * @param eventName The name of the event. This is {@code null} when first starting the machine. Note, this may + * do nothing if it does not match the event filter. + * @param arguments The arguments for this event. + * @return The result of loading this machine. Will either be OK, or the error message that occurred when + * executing. + */ + MachineResult handleEvent( @Nullable String eventName, @Nullable Object[] arguments ); + + /** + * Close the Lua machine, aborting any running functions and deleting the internal state. + */ + void close(); +} diff --git a/src/main/java/dan200/computercraft/core/lua/MachineResult.java b/src/main/java/dan200/computercraft/core/lua/MachineResult.java new file mode 100644 index 000000000..adff1058f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/MachineResult.java @@ -0,0 +1,80 @@ +/* + * 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.lua; + +import dan200.computercraft.core.computer.TimeoutState; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.InputStream; + +/** + * The result of executing an action on a machine. + * + * Errors should halt the machine and display the error to the user. + * + * @see ILuaMachine#loadBios(InputStream) + * @see ILuaMachine#handleEvent(String, Object[]) + */ +public final class MachineResult +{ + /** + * A successful complete execution. + */ + public static final MachineResult OK = new MachineResult( false, false, null ); + + /** + * A successful paused execution. + */ + public static final MachineResult PAUSE = new MachineResult( false, true, null ); + + /** + * An execution which timed out. + */ + public static final MachineResult TIMEOUT = new MachineResult( true, false, TimeoutState.ABORT_MESSAGE ); + + /** + * An error with no user-friendly error message. + */ + public static final MachineResult GENERIC_ERROR = new MachineResult( true, false, null ); + + private final boolean error; + private final boolean pause; + private final String message; + + private MachineResult( boolean error, boolean pause, String message ) + { + this.pause = pause; + this.message = message; + this.error = error; + } + + public static MachineResult error( @Nonnull String error ) + { + return new MachineResult( true, false, error ); + } + + public static MachineResult error( @Nonnull Exception error ) + { + return new MachineResult( true, false, error.getMessage() ); + } + + public boolean isError() + { + return error; + } + + public boolean isPause() + { + return pause; + } + + @Nullable + public String getMessage() + { + return message; + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java b/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java new file mode 100644 index 000000000..7d02cae7d --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java @@ -0,0 +1,122 @@ +/* + * 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.lua; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.*; +import dan200.computercraft.core.asm.LuaMethod; +import dan200.computercraft.core.lua.ResultInterpreterFunction.Container; +import org.squiddev.cobalt.*; +import org.squiddev.cobalt.debug.DebugFrame; +import org.squiddev.cobalt.function.ResumableVarArgFunction; + +import javax.annotation.Nonnull; + +/** + * Calls a {@link LuaMethod}, and interprets the resulting {@link MethodResult}, either returning the result or yielding + * and resuming the supplied continuation. + */ +class ResultInterpreterFunction extends ResumableVarArgFunction +{ + @Nonnull + static class Container + { + ILuaCallback callback; + int errorAdjust; + + Container( ILuaCallback callback, int errorAdjust ) + { + this.callback = callback; + this.errorAdjust = errorAdjust; + } + } + + private final CobaltLuaMachine machine; + private final LuaMethod method; + private final Object instance; + private final ILuaContext context; + private final String name; + + ResultInterpreterFunction( CobaltLuaMachine machine, LuaMethod method, Object instance, ILuaContext context, String name ) + { + this.machine = machine; + this.method = method; + this.instance = instance; + this.context = context; + this.name = name; + } + + @Override + protected Varargs invoke( LuaState state, DebugFrame debugFrame, Varargs args ) throws LuaError, UnwindThrowable + { + IArguments arguments = CobaltLuaMachine.toArguments( args ); + MethodResult results; + try + { + results = method.apply( instance, context, arguments ); + } + catch( LuaException e ) + { + throw wrap( e, 0 ); + } + catch( Throwable t ) + { + if( ComputerCraft.logComputerErrors ) + { + ComputerCraft.log.error( "Error calling " + name + " on " + instance, t ); + } + throw new LuaError( "Java Exception Thrown: " + t, 0 ); + } + + ILuaCallback callback = results.getCallback(); + Varargs ret = machine.toValues( results.getResult() ); + + if( callback == null ) return ret; + + debugFrame.state = new Container( callback, results.getErrorAdjust() ); + return LuaThread.yield( state, ret ); + } + + @Override + protected Varargs resumeThis( LuaState state, Container container, Varargs args ) throws LuaError, UnwindThrowable + { + MethodResult results; + Object[] arguments = CobaltLuaMachine.toObjects( args ); + try + { + results = container.callback.resume( arguments ); + } + catch( LuaException e ) + { + throw wrap( e, container.errorAdjust ); + } + catch( Throwable t ) + { + if( ComputerCraft.logComputerErrors ) + { + ComputerCraft.log.error( "Error calling " + name + " on " + container.callback, t ); + } + throw new LuaError( "Java Exception Thrown: " + t, 0 ); + } + + Varargs ret = machine.toValues( results.getResult() ); + + ILuaCallback callback = results.getCallback(); + if( callback == null ) return ret; + + container.callback = callback; + return LuaThread.yield( state, ret ); + } + + public static LuaError wrap( LuaException exception, int adjust ) + { + if( !exception.hasLevel() && adjust == 0 ) return new LuaError( exception.getMessage() ); + + int level = exception.getLevel(); + return new LuaError( exception.getMessage(), level <= 0 ? level : level + adjust + 1 ); + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/VarargArguments.java b/src/main/java/dan200/computercraft/core/lua/VarargArguments.java new file mode 100644 index 000000000..cedba0a80 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/VarargArguments.java @@ -0,0 +1,102 @@ +/* + * 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.lua; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaValues; +import org.squiddev.cobalt.*; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.Optional; + +class VarargArguments implements IArguments +{ + static final IArguments EMPTY = new VarargArguments( Constants.NONE ); + + private final Varargs varargs; + private Object[] cache; + + VarargArguments( Varargs varargs ) + { + this.varargs = varargs; + } + + @Override + public int count() + { + return varargs.count(); + } + + @Nullable + @Override + public Object get( int index ) + { + if( index < 0 || index >= varargs.count() ) return null; + + Object[] cache = this.cache; + if( cache == null ) + { + cache = this.cache = new Object[varargs.count()]; + } + else + { + Object existing = cache[index]; + if( existing != null ) return existing; + } + + return cache[index] = CobaltLuaMachine.toObject( varargs.arg( index + 1 ), null ); + } + + @Override + public IArguments drop( int count ) + { + if( count < 0 ) throw new IllegalStateException( "count cannot be negative" ); + if( count == 0 ) return this; + return new VarargArguments( varargs.subargs( count + 1 ) ); + } + + @Override + public double getDouble( int index ) throws LuaException + { + LuaValue value = varargs.arg( index + 1 ); + if( !(value instanceof LuaNumber) ) throw LuaValues.badArgument( index, "number", value.typeName() ); + return value.toDouble(); + } + + @Override + public long getLong( int index ) throws LuaException + { + LuaValue value = varargs.arg( index + 1 ); + if( !(value instanceof LuaNumber) ) throw LuaValues.badArgument( index, "number", value.typeName() ); + return value instanceof LuaInteger ? value.toInteger() : (long) LuaValues.checkFinite( index, value.toDouble() ); + } + + @Nonnull + @Override + public ByteBuffer getBytes( int index ) throws LuaException + { + LuaValue value = varargs.arg( index + 1 ); + if( !(value instanceof LuaBaseString) ) throw LuaValues.badArgument( index, "string", value.typeName() ); + + LuaString str = ((LuaBaseString) value).strvalue(); + return ByteBuffer.wrap( str.bytes, str.offset, str.length ).asReadOnlyBuffer(); + } + + @Override + public Optional optBytes( int index ) throws LuaException + { + LuaValue value = varargs.arg( index + 1 ); + if( value.isNil() ) return Optional.empty(); + if( !(value instanceof LuaBaseString) ) throw LuaValues.badArgument( index, "string", value.typeName() ); + + LuaString str = ((LuaBaseString) value).strvalue(); + return Optional.of( ByteBuffer.wrap( str.bytes, str.offset, str.length ).asReadOnlyBuffer() ); + } +} diff --git a/src/main/java/dan200/computercraft/core/terminal/Terminal.java b/src/main/java/dan200/computercraft/core/terminal/Terminal.java new file mode 100644 index 000000000..ee4e133fe --- /dev/null +++ b/src/main/java/dan200/computercraft/core/terminal/Terminal.java @@ -0,0 +1,423 @@ +/* + * 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.terminal; + +import dan200.computercraft.shared.util.Colour; +import dan200.computercraft.shared.util.Palette; +import javax.annotation.Nonnull; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.PacketByteBuf; + +public class Terminal +{ + private static final String base16 = "0123456789abcdef"; + + private int m_cursorX = 0; + private int m_cursorY = 0; + private boolean m_cursorBlink = false; + private int m_cursorColour = 0; + private int m_cursorBackgroundColour = 15; + + private int m_width; + private int m_height; + + private TextBuffer[] m_text; + private TextBuffer[] m_textColour; + private TextBuffer[] m_backgroundColour; + + private final Palette m_palette = new Palette(); + + private final Runnable onChanged; + + public Terminal( int width, int height ) + { + this( width, height, null ); + } + + public Terminal( int width, int height, Runnable changedCallback ) + { + m_width = width; + m_height = height; + onChanged = changedCallback; + + m_text = new TextBuffer[m_height]; + m_textColour = new TextBuffer[m_height]; + m_backgroundColour = new TextBuffer[m_height]; + for( int i = 0; i < m_height; i++ ) + { + m_text[i] = new TextBuffer( ' ', m_width ); + m_textColour[i] = new TextBuffer( base16.charAt( m_cursorColour ), m_width ); + m_backgroundColour[i] = new TextBuffer( base16.charAt( m_cursorBackgroundColour ), m_width ); + } + } + + public synchronized void reset() + { + m_cursorColour = 0; + m_cursorBackgroundColour = 15; + m_cursorX = 0; + m_cursorY = 0; + m_cursorBlink = false; + clear(); + setChanged(); + m_palette.resetColours(); + } + + public int getWidth() + { + return m_width; + } + + public int getHeight() + { + return m_height; + } + + public synchronized void resize( int width, int height ) + { + if( width == m_width && height == m_height ) + { + return; + } + + int oldHeight = m_height; + int oldWidth = m_width; + TextBuffer[] oldText = m_text; + TextBuffer[] oldTextColour = m_textColour; + TextBuffer[] oldBackgroundColour = m_backgroundColour; + + m_width = width; + m_height = height; + + m_text = new TextBuffer[m_height]; + m_textColour = new TextBuffer[m_height]; + m_backgroundColour = new TextBuffer[m_height]; + for( int i = 0; i < m_height; i++ ) + { + if( i >= oldHeight ) + { + m_text[i] = new TextBuffer( ' ', m_width ); + m_textColour[i] = new TextBuffer( base16.charAt( m_cursorColour ), m_width ); + m_backgroundColour[i] = new TextBuffer( base16.charAt( m_cursorBackgroundColour ), m_width ); + } + else if( m_width == oldWidth ) + { + m_text[i] = oldText[i]; + m_textColour[i] = oldTextColour[i]; + m_backgroundColour[i] = oldBackgroundColour[i]; + } + else + { + m_text[i] = new TextBuffer( ' ', m_width ); + m_textColour[i] = new TextBuffer( base16.charAt( m_cursorColour ), m_width ); + m_backgroundColour[i] = new TextBuffer( base16.charAt( m_cursorBackgroundColour ), m_width ); + m_text[i].write( oldText[i] ); + m_textColour[i].write( oldTextColour[i] ); + m_backgroundColour[i].write( oldBackgroundColour[i] ); + } + } + setChanged(); + } + + public void setCursorPos( int x, int y ) + { + if( m_cursorX != x || m_cursorY != y ) + { + m_cursorX = x; + m_cursorY = y; + setChanged(); + } + } + + public void setCursorBlink( boolean blink ) + { + if( m_cursorBlink != blink ) + { + m_cursorBlink = blink; + setChanged(); + } + } + + public void setTextColour( int colour ) + { + if( m_cursorColour != colour ) + { + m_cursorColour = colour; + setChanged(); + } + } + + public void setBackgroundColour( int colour ) + { + if( m_cursorBackgroundColour != colour ) + { + m_cursorBackgroundColour = colour; + setChanged(); + } + } + + public int getCursorX() + { + return m_cursorX; + } + + public int getCursorY() + { + return m_cursorY; + } + + public boolean getCursorBlink() + { + return m_cursorBlink; + } + + public int getTextColour() + { + return m_cursorColour; + } + + public int getBackgroundColour() + { + return m_cursorBackgroundColour; + } + + @Nonnull + public Palette getPalette() + { + return m_palette; + } + + public synchronized void blit( String text, String textColour, String backgroundColour ) + { + int x = m_cursorX; + int y = m_cursorY; + if( y >= 0 && y < m_height ) + { + m_text[y].write( text, x ); + m_textColour[y].write( textColour, x ); + m_backgroundColour[y].write( backgroundColour, x ); + setChanged(); + } + } + + public synchronized void write( String text ) + { + int x = m_cursorX; + int y = m_cursorY; + if( y >= 0 && y < m_height ) + { + m_text[y].write( text, x ); + m_textColour[y].fill( base16.charAt( m_cursorColour ), x, x + text.length() ); + m_backgroundColour[y].fill( base16.charAt( m_cursorBackgroundColour ), x, x + text.length() ); + setChanged(); + } + } + + public synchronized void scroll( int yDiff ) + { + if( yDiff != 0 ) + { + TextBuffer[] newText = new TextBuffer[m_height]; + TextBuffer[] newTextColour = new TextBuffer[m_height]; + TextBuffer[] newBackgroundColour = new TextBuffer[m_height]; + for( int y = 0; y < m_height; y++ ) + { + int oldY = y + yDiff; + if( oldY >= 0 && oldY < m_height ) + { + newText[y] = m_text[oldY]; + newTextColour[y] = m_textColour[oldY]; + newBackgroundColour[y] = m_backgroundColour[oldY]; + } + else + { + newText[y] = new TextBuffer( ' ', m_width ); + newTextColour[y] = new TextBuffer( base16.charAt( m_cursorColour ), m_width ); + newBackgroundColour[y] = new TextBuffer( base16.charAt( m_cursorBackgroundColour ), m_width ); + } + } + m_text = newText; + m_textColour = newTextColour; + m_backgroundColour = newBackgroundColour; + setChanged(); + } + } + + public synchronized void clear() + { + for( int y = 0; y < m_height; y++ ) + { + m_text[y].fill( ' ' ); + m_textColour[y].fill( base16.charAt( m_cursorColour ) ); + m_backgroundColour[y].fill( base16.charAt( m_cursorBackgroundColour ) ); + } + setChanged(); + } + + public synchronized void clearLine() + { + int y = m_cursorY; + if( y >= 0 && y < m_height ) + { + m_text[y].fill( ' ' ); + m_textColour[y].fill( base16.charAt( m_cursorColour ) ); + m_backgroundColour[y].fill( base16.charAt( m_cursorBackgroundColour ) ); + setChanged(); + } + } + + public synchronized TextBuffer getLine( int y ) + { + if( y >= 0 && y < m_height ) + { + return m_text[y]; + } + return null; + } + + public synchronized void setLine( int y, String text, String textColour, String backgroundColour ) + { + m_text[y].write( text ); + m_textColour[y].write( textColour ); + m_backgroundColour[y].write( backgroundColour ); + setChanged(); + } + + public synchronized TextBuffer getTextColourLine( int y ) + { + if( y >= 0 && y < m_height ) + { + return m_textColour[y]; + } + return null; + } + + public synchronized TextBuffer getBackgroundColourLine( int y ) + { + if( y >= 0 && y < m_height ) + { + return m_backgroundColour[y]; + } + return null; + } + + public final void setChanged() + { + if( onChanged != null ) onChanged.run(); + } + + public synchronized void write( PacketByteBuf buffer ) + { + buffer.writeInt( m_cursorX ); + buffer.writeInt( m_cursorY ); + buffer.writeBoolean( m_cursorBlink ); + buffer.writeByte( m_cursorBackgroundColour << 4 | m_cursorColour ); + + for( int y = 0; y < m_height; y++ ) + { + TextBuffer text = m_text[y]; + TextBuffer textColour = m_textColour[y]; + TextBuffer backColour = m_backgroundColour[y]; + + for( int x = 0; x < m_width; x++ ) + { + buffer.writeByte( text.charAt( x ) & 0xFF ); + buffer.writeByte( getColour( + backColour.charAt( x ), Colour.BLACK ) << 4 | + getColour( textColour.charAt( x ), Colour.WHITE ) + ); + } + } + + m_palette.write( buffer ); + } + + public synchronized void read( PacketByteBuf buffer ) + { + m_cursorX = buffer.readInt(); + m_cursorY = buffer.readInt(); + m_cursorBlink = buffer.readBoolean(); + + byte cursorColour = buffer.readByte(); + m_cursorBackgroundColour = (cursorColour >> 4) & 0xF; + m_cursorColour = cursorColour & 0xF; + + for( int y = 0; y < m_height; y++ ) + { + TextBuffer text = m_text[y]; + TextBuffer textColour = m_textColour[y]; + TextBuffer backColour = m_backgroundColour[y]; + + for( int x = 0; x < m_width; x++ ) + { + text.setChar( x, (char) (buffer.readByte() & 0xFF) ); + + byte colour = buffer.readByte(); + backColour.setChar( x, base16.charAt( (colour >> 4) & 0xF ) ); + textColour.setChar( x, base16.charAt( colour & 0xF ) ); + } + } + + m_palette.read( buffer ); + setChanged(); + } + + public synchronized CompoundTag writeToNBT( CompoundTag nbt ) + { + nbt.putInt( "term_cursorX", m_cursorX ); + nbt.putInt( "term_cursorY", m_cursorY ); + nbt.putBoolean( "term_cursorBlink", m_cursorBlink ); + nbt.putInt( "term_textColour", m_cursorColour ); + nbt.putInt( "term_bgColour", m_cursorBackgroundColour ); + for( int n = 0; n < m_height; n++ ) + { + nbt.putString( "term_text_" + n, m_text[n].toString() ); + nbt.putString( "term_textColour_" + n, m_textColour[n].toString() ); + nbt.putString( "term_textBgColour_" + n, m_backgroundColour[n].toString() ); + } + + m_palette.writeToNBT( nbt ); + return nbt; + } + + public synchronized void readFromNBT( CompoundTag nbt ) + { + m_cursorX = nbt.getInt( "term_cursorX" ); + m_cursorY = nbt.getInt( "term_cursorY" ); + m_cursorBlink = nbt.getBoolean( "term_cursorBlink" ); + m_cursorColour = nbt.getInt( "term_textColour" ); + m_cursorBackgroundColour = nbt.getInt( "term_bgColour" ); + + for( int n = 0; n < m_height; n++ ) + { + m_text[n].fill( ' ' ); + if( nbt.contains( "term_text_" + n ) ) + { + m_text[n].write( nbt.getString( "term_text_" + n ) ); + } + m_textColour[n].fill( base16.charAt( m_cursorColour ) ); + if( nbt.contains( "term_textColour_" + n ) ) + { + m_textColour[n].write( nbt.getString( "term_textColour_" + n ) ); + } + m_backgroundColour[n].fill( base16.charAt( m_cursorBackgroundColour ) ); + if( nbt.contains( "term_textBgColour_" + n ) ) + { + m_backgroundColour[n].write( nbt.getString( "term_textBgColour_" + n ) ); + } + } + + m_palette.readFromNBT( nbt ); + setChanged(); + } + + public static int getColour( char c, Colour def ) + { + if( c >= '0' && c <= '9' ) return c - '0'; + if( c >= 'a' && c <= 'f' ) return c - 'a' + 10; + return 15 - def.ordinal(); + } +} diff --git a/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java b/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java new file mode 100644 index 000000000..6a8e55355 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java @@ -0,0 +1,207 @@ +/* + * 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.terminal; + +public class TextBuffer +{ + private final char[] m_text; + + public TextBuffer( char c, int length ) + { + m_text = new char[length]; + for( int i = 0; i < length; i++ ) + { + m_text[i] = c; + } + } + + public TextBuffer( String text ) + { + this( text, 1 ); + } + + public TextBuffer( String text, int repetitions ) + { + int textLength = text.length(); + m_text = new char[textLength * repetitions]; + for( int i = 0; i < repetitions; i++ ) + { + for( int j = 0; j < textLength; j++ ) + { + m_text[j + i * textLength] = text.charAt( j ); + } + } + } + + public TextBuffer( TextBuffer text ) + { + this( text, 1 ); + } + + public TextBuffer( TextBuffer text, int repetitions ) + { + int textLength = text.length(); + m_text = new char[textLength * repetitions]; + for( int i = 0; i < repetitions; i++ ) + { + for( int j = 0; j < textLength; j++ ) + { + m_text[j + i * textLength] = text.charAt( j ); + } + } + } + + public int length() + { + return m_text.length; + } + + public String read() + { + return read( 0, m_text.length ); + } + + public String read( int start ) + { + return read( start, m_text.length ); + } + + public String read( int start, int end ) + { + start = Math.max( start, 0 ); + end = Math.min( end, m_text.length ); + int textLength = Math.max( end - start, 0 ); + return new String( m_text, start, textLength ); + } + + public void write( String text ) + { + write( text, 0, text.length() ); + } + + public void write( String text, int start ) + { + write( text, start, start + text.length() ); + } + + public void write( String text, int start, int end ) + { + int pos = start; + start = Math.max( start, 0 ); + end = Math.min( end, pos + text.length() ); + end = Math.min( end, m_text.length ); + for( int i = start; i < end; i++ ) + { + m_text[i] = text.charAt( i - pos ); + } + } + + public void write( TextBuffer text ) + { + write( text, 0, text.length() ); + } + + public void write( TextBuffer text, int start ) + { + write( text, start, start + text.length() ); + } + + public void write( TextBuffer text, int start, int end ) + { + int pos = start; + start = Math.max( start, 0 ); + end = Math.min( end, pos + text.length() ); + end = Math.min( end, m_text.length ); + for( int i = start; i < end; i++ ) + { + m_text[i] = text.charAt( i - pos ); + } + } + + public void fill( char c ) + { + fill( c, 0, m_text.length ); + } + + public void fill( char c, int start ) + { + fill( c, start, m_text.length ); + } + + public void fill( char c, int start, int end ) + { + start = Math.max( start, 0 ); + end = Math.min( end, m_text.length ); + for( int i = start; i < end; i++ ) + { + m_text[i] = c; + } + } + + public void fill( String text ) + { + fill( text, 0, m_text.length ); + } + + public void fill( String text, int start ) + { + fill( text, start, m_text.length ); + } + + public void fill( String text, int start, int end ) + { + int pos = start; + start = Math.max( start, 0 ); + end = Math.min( end, m_text.length ); + + int textLength = text.length(); + for( int i = start; i < end; i++ ) + { + m_text[i] = text.charAt( (i - pos) % textLength ); + } + } + + public void fill( TextBuffer text ) + { + fill( text, 0, m_text.length ); + } + + public void fill( TextBuffer text, int start ) + { + fill( text, start, m_text.length ); + } + + public void fill( TextBuffer text, int start, int end ) + { + int pos = start; + start = Math.max( start, 0 ); + end = Math.min( end, m_text.length ); + + int textLength = text.length(); + for( int i = start; i < end; i++ ) + { + m_text[i] = text.charAt( (i - pos) % textLength ); + } + } + + public char charAt( int i ) + { + return m_text[i]; + } + + public void setChar( int i, char c ) + { + if( i >= 0 && i < m_text.length ) + { + m_text[i] = c; + } + } + + public String toString() + { + return new String( m_text ); + } +} diff --git a/src/main/java/dan200/computercraft/core/tracking/ComputerTracker.java b/src/main/java/dan200/computercraft/core/tracking/ComputerTracker.java new file mode 100644 index 000000000..3cc9886c9 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/tracking/ComputerTracker.java @@ -0,0 +1,122 @@ +/* + * 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.tracking; + +import dan200.computercraft.core.computer.Computer; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; + +import javax.annotation.Nullable; +import java.lang.ref.WeakReference; + +public class ComputerTracker +{ + private final WeakReference computer; + private final int computerId; + + private long tasks; + private long totalTime; + private long maxTime; + + private long serverCount; + private long serverTime; + + private final Object2LongOpenHashMap fields; + + public ComputerTracker( Computer computer ) + { + this.computer = new WeakReference<>( computer ); + computerId = computer.getID(); + fields = new Object2LongOpenHashMap<>(); + } + + ComputerTracker( ComputerTracker timings ) + { + computer = timings.computer; + computerId = timings.computerId; + + tasks = timings.tasks; + totalTime = timings.totalTime; + maxTime = timings.maxTime; + + serverCount = timings.serverCount; + serverTime = timings.serverTime; + + fields = new Object2LongOpenHashMap<>( timings.fields ); + } + + @Nullable + public Computer getComputer() + { + return computer.get(); + } + + public int getComputerId() + { + return computerId; + } + + public long getTasks() + { + return tasks; + } + + public long getTotalTime() + { + return totalTime; + } + + public long getMaxTime() + { + return maxTime; + } + + public long getAverage() + { + return totalTime / tasks; + } + + void addTaskTiming( long time ) + { + tasks++; + totalTime += time; + if( time > maxTime ) maxTime = time; + } + + void addMainTiming( long time ) + { + serverCount++; + serverTime += time; + } + + void addValue( TrackingField field, long change ) + { + synchronized( fields ) + { + fields.addTo( field, change ); + } + } + + public long get( TrackingField field ) + { + if( field == TrackingField.TASKS ) return tasks; + if( field == TrackingField.MAX_TIME ) return maxTime; + if( field == TrackingField.TOTAL_TIME ) return totalTime; + if( field == TrackingField.AVERAGE_TIME ) return tasks == 0 ? 0 : totalTime / tasks; + + if( field == TrackingField.SERVER_COUNT ) return serverCount; + if( field == TrackingField.SERVER_TIME ) return serverTime; + + synchronized( fields ) + { + return fields.getLong( field ); + } + } + + public String getFormatted( TrackingField field ) + { + return field.format( get( field ) ); + } +} diff --git a/src/main/java/dan200/computercraft/core/tracking/Tracker.java b/src/main/java/dan200/computercraft/core/tracking/Tracker.java new file mode 100644 index 000000000..b0c0c4743 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/tracking/Tracker.java @@ -0,0 +1,47 @@ +/* + * 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.tracking; + +import dan200.computercraft.core.computer.Computer; + +public interface Tracker +{ + /** + * Report how long a task executed on the computer thread took. + * + * Computer thread tasks include events or a computer being turned on/off. + * + * @param computer The computer processing this task + * @param time The time taken for this task. + */ + default void addTaskTiming( Computer computer, long time ) + { + } + + /** + * Report how long a task executed on the server thread took. + * + * Server tasks include actions performed by peripherals. + * + * @param computer The computer processing this task + * @param time The time taken for this task. + */ + default void addServerTiming( Computer computer, long time ) + { + } + + /** + * Increment an arbitrary field by some value. Implementations may track how often this is called + * as well as the change, to compute some level of "average". + * + * @param computer The computer to increment + * @param field The field to increment. + * @param change The amount to increment said field by. + */ + default void addValue( Computer computer, TrackingField field, long change ) + { + } +} diff --git a/src/main/java/dan200/computercraft/core/tracking/Tracking.java b/src/main/java/dan200/computercraft/core/tracking/Tracking.java new file mode 100644 index 000000000..587b242cc --- /dev/null +++ b/src/main/java/dan200/computercraft/core/tracking/Tracking.java @@ -0,0 +1,87 @@ +/* + * 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.tracking; + +import dan200.computercraft.core.computer.Computer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public final class Tracking +{ + static final AtomicInteger tracking = new AtomicInteger( 0 ); + + private static final Object lock = new Object(); + private static final HashMap contexts = new HashMap<>(); + private static final List trackers = new ArrayList<>(); + + private Tracking() {} + + public static TrackingContext getContext( UUID uuid ) + { + synchronized( lock ) + { + TrackingContext context = contexts.get( uuid ); + if( context == null ) contexts.put( uuid, context = new TrackingContext() ); + return context; + } + } + + public static void add( Tracker tracker ) + { + synchronized( lock ) + { + trackers.add( tracker ); + tracking.incrementAndGet(); + } + } + + public static void addTaskTiming( Computer computer, long time ) + { + if( tracking.get() == 0 ) return; + + synchronized( contexts ) + { + for( TrackingContext context : contexts.values() ) context.addTaskTiming( computer, time ); + for( Tracker tracker : trackers ) tracker.addTaskTiming( computer, time ); + } + } + + public static void addServerTiming( Computer computer, long time ) + { + if( tracking.get() == 0 ) return; + + synchronized( contexts ) + { + for( TrackingContext context : contexts.values() ) context.addServerTiming( computer, time ); + for( Tracker tracker : trackers ) tracker.addServerTiming( computer, time ); + } + } + + public static void addValue( Computer computer, TrackingField field, long change ) + { + if( tracking.get() == 0 ) return; + + synchronized( lock ) + { + for( TrackingContext context : contexts.values() ) context.addValue( computer, field, change ); + for( Tracker tracker : trackers ) tracker.addValue( computer, field, change ); + } + } + + public static void reset() + { + synchronized( lock ) + { + contexts.clear(); + trackers.clear(); + tracking.set( 0 ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/tracking/TrackingContext.java b/src/main/java/dan200/computercraft/core/tracking/TrackingContext.java new file mode 100644 index 000000000..d1205c2e2 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/tracking/TrackingContext.java @@ -0,0 +1,116 @@ +/* + * 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.tracking; + +import com.google.common.collect.MapMaker; +import dan200.computercraft.core.computer.Computer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Tracks timing information about computers, including how long they ran for + * and the number of events they handled. + * + * Note that this will track computers which have been deleted (hence + * the presence of {@link #timingLookup} and {@link #timings} + */ +public class TrackingContext implements Tracker +{ + private boolean tracking = false; + + private final List timings = new ArrayList<>(); + private final Map timingLookup = new MapMaker().weakKeys().makeMap(); + + public synchronized void start() + { + if( !tracking ) Tracking.tracking.incrementAndGet(); + tracking = true; + + timings.clear(); + timingLookup.clear(); + } + + public synchronized boolean stop() + { + if( !tracking ) return false; + + Tracking.tracking.decrementAndGet(); + tracking = false; + timingLookup.clear(); + return true; + } + + public synchronized List getImmutableTimings() + { + ArrayList timings = new ArrayList<>( this.timings.size() ); + for( ComputerTracker timing : this.timings ) timings.add( new ComputerTracker( timing ) ); + return timings; + } + + public synchronized List getTimings() + { + return new ArrayList<>( timings ); + } + + @Override + public void addTaskTiming( Computer computer, long time ) + { + if( !tracking ) return; + + synchronized( this ) + { + ComputerTracker computerTimings = timingLookup.get( computer ); + if( computerTimings == null ) + { + computerTimings = new ComputerTracker( computer ); + timingLookup.put( computer, computerTimings ); + timings.add( computerTimings ); + } + + computerTimings.addTaskTiming( time ); + } + } + + @Override + public void addServerTiming( Computer computer, long time ) + { + if( !tracking ) return; + + synchronized( this ) + { + ComputerTracker computerTimings = timingLookup.get( computer ); + if( computerTimings == null ) + { + computerTimings = new ComputerTracker( computer ); + timingLookup.put( computer, computerTimings ); + timings.add( computerTimings ); + } + + computerTimings.addMainTiming( time ); + } + } + + @Override + public void addValue( Computer computer, TrackingField field, long change ) + { + if( !tracking ) return; + + synchronized( this ) + { + ComputerTracker computerTimings = timingLookup.get( computer ); + if( computerTimings == null ) + { + computerTimings = new ComputerTracker( computer ); + timingLookup.put( computer, computerTimings ); + timings.add( computerTimings ); + } + + computerTimings.addValue( field, change ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/tracking/TrackingField.java b/src/main/java/dan200/computercraft/core/tracking/TrackingField.java new file mode 100644 index 000000000..bc3f151ec --- /dev/null +++ b/src/main/java/dan200/computercraft/core/tracking/TrackingField.java @@ -0,0 +1,96 @@ +/* + * 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.tracking; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.LongFunction; + +public final class TrackingField +{ + private static final Map fields = new HashMap<>(); + + public static final TrackingField TASKS = TrackingField.of( "tasks", x -> String.format( "%4d", x ) ); + public static final TrackingField TOTAL_TIME = TrackingField.of( "total", x -> String.format( "%7.1fms", x / 1e6 ) ); + public static final TrackingField AVERAGE_TIME = TrackingField.of( "average", x -> String.format( "%4.1fms", x / 1e6 ) ); + public static final TrackingField MAX_TIME = TrackingField.of( "max", x -> String.format( "%5.1fms", x / 1e6 ) ); + + public static final TrackingField SERVER_COUNT = TrackingField.of( "server_count", x -> String.format( "%4d", x ) ); + public static final TrackingField SERVER_TIME = TrackingField.of( "server_time", x -> String.format( "%7.1fms", x / 1e6 ) ); + + public static final TrackingField PERIPHERAL_OPS = TrackingField.of( "peripheral", TrackingField::formatDefault ); + public static final TrackingField FS_OPS = TrackingField.of( "fs", TrackingField::formatDefault ); + public static final TrackingField TURTLE_OPS = TrackingField.of( "turtle", TrackingField::formatDefault ); + + public static final TrackingField HTTP_REQUESTS = TrackingField.of( "http", TrackingField::formatDefault ); + public static final TrackingField HTTP_UPLOAD = TrackingField.of( "http_upload", TrackingField::formatBytes ); + public static final TrackingField HTTP_DOWNLOAD = TrackingField.of( "http_download", TrackingField::formatBytes ); + + public static final TrackingField WEBSOCKET_INCOMING = TrackingField.of( "websocket_incoming", TrackingField::formatBytes ); + public static final TrackingField WEBSOCKET_OUTGOING = TrackingField.of( "websocket_outgoing", TrackingField::formatBytes ); + + public static final TrackingField COROUTINES_CREATED = TrackingField.of( "coroutines_created", x -> String.format( "%4d", x ) ); + public static final TrackingField COROUTINES_DISPOSED = TrackingField.of( "coroutines_dead", x -> String.format( "%4d", x ) ); + + private final String id; + private final String translationKey; + private final LongFunction format; + + public String id() + { + return id; + } + + public String translationKey() + { + return translationKey; + } + + private TrackingField( String id, LongFunction format ) + { + this.id = id; + translationKey = "tracking_field.computercraft." + id + ".name"; + this.format = format; + } + + public String format( long value ) + { + return format.apply( value ); + } + + public static TrackingField of( String id, LongFunction format ) + { + TrackingField field = new TrackingField( id, format ); + fields.put( id, field ); + return field; + } + + public static Map fields() + { + return Collections.unmodifiableMap( fields ); + } + + private static String formatDefault( long value ) + { + return String.format( "%6d", value ); + } + + /** + * So technically a kibibyte, but let's not argue here. + */ + private static final int KILOBYTE_SIZE = 1024; + + private static final String SI_PREFIXES = "KMGT"; + + private static String formatBytes( long bytes ) + { + if( bytes < 1024 ) return String.format( "%10d B", bytes ); + int exp = (int) (Math.log( bytes ) / Math.log( KILOBYTE_SIZE )); + if( exp > SI_PREFIXES.length() ) exp = SI_PREFIXES.length(); + return String.format( "%10.1f %siB", bytes / Math.pow( KILOBYTE_SIZE, exp ), SI_PREFIXES.charAt( exp - 1 ) ); + } +} diff --git a/src/main/java/dan200/computercraft/data/Generators.java b/src/main/java/dan200/computercraft/data/Generators.java new file mode 100644 index 000000000..ca54e51e8 --- /dev/null +++ b/src/main/java/dan200/computercraft/data/Generators.java @@ -0,0 +1,29 @@ +/* + * 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.data; + +import dan200.computercraft.shared.proxy.ComputerCraftProxyCommon; +import net.minecraft.data.DataGenerator; +import net.minecraft.data.server.BlockTagsProvider; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.GatherDataEvent; + +@Mod.EventBusSubscriber( bus = Mod.EventBusSubscriber.Bus.MOD ) +public class Generators +{ + @SubscribeEvent + public static void gather( GatherDataEvent event ) + { + ComputerCraftProxyCommon.registerLoot(); + + DataGenerator generator = event.getGenerator(); + generator.install( new Recipes( generator ) ); + generator.install( new LootTables( generator ) ); + generator.install( new Tags( generator, new BlockTagsProvider( generator ) ) ); + } +} diff --git a/src/main/java/dan200/computercraft/data/LootTableProvider.java b/src/main/java/dan200/computercraft/data/LootTableProvider.java new file mode 100644 index 000000000..ef822d4f8 --- /dev/null +++ b/src/main/java/dan200/computercraft/data/LootTableProvider.java @@ -0,0 +1,91 @@ +/* + * 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.data; + +import com.google.common.collect.Multimap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import dan200.computercraft.ComputerCraft; +import net.minecraft.data.DataCache; +import net.minecraft.data.DataGenerator; +import net.minecraft.data.DataProvider; +import net.minecraft.loot.LootManager; +import net.minecraft.loot.LootTable; +import net.minecraft.loot.LootTableReporter; +import net.minecraft.loot.context.LootContextTypes; +import net.minecraft.util.Identifier; +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +/** + * An alternative to {@link net.minecraft.data.LootTableProvider}, with a more flexible interface. + */ +public abstract class LootTableProvider implements DataProvider +{ + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + + private final DataGenerator generator; + + public LootTableProvider( DataGenerator generator ) + { + this.generator = generator; + } + + @Override + public void run( @Nonnull DataCache cache ) + { + Map tables = new HashMap<>(); + LootTableReporter validation = new LootTableReporter( LootContextTypes.GENERIC, x -> null, tables::get ); + + registerLoot( ( id, table ) -> { + if( tables.containsKey( id ) ) validation.report( "Duplicate loot tables for " + id ); + tables.put( id, table ); + } ); + + tables.forEach( ( key, value ) -> LootManager.validate( validation, key, value ) ); + + Multimap problems = validation.getMessages(); + if( !problems.isEmpty() ) + { + problems.forEach( ( child, problem ) -> + ComputerCraft.log.warn( "Found validation problem in " + child + ": " + problem ) ); + throw new IllegalStateException( "Failed to validate loot tables, see logs" ); + } + + tables.forEach( ( key, value ) -> { + Path path = getPath( key ); + try + { + DataProvider.writeToPath( GSON, cache, LootManager.toJson( value ), path ); + } + catch( IOException e ) + { + ComputerCraft.log.error( "Couldn't save loot table {}", path, e ); + } + } ); + } + + protected abstract void registerLoot( BiConsumer add ); + + @Nonnull + @Override + public String getName() + { + return "LootTables"; + } + + private Path getPath( Identifier id ) + { + return generator.getOutput() + .resolve( "data" ).resolve( id.getNamespace() ).resolve( "loot_tables" ) + .resolve( id.getPath() + ".json" ); + } +} diff --git a/src/main/java/dan200/computercraft/data/LootTables.java b/src/main/java/dan200/computercraft/data/LootTables.java new file mode 100644 index 000000000..e698b97f8 --- /dev/null +++ b/src/main/java/dan200/computercraft/data/LootTables.java @@ -0,0 +1,89 @@ +/* + * 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.data; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.data.BlockNamedEntityLootCondition; +import dan200.computercraft.shared.data.HasComputerIdLootCondition; +import dan200.computercraft.shared.data.PlayerCreativeLootCondition; +import dan200.computercraft.shared.proxy.ComputerCraftProxyCommon; +import net.minecraft.block.Block; +import net.minecraft.data.DataGenerator; +import net.minecraft.loot.*; +import net.minecraft.loot.condition.AlternativeLootCondition; +import net.minecraft.loot.condition.SurvivesExplosionLootCondition; +import net.minecraft.loot.context.LootContextTypes; +import net.minecraft.loot.entry.DynamicEntry; +import net.minecraft.loot.entry.ItemEntry; +import net.minecraft.util.Identifier; +import net.minecraftforge.fml.RegistryObject; + +import java.util.function.BiConsumer; + +public class LootTables extends LootTableProvider +{ + public LootTables( DataGenerator generator ) + { + super( generator ); + } + + @Override + protected void registerLoot( BiConsumer add ) + { + basicDrop( add, Registry.ModBlocks.DISK_DRIVE ); + basicDrop( add, Registry.ModBlocks.MONITOR_NORMAL ); + basicDrop( add, Registry.ModBlocks.MONITOR_ADVANCED ); + basicDrop( add, Registry.ModBlocks.PRINTER ); + basicDrop( add, Registry.ModBlocks.SPEAKER ); + basicDrop( add, Registry.ModBlocks.WIRED_MODEM_FULL ); + basicDrop( add, Registry.ModBlocks.WIRELESS_MODEM_NORMAL ); + basicDrop( add, Registry.ModBlocks.WIRELESS_MODEM_ADVANCED ); + + computerDrop( add, Registry.ModBlocks.COMPUTER_NORMAL ); + computerDrop( add, Registry.ModBlocks.COMPUTER_ADVANCED ); + computerDrop( add, Registry.ModBlocks.TURTLE_NORMAL ); + computerDrop( add, Registry.ModBlocks.TURTLE_ADVANCED ); + + add.accept( ComputerCraftProxyCommon.ForgeHandlers.LOOT_TREASURE_DISK, LootTable + .builder() + .type( LootContextTypes.GENERIC ) + .build() ); + } + + private static void basicDrop( BiConsumer add, RegistryObject wrapper ) + { + Block block = wrapper.get(); + add.accept( block.getLootTableId(), LootTable + .builder() + .type( LootContextTypes.BLOCK ) + .pool( LootPool.builder() + .name( "main" ) + .rolls( ConstantLootTableRange.create( 1 ) ) + .with( ItemEntry.builder( block ) ) + .conditionally( SurvivesExplosionLootCondition.builder() ) + ).build() ); + } + + private static void computerDrop( BiConsumer add, RegistryObject wrapper ) + { + Block block = wrapper.get(); + add.accept( block.getLootTableId(), LootTable + .builder() + .type( LootContextTypes.BLOCK ) + .pool( LootPool.builder() + .name( "main" ) + .rolls( ConstantLootTableRange.create( 1 ) ) + .with( DynamicEntry.builder( new Identifier( ComputerCraft.MOD_ID, "computer" ) ) ) + .conditionally( AlternativeLootCondition.builder( + BlockNamedEntityLootCondition.BUILDER, + HasComputerIdLootCondition.BUILDER, + PlayerCreativeLootCondition.BUILDER.invert() + ) ) + ).build() ); + } +} diff --git a/src/main/java/dan200/computercraft/data/RecipeWrapper.java b/src/main/java/dan200/computercraft/data/RecipeWrapper.java new file mode 100644 index 000000000..52d597cdb --- /dev/null +++ b/src/main/java/dan200/computercraft/data/RecipeWrapper.java @@ -0,0 +1,91 @@ +/* + * 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.data; + +import com.google.gson.JsonObject; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import net.minecraft.data.server.recipe.RecipeJsonProvider; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.util.Identifier; +import net.minecraft.util.JsonHelper; +import java.util.function.Consumer; + +/** + * Adapter for recipes which overrides the serializer and adds custom item NBT. + */ +public final class RecipeWrapper implements RecipeJsonProvider +{ + private final RecipeJsonProvider recipe; + private final CompoundTag resultData; + private final RecipeSerializer serializer; + + private RecipeWrapper( RecipeJsonProvider recipe, CompoundTag resultData, RecipeSerializer serializer ) + { + this.resultData = resultData; + this.recipe = recipe; + this.serializer = serializer; + } + + public static Consumer wrap( RecipeSerializer serializer, Consumer original ) + { + return x -> original.accept( new RecipeWrapper( x, null, serializer ) ); + } + + public static Consumer wrap( RecipeSerializer serializer, Consumer original, CompoundTag resultData ) + { + return x -> original.accept( new RecipeWrapper( x, resultData, serializer ) ); + } + + public static Consumer wrap( RecipeSerializer serializer, Consumer original, Consumer resultData ) + { + CompoundTag tag = new CompoundTag(); + resultData.accept( tag ); + return x -> original.accept( new RecipeWrapper( x, tag, serializer ) ); + } + + @Override + public void serialize( @Nonnull JsonObject jsonObject ) + { + recipe.serialize( jsonObject ); + + if( resultData != null ) + { + JsonObject object = JsonHelper.getObject( jsonObject, "result" ); + object.addProperty( "nbt", resultData.toString() ); + } + } + + @Nonnull + @Override + public Identifier getRecipeId() + { + return recipe.getRecipeId(); + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return serializer; + } + + @Nullable + @Override + public JsonObject toAdvancementJson() + { + return recipe.toAdvancementJson(); + } + + @Nullable + @Override + public Identifier getAdvancementId() + { + return recipe.getAdvancementId(); + } +} diff --git a/src/main/java/dan200/computercraft/data/Recipes.java b/src/main/java/dan200/computercraft/data/Recipes.java new file mode 100644 index 000000000..b2daa1c7f --- /dev/null +++ b/src/main/java/dan200/computercraft/data/Recipes.java @@ -0,0 +1,325 @@ +/* + * 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.data; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.data.Tags.CCTags; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.TurtleUpgrades; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.pocket.items.PocketComputerItemFactory; +import dan200.computercraft.shared.turtle.items.TurtleItemFactory; +import dan200.computercraft.shared.util.Colour; +import dan200.computercraft.shared.util.ImpostorRecipe; +import dan200.computercraft.shared.util.ImpostorShapelessRecipe; +import net.minecraft.advancement.criterion.InventoryChangedCriterion; +import net.minecraft.block.Blocks; +import net.minecraft.data.*; +import net.minecraft.data.server.RecipesProvider; +import net.minecraft.data.server.recipe.RecipeJsonProvider; +import net.minecraft.data.server.recipe.ShapedRecipeJsonFactory; +import net.minecraft.data.server.recipe.ShapelessRecipeJsonFactory; +import net.minecraft.item.*; +import net.minecraft.predicate.item.ItemPredicate; +import net.minecraft.tag.Tag; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Identifier; +import net.minecraftforge.common.Tags; + +import javax.annotation.Nonnull; +import java.util.Locale; +import java.util.function.Consumer; + +public class Recipes extends RecipesProvider +{ + public Recipes( DataGenerator generator ) + { + super( generator ); + } + + @Override + protected void generate( @Nonnull Consumer add ) + { + basicRecipes( add ); + diskColours( add ); + pocketUpgrades( add ); + turtleUpgrades( add ); + } + + /** + * Register a crafting recipe for a disk of every dye colour. + * + * @param add The callback to add recipes. + */ + private void diskColours( @Nonnull Consumer add ) + { + for( Colour colour : Colour.VALUES ) + { + ShapelessRecipeJsonFactory + .create( Registry.ModItems.DISK.get() ) + .input( Tags.Items.DUSTS_REDSTONE ) + .input( Items.PAPER ) + .input( DyeItem.byColor( ofColour( colour ) ) ) + .group( "computercraft:disk" ) + .criterion( "has_drive", inventoryChange( Registry.ModBlocks.DISK_DRIVE.get() ) ) + .offerTo( RecipeWrapper.wrap( + ImpostorShapelessRecipe.SERIALIZER, add, + x -> x.putInt( "color", colour.getHex() ) + ), new Identifier( ComputerCraft.MOD_ID, "disk_" + (colour.ordinal() + 1) ) ); + } + } + + /** + * Register a crafting recipe for each turtle upgrade. + * + * @param add The callback to add recipes. + */ + private void turtleUpgrades( @Nonnull Consumer add ) + { + for( ComputerFamily family : ComputerFamily.values() ) + { + ItemStack base = TurtleItemFactory.create( -1, null, -1, family, null, null, 0, null ); + if( base.isEmpty() ) continue; + + String nameId = family.name().toLowerCase( Locale.ROOT ); + + TurtleUpgrades.getVanillaUpgrades().forEach( upgrade -> { + ItemStack result = TurtleItemFactory.create( -1, null, -1, family, null, upgrade, -1, null ); + ShapedRecipeJsonFactory + .create( result.getItem() ) + .group( String.format( "%s:turtle_%s", ComputerCraft.MOD_ID, nameId ) ) + .pattern( "#T" ) + .input( '#', base.getItem() ) + .input( 'T', upgrade.getCraftingItem().getItem() ) + .criterion( "has_items", + inventoryChange( base.getItem(), upgrade.getCraftingItem().getItem() ) ) + .offerTo( + RecipeWrapper.wrap( ImpostorRecipe.SERIALIZER, add, result.getTag() ), + new Identifier( ComputerCraft.MOD_ID, String.format( "turtle_%s/%s/%s", + nameId, upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath() + ) ) + ); + } ); + } + } + + /** + * Register a crafting recipe for each pocket upgrade. + * + * @param add The callback to add recipes. + */ + private void pocketUpgrades( @Nonnull Consumer add ) + { + for( ComputerFamily family : ComputerFamily.values() ) + { + ItemStack base = PocketComputerItemFactory.create( -1, null, -1, family, null ); + if( base.isEmpty() ) continue; + + String nameId = family.name().toLowerCase( Locale.ROOT ); + + TurtleUpgrades.getVanillaUpgrades().forEach( upgrade -> { + ItemStack result = PocketComputerItemFactory.create( -1, null, -1, family, null ); + ShapedRecipeJsonFactory + .create( result.getItem() ) + .group( String.format( "%s:pocket_%s", ComputerCraft.MOD_ID, nameId ) ) + .pattern( "#" ) + .pattern( "P" ) + .input( '#', base.getItem() ) + .input( 'P', upgrade.getCraftingItem().getItem() ) + .criterion( "has_items", + inventoryChange( base.getItem(), upgrade.getCraftingItem().getItem() ) ) + .offerTo( + RecipeWrapper.wrap( ImpostorRecipe.SERIALIZER, add, result.getTag() ), + new Identifier( ComputerCraft.MOD_ID, String.format( "pocket_%s/%s/%s", + nameId, upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath() + ) ) + ); + } ); + } + } + + private void basicRecipes( @Nonnull Consumer add ) + { + ShapedRecipeJsonFactory + .create( Registry.ModItems.CABLE.get(), 6 ) + .pattern( " # " ) + .pattern( "#R#" ) + .pattern( " # " ) + .input( '#', Tags.Items.STONE ) + .input( 'R', Tags.Items.DUSTS_REDSTONE ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .criterion( "has_modem", inventoryChange( CCTags.COMPUTER ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModBlocks.COMPUTER_NORMAL.get() ) + .pattern( "###" ) + .pattern( "#R#" ) + .pattern( "#G#" ) + .input( '#', Tags.Items.STONE ) + .input( 'R', Tags.Items.DUSTS_REDSTONE ) + .input( 'G', Tags.Items.GLASS_PANES ) + .criterion( "has_redstone", inventoryChange( Tags.Items.DUSTS_REDSTONE ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModBlocks.COMPUTER_ADVANCED.get() ) + .pattern( "###" ) + .pattern( "#R#" ) + .pattern( "#G#" ) + .input( '#', Tags.Items.INGOTS_GOLD ) + .input( 'R', Tags.Items.DUSTS_REDSTONE ) + .input( 'G', Tags.Items.GLASS_PANES ) + .criterion( "has_components", inventoryChange( Items.REDSTONE, Items.GOLD_INGOT ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModBlocks.COMPUTER_COMMAND.get() ) + .pattern( "###" ) + .pattern( "#R#" ) + .pattern( "#G#" ) + .input( '#', Tags.Items.INGOTS_GOLD ) + .input( 'R', Blocks.COMMAND_BLOCK ) + .input( 'G', Tags.Items.GLASS_PANES ) + .criterion( "has_components", inventoryChange( Blocks.COMMAND_BLOCK ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModBlocks.DISK_DRIVE.get() ) + .pattern( "###" ) + .pattern( "#R#" ) + .pattern( "#R#" ) + .input( '#', Tags.Items.STONE ) + .input( 'R', Tags.Items.DUSTS_REDSTONE ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModBlocks.MONITOR_NORMAL.get() ) + .pattern( "###" ) + .pattern( "#G#" ) + .pattern( "###" ) + .input( '#', Tags.Items.STONE ) + .input( 'G', Tags.Items.GLASS_PANES ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModBlocks.MONITOR_ADVANCED.get(), 4 ) + .pattern( "###" ) + .pattern( "#G#" ) + .pattern( "###" ) + .input( '#', Tags.Items.INGOTS_GOLD ) + .input( 'G', Tags.Items.GLASS_PANES ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModItems.POCKET_COMPUTER_NORMAL.get() ) + .pattern( "###" ) + .pattern( "#A#" ) + .pattern( "#G#" ) + .input( '#', Tags.Items.STONE ) + .input( 'A', Items.GOLDEN_APPLE ) + .input( 'G', Tags.Items.GLASS_PANES ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .criterion( "has_apple", inventoryChange( Items.GOLDEN_APPLE ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModItems.POCKET_COMPUTER_ADVANCED.get() ) + .pattern( "###" ) + .pattern( "#A#" ) + .pattern( "#G#" ) + .input( '#', Tags.Items.INGOTS_GOLD ) + .input( 'A', Items.GOLDEN_APPLE ) + .input( 'G', Tags.Items.GLASS_PANES ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .criterion( "has_apple", inventoryChange( Items.GOLDEN_APPLE ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModBlocks.PRINTER.get() ) + .pattern( "###" ) + .pattern( "#R#" ) + .pattern( "#D#" ) + .input( '#', Tags.Items.STONE ) + .input( 'R', Tags.Items.DUSTS_REDSTONE ) + .input( 'D', Tags.Items.DYES ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModBlocks.SPEAKER.get() ) + .pattern( "###" ) + .pattern( "#N#" ) + .pattern( "#R#" ) + .input( '#', Tags.Items.STONE ) + .input( 'N', Blocks.NOTE_BLOCK ) + .input( 'R', Tags.Items.DUSTS_REDSTONE ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModItems.WIRED_MODEM.get() ) + .pattern( "###" ) + .pattern( "#R#" ) + .pattern( "###" ) + .input( '#', Tags.Items.STONE ) + .input( 'R', Tags.Items.DUSTS_REDSTONE ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .criterion( "has_cable", inventoryChange( Registry.ModItems.CABLE.get() ) ) + .offerTo( add ); + + ShapelessRecipeJsonFactory + .create( Registry.ModBlocks.WIRED_MODEM_FULL.get() ) + .input( Registry.ModItems.WIRED_MODEM.get() ) + .criterion( "has_modem", inventoryChange( CCTags.WIRED_MODEM ) ) + .offerTo( add, new Identifier( ComputerCraft.MOD_ID, "wired_modem_full_from" ) ); + ShapelessRecipeJsonFactory + .create( Registry.ModItems.WIRED_MODEM.get() ) + .input( Registry.ModBlocks.WIRED_MODEM_FULL.get() ) + .criterion( "has_modem", inventoryChange( CCTags.WIRED_MODEM ) ) + .offerTo( add, new Identifier( ComputerCraft.MOD_ID, "wired_modem_full_to" ) ); + + ShapedRecipeJsonFactory + .create( Registry.ModBlocks.WIRELESS_MODEM_NORMAL.get() ) + .pattern( "###" ) + .pattern( "#E#" ) + .pattern( "###" ) + .input( '#', Tags.Items.STONE ) + .input( 'E', Tags.Items.ENDER_PEARLS ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .offerTo( add ); + + ShapedRecipeJsonFactory + .create( Registry.ModBlocks.WIRELESS_MODEM_ADVANCED.get() ) + .pattern( "###" ) + .pattern( "#E#" ) + .pattern( "###" ) + .input( '#', Tags.Items.INGOTS_GOLD ) + .input( 'E', Items.ENDER_EYE ) + .criterion( "has_computer", inventoryChange( CCTags.COMPUTER ) ) + .criterion( "has_wireless", inventoryChange( Registry.ModBlocks.WIRELESS_MODEM_NORMAL.get() ) ) + .offerTo( add ); + } + + private static DyeColor ofColour( Colour colour ) + { + return DyeColor.byId( 15 - colour.ordinal() ); + } + + private static InventoryChangedCriterion.Conditions inventoryChange( Tag stack ) + { + return InventoryChangedCriterion.Conditions.items( ItemPredicate.Builder.create().tag( stack ).build() ); + } + + private static InventoryChangedCriterion.Conditions inventoryChange( ItemConvertible... stack ) + { + return InventoryChangedCriterion.Conditions.items( stack ); + } +} diff --git a/src/main/java/dan200/computercraft/data/Tags.java b/src/main/java/dan200/computercraft/data/Tags.java new file mode 100644 index 000000000..6c2eb03f7 --- /dev/null +++ b/src/main/java/dan200/computercraft/data/Tags.java @@ -0,0 +1,61 @@ +/* + * 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.data; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.Registry; +import net.minecraft.data.DataGenerator; +import net.minecraft.data.server.BlockTagsProvider; +import net.minecraft.data.server.ItemTagsProvider; +import net.minecraft.item.Item; +import net.minecraft.tag.ItemTags; +import net.minecraft.tag.Tag; +import net.minecraft.util.Identifier; + +import static dan200.computercraft.data.Tags.CCTags.*; + +public class Tags extends ItemTagsProvider +{ + private static final Tag.Identified PIGLIN_LOVED = ItemTags.PIGLIN_LOVED; + + public static class CCTags + { + public static final Tag.Identified COMPUTER = item( "computer" ); + public static final Tag.Identified TURTLE = item( "turtle" ); + public static final Tag.Identified WIRED_MODEM = item( "wired_modem" ); + public static final Tag.Identified MONITOR = item( "monitor" ); + } + + public Tags( DataGenerator generator, BlockTagsProvider tags ) + { + super( generator, tags ); + } + + @Override + protected void configure() + { + getOrCreateTagBuilder( COMPUTER ).add( + Registry.ModItems.COMPUTER_NORMAL.get(), + Registry.ModItems.COMPUTER_ADVANCED.get(), + Registry.ModItems.COMPUTER_COMMAND.get() + ); + getOrCreateTagBuilder( TURTLE ).add( Registry.ModItems.TURTLE_NORMAL.get(), Registry.ModItems.TURTLE_ADVANCED.get() ); + getOrCreateTagBuilder( WIRED_MODEM ).add( Registry.ModItems.WIRED_MODEM.get(), Registry.ModItems.WIRED_MODEM_FULL.get() ); + getOrCreateTagBuilder( MONITOR ).add( Registry.ModItems.MONITOR_NORMAL.get(), Registry.ModItems.MONITOR_ADVANCED.get() ); + + getOrCreateTagBuilder( PIGLIN_LOVED ).add( + Registry.ModItems.COMPUTER_ADVANCED.get(), Registry.ModItems.TURTLE_ADVANCED.get(), + Registry.ModItems.WIRELESS_MODEM_ADVANCED.get(), Registry.ModItems.POCKET_COMPUTER_ADVANCED.get(), + Registry.ModItems.MONITOR_ADVANCED.get() + ); + } + + private static Tag.Identified item( String name ) + { + return ItemTags.register( new Identifier( ComputerCraft.MOD_ID, name ).toString() ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/BundledRedstone.java b/src/main/java/dan200/computercraft/shared/BundledRedstone.java new file mode 100644 index 000000000..24439ebe5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/BundledRedstone.java @@ -0,0 +1,67 @@ +/* + * 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; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.redstone.IBundledRedstoneProvider; +import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +public final class BundledRedstone +{ + private static final Set providers = new LinkedHashSet<>(); + + private BundledRedstone() {} + + public static synchronized void register( @Nonnull IBundledRedstoneProvider provider ) + { + Objects.requireNonNull( provider, "provider cannot be null" ); + providers.add( provider ); + } + + public static int getDefaultOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + return World.method_24794( pos ) ? DefaultBundledRedstoneProvider.getDefaultBundledRedstoneOutput( world, pos, side ) : -1; + } + + private static int getUnmaskedOutput( World world, BlockPos pos, Direction side ) + { + if( !World.method_24794( pos ) ) return -1; + + // Try the providers in order: + int combinedSignal = -1; + for( IBundledRedstoneProvider bundledRedstoneProvider : providers ) + { + try + { + int signal = bundledRedstoneProvider.getBundledRedstoneOutput( world, pos, side ); + if( signal >= 0 ) + { + combinedSignal = combinedSignal < 0 ? signal & 0xffff : combinedSignal | (signal & 0xffff); + } + } + catch( Exception e ) + { + ComputerCraft.log.error( "Bundled redstone provider " + bundledRedstoneProvider + " errored.", e ); + } + } + + return combinedSignal; + } + + public static int getOutput( World world, BlockPos pos, Direction side ) + { + int signal = getUnmaskedOutput( world, pos, side ); + return signal >= 0 ? signal : 0; + } +} diff --git a/src/main/java/dan200/computercraft/shared/Capabilities.java b/src/main/java/dan200/computercraft/shared/Capabilities.java new file mode 100644 index 000000000..ba820e183 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/Capabilities.java @@ -0,0 +1,25 @@ +/* + * 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; + +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.CapabilityInject; + +public final class Capabilities +{ + @CapabilityInject( IPeripheral.class ) + public static Capability CAPABILITY_PERIPHERAL = null; + + @CapabilityInject( IWiredElement.class ) + public static Capability CAPABILITY_WIRED_ELEMENT = null; + + private Capabilities() + { + } +} diff --git a/src/main/java/dan200/computercraft/shared/Config.java b/src/main/java/dan200/computercraft/shared/Config.java new file mode 100644 index 000000000..193e60521 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/Config.java @@ -0,0 +1,421 @@ +/* + * 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; + +import com.electronwill.nightconfig.core.CommentedConfig; +import com.electronwill.nightconfig.core.UnmodifiableConfig; +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.google.common.base.CaseFormat; +import com.google.common.base.Converter; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.event.TurtleAction; +import dan200.computercraft.core.apis.http.options.Action; +import dan200.computercraft.core.apis.http.options.AddressRuleConfig; +import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.common.ForgeConfigSpec.Builder; +import net.minecraftforge.common.ForgeConfigSpec.ConfigValue; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static net.minecraftforge.common.ForgeConfigSpec.Builder; +import static net.minecraftforge.common.ForgeConfigSpec.ConfigValue; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD ) +public final class Config +{ + private static final int MODEM_MAX_RANGE = 100000; + + private static final String TRANSLATION_PREFIX = "gui.computercraft.config."; + + private static final ConfigValue computerSpaceLimit; + private static final ConfigValue floppySpaceLimit; + private static final ConfigValue maximumFilesOpen; + private static final ConfigValue disableLua51Features; + private static final ConfigValue defaultComputerSettings; + private static final ConfigValue debugEnabled; + private static final ConfigValue logComputerErrors; + private static final ConfigValue commandRequireCreative; + + private static final ConfigValue computerThreads; + private static final ConfigValue maxMainGlobalTime; + private static final ConfigValue maxMainComputerTime; + + private static final ConfigValue httpEnabled; + private static final ConfigValue httpWebsocketEnabled; + private static final ConfigValue> httpRules; + + private static final ConfigValue httpMaxRequests; + private static final ConfigValue httpMaxWebsockets; + + private static final ConfigValue commandBlockEnabled; + private static final ConfigValue modemRange; + private static final ConfigValue modemHighAltitudeRange; + private static final ConfigValue modemRangeDuringStorm; + private static final ConfigValue modemHighAltitudeRangeDuringStorm; + private static final ConfigValue maxNotesPerTick; + private static final ConfigValue monitorBandwidth; + + private static final ConfigValue turtlesNeedFuel; + private static final ConfigValue turtleFuelLimit; + private static final ConfigValue advancedTurtleFuelLimit; + private static final ConfigValue turtlesObeyBlockProtection; + private static final ConfigValue turtlesCanPush; + private static final ConfigValue> turtleDisabledActions; + + private static final ConfigValue computerTermWidth; + private static final ConfigValue computerTermHeight; + + private static final ConfigValue pocketTermWidth; + private static final ConfigValue pocketTermHeight; + + private static final ConfigValue monitorWidth; + private static final ConfigValue monitorHeight; + + private static final ConfigValue genericPeripheral; + + private static final ConfigValue monitorRenderer; + private static final ConfigValue monitorDistance; + + private static final ForgeConfigSpec serverSpec; + private static final ForgeConfigSpec clientSpec; + + private Config() {} + + static + { + Builder builder = new Builder(); + + { // General computers + computerSpaceLimit = builder + .comment( "The disk space limit for computers and turtles, in bytes" ) + .translation( TRANSLATION_PREFIX + "computer_space_limit" ) + .define( "computer_space_limit", ComputerCraft.computerSpaceLimit ); + + floppySpaceLimit = builder + .comment( "The disk space limit for floppy disks, in bytes" ) + .translation( TRANSLATION_PREFIX + "floppy_space_limit" ) + .define( "floppy_space_limit", ComputerCraft.floppySpaceLimit ); + + maximumFilesOpen = builder + .comment( "Set how many files a computer can have open at the same time. Set to 0 for unlimited." ) + .translation( TRANSLATION_PREFIX + "maximum_open_files" ) + .defineInRange( "maximum_open_files", ComputerCraft.maximumFilesOpen, 0, Integer.MAX_VALUE ); + + disableLua51Features = builder + .comment( "Set this to true to disable Lua 5.1 functions that will be removed in a future update. " + + "Useful for ensuring forward compatibility of your programs now." ) + .define( "disable_lua51_features", ComputerCraft.disableLua51Features ); + + defaultComputerSettings = builder + .comment( "A comma separated list of default system settings to set on new computers. Example: " + + "\"shell.autocomplete=false,lua.autocomplete=false,edit.autocomplete=false\" will disable all " + + "autocompletion" ) + .define( "default_computer_settings", ComputerCraft.defaultComputerSettings ); + + debugEnabled = builder + .comment( "Enable Lua's debug library. This is sandboxed to each computer, so is generally safe to be used by players." ) + .define( "debug_enabled", ComputerCraft.debugEnable ); + + logComputerErrors = builder + .comment( "Log exceptions thrown by peripherals and other Lua objects.\n" + + "This makes it easier for mod authors to debug problems, but may result in log spam should people use buggy methods." ) + .define( "log_computer_errors", ComputerCraft.logComputerErrors ); + + commandRequireCreative = builder + .comment( "Require players to be in creative mode and be opped in order to interact with command computers." + + "This is the default behaviour for vanilla's Command blocks." ) + .define( "command_require_creative", ComputerCraft.commandRequireCreative ); + } + + { + builder.comment( "Controls execution behaviour of computers. This is largely intended for fine-tuning " + + "servers, and generally shouldn't need to be touched" ); + builder.push( "execution" ); + + computerThreads = builder + .comment( "Set the number of threads computers can run on. A higher number means more computers can run " + + "at once, but may induce lag.\n" + + "Please note that some mods may not work with a thread count higher than 1. Use with caution." ) + .worldRestart() + .defineInRange( "computer_threads", ComputerCraft.computerThreads, 1, Integer.MAX_VALUE ); + + maxMainGlobalTime = builder + .comment( "The maximum time that can be spent executing tasks in a single tick, in milliseconds.\n" + + "Note, we will quite possibly go over this limit, as there's no way to tell how long a will take " + + "- this aims to be the upper bound of the average time." ) + .defineInRange( "max_main_global_time", (int) TimeUnit.NANOSECONDS.toMillis( ComputerCraft.maxMainGlobalTime ), 1, Integer.MAX_VALUE ); + + maxMainComputerTime = builder + .comment( "The ideal maximum time a computer can execute for in a tick, in milliseconds.\n" + + "Note, we will quite possibly go over this limit, as there's no way to tell how long a will take " + + "- this aims to be the upper bound of the average time." ) + .defineInRange( "max_main_computer_time", (int) TimeUnit.NANOSECONDS.toMillis( ComputerCraft.maxMainComputerTime ), 1, Integer.MAX_VALUE ); + + builder.pop(); + } + + { // HTTP + builder.comment( "Controls the HTTP API" ); + builder.push( "http" ); + + httpEnabled = builder + .comment( "Enable the \"http\" API on Computers (see \"rules\" for more fine grained control than this)." ) + .define( "enabled", ComputerCraft.httpEnabled ); + + httpWebsocketEnabled = builder + .comment( "Enable use of http websockets. This requires the \"http_enable\" option to also be true." ) + .define( "websocket_enabled", ComputerCraft.httpWebsocketEnabled ); + + httpRules = builder + .comment( "A list of rules which control behaviour of the \"http\" API for specific domains or IPs.\n" + + "Each rule is an item with a 'host' to match against, and a series of properties. " + + "The host may be a domain name (\"pastebin.com\"),\n" + + "wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\"). If no rules, the domain is blocked." ) + .defineList( "rules", + Stream.concat( + Stream.of( ComputerCraft.DEFAULT_HTTP_DENY ).map( x -> AddressRuleConfig.makeRule( x, Action.DENY ) ), + Stream.of( ComputerCraft.DEFAULT_HTTP_ALLOW ).map( x -> AddressRuleConfig.makeRule( x, Action.ALLOW ) ) + ).collect( Collectors.toList() ), + x -> x instanceof UnmodifiableConfig && AddressRuleConfig.checkRule( (UnmodifiableConfig) x ) ); + + httpMaxRequests = builder + .comment( "The number of http requests a computer can make at one time. Additional requests will be queued, and sent when the running requests have finished. Set to 0 for unlimited." ) + .defineInRange( "max_requests", ComputerCraft.httpMaxRequests, 0, Integer.MAX_VALUE ); + + httpMaxWebsockets = builder + .comment( "The number of websockets a computer can have open at one time. Set to 0 for unlimited." ) + .defineInRange( "max_websockets", ComputerCraft.httpMaxWebsockets, 1, Integer.MAX_VALUE ); + + builder.pop(); + } + + { // Peripherals + builder.comment( "Various options relating to peripherals." ); + builder.push( "peripheral" ); + + commandBlockEnabled = builder + .comment( "Enable Command Block peripheral support" ) + .define( "command_block_enabled", ComputerCraft.enableCommandBlock ); + + modemRange = builder + .comment( "The range of Wireless Modems at low altitude in clear weather, in meters" ) + .defineInRange( "modem_range", ComputerCraft.modemRange, 0, MODEM_MAX_RANGE ); + + modemHighAltitudeRange = builder + .comment( "The range of Wireless Modems at maximum altitude in clear weather, in meters" ) + .defineInRange( "modem_high_altitude_range", ComputerCraft.modemHighAltitudeRange, 0, MODEM_MAX_RANGE ); + + modemRangeDuringStorm = builder + .comment( "The range of Wireless Modems at low altitude in stormy weather, in meters" ) + .defineInRange( "modem_range_during_storm", ComputerCraft.modemRangeDuringStorm, 0, MODEM_MAX_RANGE ); + + modemHighAltitudeRangeDuringStorm = builder + .comment( "The range of Wireless Modems at maximum altitude in stormy weather, in meters" ) + .defineInRange( "modem_high_altitude_range_during_storm", ComputerCraft.modemHighAltitudeRangeDuringStorm, 0, MODEM_MAX_RANGE ); + + maxNotesPerTick = builder + .comment( "Maximum amount of notes a speaker can play at once" ) + .defineInRange( "max_notes_per_tick", ComputerCraft.maxNotesPerTick, 1, Integer.MAX_VALUE ); + + monitorBandwidth = builder + .comment( "The limit to how much monitor data can be sent *per tick*. Note:\n" + + " - Bandwidth is measured before compression, so the data sent to the client is smaller.\n" + + " - This ignores the number of players a packet is sent to. Updating a monitor for one player consumes " + + "the same bandwidth limit as sending to 20.\n" + + " - A full sized monitor sends ~25kb of data. So the default (1MB) allows for ~40 monitors to be updated " + + "in a single tick. \n" + + "Set to 0 to disable." ) + .defineInRange( "monitor_bandwidth", (int) ComputerCraft.monitorBandwidth, 0, Integer.MAX_VALUE ); + + builder.pop(); + } + + { // Turtles + builder.comment( "Various options relating to turtles." ); + builder.push( "turtle" ); + + turtlesNeedFuel = builder + .comment( "Set whether Turtles require fuel to move" ) + .define( "need_fuel", ComputerCraft.turtlesNeedFuel ); + + turtleFuelLimit = builder + .comment( "The fuel limit for Turtles" ) + .defineInRange( "normal_fuel_limit", ComputerCraft.turtleFuelLimit, 0, Integer.MAX_VALUE ); + + advancedTurtleFuelLimit = builder + .comment( "The fuel limit for Advanced Turtles" ) + .defineInRange( "advanced_fuel_limit", ComputerCraft.advancedTurtleFuelLimit, 0, Integer.MAX_VALUE ); + + turtlesObeyBlockProtection = builder + .comment( "If set to true, Turtles will be unable to build, dig, or enter protected areas (such as near the server spawn point)" ) + .define( "obey_block_protection", ComputerCraft.turtlesObeyBlockProtection ); + + turtlesCanPush = builder + .comment( "If set to true, Turtles will push entities out of the way instead of stopping if there is space to do so" ) + .define( "can_push", ComputerCraft.turtlesCanPush ); + + turtleDisabledActions = builder + .comment( "A list of turtle actions which are disabled." ) + .defineList( "disabled_actions", Collections.emptyList(), x -> x instanceof String && getAction( (String) x ) != null ); + + builder.pop(); + } + + { + builder.comment( "Configure the size of various computer's terminals.\n" + + "Larger terminals require more bandwidth, so use with care." ).push( "term_sizes" ); + + builder.comment( "Terminal size of computers" ).push( "computer" ); + computerTermWidth = builder.defineInRange( "width", ComputerCraft.computerTermWidth, 1, 255 ); + computerTermHeight = builder.defineInRange( "height", ComputerCraft.computerTermHeight, 1, 255 ); + builder.pop(); + + builder.comment( "Terminal size of pocket computers" ).push( "pocket_computer" ); + pocketTermWidth = builder.defineInRange( "width", ComputerCraft.pocketTermWidth, 1, 255 ); + pocketTermHeight = builder.defineInRange( "height", ComputerCraft.pocketTermHeight, 1, 255 ); + builder.pop(); + + builder.comment( "Maximum size of monitors (in blocks)" ).push( "monitor" ); + monitorWidth = builder.defineInRange( "width", ComputerCraft.monitorWidth, 1, 32 ); + monitorHeight = builder.defineInRange( "height", ComputerCraft.monitorHeight, 1, 32 ); + builder.pop(); + + 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(); + monitorRenderer = clientBuilder + .comment( "The renderer to use for monitors. Generally this should be kept at \"best\" - if " + + "monitors have performance issues, you may wish to experiment with alternative renderers." ) + .defineEnum( "monitor_renderer", MonitorRenderer.BEST ); + monitorDistance = clientBuilder + .comment( "The maximum distance monitors will render at. This defaults to the standard tile entity limit, " + + "but may be extended if you wish to build larger monitors." ) + .defineInRange( "monitor_distance", 64, 16, 1024 ); + clientSpec = clientBuilder.build(); + } + + public static void setup() + { + ModLoadingContext.get().registerConfig( ModConfig.Type.SERVER, serverSpec ); + ModLoadingContext.get().registerConfig( ModConfig.Type.CLIENT, clientSpec ); + } + + public static void sync() + { + // General + ComputerCraft.computerSpaceLimit = computerSpaceLimit.get(); + ComputerCraft.floppySpaceLimit = floppySpaceLimit.get(); + ComputerCraft.maximumFilesOpen = maximumFilesOpen.get(); + ComputerCraft.disableLua51Features = disableLua51Features.get(); + ComputerCraft.defaultComputerSettings = defaultComputerSettings.get(); + ComputerCraft.debugEnable = debugEnabled.get(); + ComputerCraft.computerThreads = computerThreads.get(); + ComputerCraft.logComputerErrors = logComputerErrors.get(); + ComputerCraft.commandRequireCreative = commandRequireCreative.get(); + + // Execution + ComputerCraft.computerThreads = computerThreads.get(); + ComputerCraft.maxMainGlobalTime = TimeUnit.MILLISECONDS.toNanos( maxMainGlobalTime.get() ); + ComputerCraft.maxMainComputerTime = TimeUnit.MILLISECONDS.toNanos( maxMainComputerTime.get() ); + + // HTTP + ComputerCraft.httpEnabled = httpEnabled.get(); + ComputerCraft.httpWebsocketEnabled = httpWebsocketEnabled.get(); + ComputerCraft.httpRules = Collections.unmodifiableList( httpRules.get().stream() + .map( AddressRuleConfig::parseRule ).filter( Objects::nonNull ).collect( Collectors.toList() ) ); + + ComputerCraft.httpMaxRequests = httpMaxRequests.get(); + ComputerCraft.httpMaxWebsockets = httpMaxWebsockets.get(); + + // Peripheral + ComputerCraft.enableCommandBlock = commandBlockEnabled.get(); + ComputerCraft.maxNotesPerTick = maxNotesPerTick.get(); + ComputerCraft.modemRange = modemRange.get(); + ComputerCraft.modemHighAltitudeRange = modemHighAltitudeRange.get(); + ComputerCraft.modemRangeDuringStorm = modemRangeDuringStorm.get(); + ComputerCraft.modemHighAltitudeRangeDuringStorm = modemHighAltitudeRangeDuringStorm.get(); + ComputerCraft.monitorBandwidth = monitorBandwidth.get(); + + // Turtles + ComputerCraft.turtlesNeedFuel = turtlesNeedFuel.get(); + ComputerCraft.turtleFuelLimit = turtleFuelLimit.get(); + ComputerCraft.advancedTurtleFuelLimit = advancedTurtleFuelLimit.get(); + ComputerCraft.turtlesObeyBlockProtection = turtlesObeyBlockProtection.get(); + ComputerCraft.turtlesCanPush = turtlesCanPush.get(); + + ComputerCraft.turtleDisabledActions.clear(); + for( String value : turtleDisabledActions.get() ) ComputerCraft.turtleDisabledActions.add( getAction( value ) ); + + // Terminal size + ComputerCraft.computerTermWidth = computerTermWidth.get(); + ComputerCraft.computerTermHeight = computerTermHeight.get(); + ComputerCraft.pocketTermWidth = pocketTermWidth.get(); + ComputerCraft.pocketTermHeight = pocketTermHeight.get(); + ComputerCraft.monitorWidth = monitorWidth.get(); + ComputerCraft.monitorHeight = monitorHeight.get(); + + // Experimental + ComputerCraft.genericPeripheral = genericPeripheral.get(); + + // Client + ComputerCraft.monitorRenderer = monitorRenderer.get(); + ComputerCraft.monitorDistanceSq = monitorDistance.get() * monitorDistance.get(); + } + + @SubscribeEvent + public static void sync( ModConfig.Loading event ) + { + sync(); + } + + @SubscribeEvent + public static void sync( ModConfig.Reloading event ) + { + // Ensure file configs are reloaded. Forge should probably do this, so worth checking in the future. + CommentedConfig config = event.getConfig().getConfigData(); + if( config instanceof CommentedFileConfig ) ((CommentedFileConfig) config).load(); + + sync(); + } + + private static final Converter converter = CaseFormat.LOWER_CAMEL.converterTo( CaseFormat.UPPER_UNDERSCORE ); + + private static TurtleAction getAction( String value ) + { + try + { + return TurtleAction.valueOf( converter.convert( value ) ); + } + catch( IllegalArgumentException e ) + { + return null; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/MediaProviders.java b/src/main/java/dan200/computercraft/shared/MediaProviders.java new file mode 100644 index 000000000..2ef132e52 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/MediaProviders.java @@ -0,0 +1,50 @@ +/* + * 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; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.api.media.IMediaProvider; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +public final class MediaProviders +{ + private static final Set providers = new LinkedHashSet<>(); + + private MediaProviders() {} + + public static synchronized void register( @Nonnull IMediaProvider provider ) + { + Objects.requireNonNull( provider, "provider cannot be null" ); + providers.add( provider ); + } + + public static IMedia get( @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return null; + + // Try the handlers in order: + for( IMediaProvider mediaProvider : providers ) + { + try + { + IMedia media = mediaProvider.getMedia( stack ); + if( media != null ) return media; + } + catch( Exception e ) + { + // mod misbehaved, ignore it + ComputerCraft.log.error( "Media provider " + mediaProvider + " errored.", e ); + } + } + return null; + } +} diff --git a/src/main/java/dan200/computercraft/shared/Peripherals.java b/src/main/java/dan200/computercraft/shared/Peripherals.java new file mode 100644 index 000000000..9081fc35d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/Peripherals.java @@ -0,0 +1,73 @@ +/* + * 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; + +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.block.entity.BlockEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.common.util.NonNullConsumer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; + +public final class Peripherals +{ + private static final Collection providers = new LinkedHashSet<>(); + + private Peripherals() {} + + public static synchronized void register( @Nonnull IPeripheralProvider provider ) + { + Objects.requireNonNull( provider, "provider cannot be null" ); + providers.add( provider ); + } + + @Nullable + public static IPeripheral getPeripheral( World world, BlockPos pos, Direction side, NonNullConsumer> invalidate ) + { + return World.method_24794( pos ) && !world.isClient ? getPeripheralAt( world, pos, side, invalidate ) : null; + } + + @Nullable + private static IPeripheral getPeripheralAt( World world, BlockPos pos, Direction side, NonNullConsumer> invalidate ) + { + BlockEntity block = world.getBlockEntity( pos ); + if( block != null ) + { + LazyOptional peripheral = block.getCapability( CAPABILITY_PERIPHERAL, side ); + if( peripheral.isPresent() ) return CapabilityUtil.unwrap( peripheral, invalidate ); + } + + // Try the handlers in order: + for( IPeripheralProvider peripheralProvider : providers ) + { + try + { + LazyOptional peripheral = peripheralProvider.getPeripheral( world, pos, side ); + if( peripheral.isPresent() ) return CapabilityUtil.unwrap( peripheral, invalidate ); + } + catch( Exception e ) + { + ComputerCraft.log.error( "Peripheral provider " + peripheralProvider + " errored.", e ); + } + } + + return CapabilityUtil.unwrap( GenericPeripheralProvider.getPeripheral( world, pos, side ), invalidate ); + } + +} diff --git a/src/main/java/dan200/computercraft/shared/PocketUpgrades.java b/src/main/java/dan200/computercraft/shared/PocketUpgrades.java new file mode 100644 index 000000000..f46a87820 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/PocketUpgrades.java @@ -0,0 +1,86 @@ +/* + * 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; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.shared.util.InventoryUtil; +import net.minecraft.item.ItemStack; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModLoadingContext; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; + +public final class PocketUpgrades +{ + private static final Map upgrades = new HashMap<>(); + private static final IdentityHashMap upgradeOwners = new IdentityHashMap<>(); + + private PocketUpgrades() {} + + public static synchronized void register( @Nonnull IPocketUpgrade upgrade ) + { + Objects.requireNonNull( upgrade, "upgrade cannot be null" ); + + String id = upgrade.getUpgradeID().toString(); + IPocketUpgrade existing = upgrades.get( id ); + if( existing != null ) + { + throw new IllegalStateException( "Error registering '" + upgrade.getUnlocalisedAdjective() + " pocket computer'. UpgradeID '" + id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " pocket computer'" ); + } + + upgrades.put( id, upgrade ); + + ModContainer mc = ModLoadingContext.get().getActiveContainer(); + if( mc != null && mc.getModId() != null ) upgradeOwners.put( upgrade, mc.getModId() ); + } + + public static IPocketUpgrade get( String id ) + { + // Fix a typo in the advanced modem upgrade's name. I'm sorry, I realise this is horrible. + if( id.equals( "computercraft:advanved_modem" ) ) id = "computercraft:advanced_modem"; + + return upgrades.get( id ); + } + + public static IPocketUpgrade get( @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return null; + + for( IPocketUpgrade upgrade : upgrades.values() ) + { + ItemStack craftingStack = upgrade.getCraftingItem(); + if( !craftingStack.isEmpty() && InventoryUtil.areItemsSimilar( stack, craftingStack ) ) + { + return upgrade; + } + } + + return null; + } + + @Nullable + public static String getOwner( IPocketUpgrade upgrade ) + { + return upgradeOwners.get( upgrade ); + } + + public static Iterable getVanillaUpgrades() + { + List vanilla = new ArrayList<>(); + vanilla.add( ComputerCraft.PocketUpgrades.wirelessModemNormal ); + vanilla.add( ComputerCraft.PocketUpgrades.wirelessModemAdvanced ); + vanilla.add( ComputerCraft.PocketUpgrades.speaker ); + return vanilla; + } + + public static Iterable getUpgrades() + { + return Collections.unmodifiableCollection( upgrades.values() ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/Registry.java b/src/main/java/dan200/computercraft/shared/Registry.java new file mode 100644 index 000000000..7c5564c1c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/Registry.java @@ -0,0 +1,345 @@ +/* + * 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; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.shared.common.ColourableRecipe; +import dan200.computercraft.shared.common.ContainerHeldItem; +import dan200.computercraft.shared.computer.blocks.BlockComputer; +import dan200.computercraft.shared.computer.blocks.TileCommandComputer; +import dan200.computercraft.shared.computer.blocks.TileComputer; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.inventory.ContainerComputer; +import dan200.computercraft.shared.computer.inventory.ContainerViewComputer; +import dan200.computercraft.shared.computer.items.ItemComputer; +import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe; +import dan200.computercraft.shared.media.items.ItemDisk; +import dan200.computercraft.shared.media.items.ItemPrintout; +import dan200.computercraft.shared.media.items.ItemTreasureDisk; +import dan200.computercraft.shared.media.recipes.DiskRecipe; +import dan200.computercraft.shared.media.recipes.PrintoutRecipe; +import dan200.computercraft.shared.network.container.ComputerContainerData; +import dan200.computercraft.shared.network.container.ContainerData; +import dan200.computercraft.shared.network.container.HeldItemContainerData; +import dan200.computercraft.shared.network.container.ViewComputerContainerData; +import dan200.computercraft.shared.peripheral.diskdrive.BlockDiskDrive; +import dan200.computercraft.shared.peripheral.diskdrive.ContainerDiskDrive; +import dan200.computercraft.shared.peripheral.diskdrive.TileDiskDrive; +import dan200.computercraft.shared.peripheral.modem.wired.*; +import dan200.computercraft.shared.peripheral.modem.wireless.BlockWirelessModem; +import dan200.computercraft.shared.peripheral.modem.wireless.TileWirelessModem; +import dan200.computercraft.shared.peripheral.monitor.BlockMonitor; +import dan200.computercraft.shared.peripheral.monitor.TileMonitor; +import dan200.computercraft.shared.peripheral.printer.BlockPrinter; +import dan200.computercraft.shared.peripheral.printer.ContainerPrinter; +import dan200.computercraft.shared.peripheral.printer.TilePrinter; +import dan200.computercraft.shared.peripheral.speaker.BlockSpeaker; +import dan200.computercraft.shared.peripheral.speaker.TileSpeaker; +import dan200.computercraft.shared.pocket.inventory.ContainerPocketComputer; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import dan200.computercraft.shared.pocket.peripherals.PocketModem; +import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker; +import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe; +import dan200.computercraft.shared.turtle.blocks.BlockTurtle; +import dan200.computercraft.shared.turtle.blocks.TileTurtle; +import dan200.computercraft.shared.turtle.core.TurtlePlayer; +import dan200.computercraft.shared.turtle.inventory.ContainerTurtle; +import dan200.computercraft.shared.turtle.items.ItemTurtle; +import dan200.computercraft.shared.turtle.recipes.TurtleRecipe; +import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe; +import dan200.computercraft.shared.turtle.upgrades.*; +import dan200.computercraft.shared.util.CreativeTabMain; +import dan200.computercraft.shared.util.FixedPointTileEntityType; +import dan200.computercraft.shared.util.ImpostorRecipe; +import dan200.computercraft.shared.util.ImpostorShapelessRecipe; +import net.minecraft.block.Block; +import net.minecraft.block.Material; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.SpawnGroup; +import net.minecraft.item.BlockItem; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.Items; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.util.Identifier; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.RegistryObject; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; + +import java.util.function.BiFunction; +import java.util.function.Function; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD ) +public final class Registry +{ + private static final ItemGroup mainItemGroup = new CreativeTabMain(); + + private Registry() + { + } + + public static final class ModBlocks + { + static final DeferredRegister BLOCKS = DeferredRegister.create( ForgeRegistries.BLOCKS, ComputerCraft.MOD_ID ); + + private static Block.Properties properties() + { + return Block.Properties.of( Material.STONE ).strength( 2 ); + } + + private static Block.Properties turtleProperties() + { + return Block.Properties.of( Material.STONE ).strength( 2.5f ); + } + + private static Block.Properties modemProperties() + { + return Block.Properties.of( Material.STONE ).strength( 1.5f ); + } + + public static final RegistryObject COMPUTER_NORMAL = BLOCKS.register( "computer_normal", + () -> new BlockComputer( properties(), ComputerFamily.NORMAL, ModTiles.COMPUTER_NORMAL ) ); + public static final RegistryObject COMPUTER_ADVANCED = BLOCKS.register( "computer_advanced", + () -> new BlockComputer( properties(), ComputerFamily.ADVANCED, ModTiles.COMPUTER_ADVANCED ) ); + + public static final RegistryObject COMPUTER_COMMAND = BLOCKS.register( "computer_command", () -> new BlockComputer( + Block.Properties.of( Material.STONE ).strength( -1, 6000000.0F ), + ComputerFamily.COMMAND, ModTiles.COMPUTER_COMMAND + ) ); + + public static final RegistryObject TURTLE_NORMAL = BLOCKS.register( "turtle_normal", + () -> new BlockTurtle( turtleProperties(), ComputerFamily.NORMAL, ModTiles.TURTLE_NORMAL ) ); + public static final RegistryObject TURTLE_ADVANCED = BLOCKS.register( "turtle_advanced", + () -> new BlockTurtle( turtleProperties(), ComputerFamily.ADVANCED, ModTiles.TURTLE_ADVANCED ) ); + + public static final RegistryObject SPEAKER = BLOCKS.register( "speaker", () -> new BlockSpeaker( properties() ) ); + public static final RegistryObject DISK_DRIVE = BLOCKS.register( "disk_drive", () -> new BlockDiskDrive( properties() ) ); + public static final RegistryObject PRINTER = BLOCKS.register( "printer", () -> new BlockPrinter( properties() ) ); + + public static final RegistryObject MONITOR_NORMAL = BLOCKS.register( "monitor_normal", + () -> new BlockMonitor( properties(), ModTiles.MONITOR_NORMAL ) ); + public static final RegistryObject MONITOR_ADVANCED = BLOCKS.register( "monitor_advanced", + () -> new BlockMonitor( properties(), ModTiles.MONITOR_ADVANCED ) ); + + public static final RegistryObject WIRELESS_MODEM_NORMAL = BLOCKS.register( "wireless_modem_normal", + () -> new BlockWirelessModem( properties(), ModTiles.WIRELESS_MODEM_NORMAL ) ); + public static final RegistryObject WIRELESS_MODEM_ADVANCED = BLOCKS.register( "wireless_modem_advanced", + () -> new BlockWirelessModem( properties(), ModTiles.WIRELESS_MODEM_ADVANCED ) ); + + public static final RegistryObject WIRED_MODEM_FULL = BLOCKS.register( "wired_modem_full", + () -> new BlockWiredModemFull( modemProperties() ) ); + public static final RegistryObject CABLE = BLOCKS.register( "cable", () -> new BlockCable( modemProperties() ) ); + } + + public static class ModTiles + { + static final DeferredRegister> TILES = DeferredRegister.create( ForgeRegistries.TILE_ENTITIES, ComputerCraft.MOD_ID ); + + private static RegistryObject> ofBlock( RegistryObject block, Function, T> factory ) + { + return TILES.register( block.getId().getPath(), () -> FixedPointTileEntityType.create( block, factory ) ); + } + + public static final RegistryObject> MONITOR_NORMAL = + ofBlock( ModBlocks.MONITOR_NORMAL, f -> new TileMonitor( f, false ) ); + public static final RegistryObject> MONITOR_ADVANCED = + ofBlock( ModBlocks.MONITOR_ADVANCED, f -> new TileMonitor( f, true ) ); + + public static final RegistryObject> COMPUTER_NORMAL = + ofBlock( ModBlocks.COMPUTER_NORMAL, f -> new TileComputer( ComputerFamily.NORMAL, f ) ); + public static final RegistryObject> COMPUTER_ADVANCED = + ofBlock( ModBlocks.COMPUTER_ADVANCED, f -> new TileComputer( ComputerFamily.ADVANCED, f ) ); + public static final RegistryObject> COMPUTER_COMMAND = + ofBlock( ModBlocks.COMPUTER_COMMAND, f -> new TileCommandComputer( ComputerFamily.COMMAND, f ) ); + + public static final RegistryObject> TURTLE_NORMAL = + ofBlock( ModBlocks.TURTLE_NORMAL, f -> new TileTurtle( f, ComputerFamily.NORMAL ) ); + public static final RegistryObject> TURTLE_ADVANCED = + ofBlock( ModBlocks.TURTLE_ADVANCED, f -> new TileTurtle( f, ComputerFamily.ADVANCED ) ); + + public static final RegistryObject> SPEAKER = ofBlock( ModBlocks.SPEAKER, TileSpeaker::new ); + public static final RegistryObject> DISK_DRIVE = ofBlock( ModBlocks.DISK_DRIVE, TileDiskDrive::new ); + public static final RegistryObject> PRINTER = ofBlock( ModBlocks.PRINTER, TilePrinter::new ); + public static final RegistryObject> WIRED_MODEM_FULL = ofBlock( ModBlocks.WIRED_MODEM_FULL, TileWiredModemFull::new ); + public static final RegistryObject> CABLE = ofBlock( ModBlocks.CABLE, TileCable::new ); + + public static final RegistryObject> WIRELESS_MODEM_NORMAL = + ofBlock( ModBlocks.WIRELESS_MODEM_NORMAL, f -> new TileWirelessModem( f, false ) ); + public static final RegistryObject> WIRELESS_MODEM_ADVANCED = + ofBlock( ModBlocks.WIRELESS_MODEM_ADVANCED, f -> new TileWirelessModem( f, true ) ); + } + + public static final class ModItems + { + static final DeferredRegister ITEMS = DeferredRegister.create( ForgeRegistries.ITEMS, ComputerCraft.MOD_ID ); + + private static Item.Settings properties() + { + return new Item.Settings().group( mainItemGroup ); + } + + private static RegistryObject ofBlock( RegistryObject parent, BiFunction supplier ) + { + return ITEMS.register( parent.getId().getPath(), () -> supplier.apply( parent.get(), properties() ) ); + } + + public static final RegistryObject COMPUTER_NORMAL = ofBlock( ModBlocks.COMPUTER_NORMAL, ItemComputer::new ); + public static final RegistryObject COMPUTER_ADVANCED = ofBlock( ModBlocks.COMPUTER_ADVANCED, ItemComputer::new ); + public static final RegistryObject COMPUTER_COMMAND = ofBlock( ModBlocks.COMPUTER_COMMAND, ItemComputer::new ); + + public static final RegistryObject POCKET_COMPUTER_NORMAL = ITEMS.register( "pocket_computer_normal", + () -> new ItemPocketComputer( properties().maxCount( 1 ), ComputerFamily.NORMAL ) ); + public static final RegistryObject POCKET_COMPUTER_ADVANCED = ITEMS.register( "pocket_computer_advanced", + () -> new ItemPocketComputer( properties().maxCount( 1 ), ComputerFamily.ADVANCED ) ); + + public static final RegistryObject TURTLE_NORMAL = ofBlock( ModBlocks.TURTLE_NORMAL, ItemTurtle::new ); + public static final RegistryObject TURTLE_ADVANCED = ofBlock( ModBlocks.TURTLE_ADVANCED, ItemTurtle::new ); + + public static final RegistryObject DISK = + ITEMS.register( "disk", () -> new ItemDisk( properties().maxCount( 1 ) ) ); + public static final RegistryObject TREASURE_DISK = + ITEMS.register( "treasure_disk", () -> new ItemTreasureDisk( properties().maxCount( 1 ) ) ); + + public static final RegistryObject PRINTED_PAGE = ITEMS.register( "printed_page", + () -> new ItemPrintout( properties().maxCount( 1 ), ItemPrintout.Type.PAGE ) ); + public static final RegistryObject PRINTED_PAGES = ITEMS.register( "printed_pages", + () -> new ItemPrintout( properties().maxCount( 1 ), ItemPrintout.Type.PAGES ) ); + public static final RegistryObject PRINTED_BOOK = ITEMS.register( "printed_book", + () -> new ItemPrintout( properties().maxCount( 1 ), ItemPrintout.Type.BOOK ) ); + + public static final RegistryObject SPEAKER = ofBlock( ModBlocks.SPEAKER, BlockItem::new ); + public static final RegistryObject DISK_DRIVE = ofBlock( ModBlocks.DISK_DRIVE, BlockItem::new ); + public static final RegistryObject PRINTER = ofBlock( ModBlocks.PRINTER, BlockItem::new ); + public static final RegistryObject MONITOR_NORMAL = ofBlock( ModBlocks.MONITOR_NORMAL, BlockItem::new ); + public static final RegistryObject MONITOR_ADVANCED = ofBlock( ModBlocks.MONITOR_ADVANCED, BlockItem::new ); + public static final RegistryObject WIRELESS_MODEM_NORMAL = ofBlock( ModBlocks.WIRELESS_MODEM_NORMAL, BlockItem::new ); + public static final RegistryObject WIRELESS_MODEM_ADVANCED = ofBlock( ModBlocks.WIRELESS_MODEM_ADVANCED, BlockItem::new ); + public static final RegistryObject WIRED_MODEM_FULL = ofBlock( ModBlocks.WIRED_MODEM_FULL, BlockItem::new ); + + public static final RegistryObject CABLE = ITEMS.register( "cable", + () -> new ItemBlockCable.Cable( ModBlocks.CABLE.get(), properties() ) ); + public static final RegistryObject WIRED_MODEM = ITEMS.register( "wired_modem", + () -> new ItemBlockCable.WiredModem( ModBlocks.CABLE.get(), properties() ) ); + } + + @SubscribeEvent + public static void registerItems( RegistryEvent.Register event ) + { + registerTurtleUpgrades(); + registerPocketUpgrades(); + } + + private static void registerTurtleUpgrades() + { + // Upgrades + ComputerCraft.TurtleUpgrades.wirelessModemNormal = new TurtleModem( false, new Identifier( ComputerCraft.MOD_ID, "wireless_modem_normal" ) ); + TurtleUpgrades.register( ComputerCraft.TurtleUpgrades.wirelessModemNormal ); + + ComputerCraft.TurtleUpgrades.wirelessModemAdvanced = new TurtleModem( true, new Identifier( ComputerCraft.MOD_ID, "wireless_modem_advanced" ) ); + ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.wirelessModemAdvanced ); + + ComputerCraft.TurtleUpgrades.speaker = new TurtleSpeaker( new Identifier( ComputerCraft.MOD_ID, "speaker" ) ); + ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.speaker ); + + ComputerCraft.TurtleUpgrades.craftingTable = new TurtleCraftingTable( new Identifier( "minecraft", "crafting_table" ) ); + ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.craftingTable ); + + ComputerCraft.TurtleUpgrades.diamondSword = new TurtleSword( new Identifier( "minecraft", "diamond_sword" ), Items.DIAMOND_SWORD ); + ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.diamondSword ); + + ComputerCraft.TurtleUpgrades.diamondShovel = new TurtleShovel( new Identifier( "minecraft", "diamond_shovel" ), Items.DIAMOND_SHOVEL ); + ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.diamondShovel ); + + ComputerCraft.TurtleUpgrades.diamondPickaxe = new TurtleTool( new Identifier( "minecraft", "diamond_pickaxe" ), Items.DIAMOND_PICKAXE ); + ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.diamondPickaxe ); + + ComputerCraft.TurtleUpgrades.diamondAxe = new TurtleAxe( new Identifier( "minecraft", "diamond_axe" ), Items.DIAMOND_AXE ); + ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.diamondAxe ); + + ComputerCraft.TurtleUpgrades.diamondHoe = new TurtleHoe( new Identifier( "minecraft", "diamond_hoe" ), Items.DIAMOND_HOE ); + ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.diamondHoe ); + } + + private static void registerPocketUpgrades() + { + ComputerCraftAPI.registerPocketUpgrade( ComputerCraft.PocketUpgrades.wirelessModemNormal = new PocketModem( false ) ); + ComputerCraftAPI.registerPocketUpgrade( ComputerCraft.PocketUpgrades.wirelessModemAdvanced = new PocketModem( true ) ); + ComputerCraftAPI.registerPocketUpgrade( ComputerCraft.PocketUpgrades.speaker = new PocketSpeaker() ); + } + + public static class ModEntities + { + static final DeferredRegister> ENTITIES = DeferredRegister.create( ForgeRegistries.ENTITIES, ComputerCraft.MOD_ID ); + + public static final RegistryObject> TURTLE_PLAYER = ENTITIES.register( "turtle_player", () -> + EntityType.Builder.create( SpawnGroup.MISC ) + .disableSaving() + .disableSummon() + .setDimensions( 0, 0 ) + .build( ComputerCraft.MOD_ID + ":turtle_player" ) ); + } + + public static class ModContainers + { + static final DeferredRegister> CONTAINERS = DeferredRegister.create( ForgeRegistries.CONTAINERS, ComputerCraft.MOD_ID ); + + public static final RegistryObject> COMPUTER = CONTAINERS.register( "computer", + () -> ContainerData.toType( ComputerContainerData::new, ContainerComputer::new ) ); + + public static final RegistryObject> POCKET_COMPUTER = CONTAINERS.register( "pocket_computer", + () -> ContainerData.toType( ComputerContainerData::new, ContainerPocketComputer::new ) ); + + public static final RegistryObject> TURTLE = CONTAINERS.register( "turtle", + () -> ContainerData.toType( ComputerContainerData::new, ContainerTurtle::new ) ); + + public static final RegistryObject> DISK_DRIVE = CONTAINERS.register( "disk_drive", + () -> new ScreenHandlerType<>( ContainerDiskDrive::new ) ); + + public static final RegistryObject> PRINTER = CONTAINERS.register( "printer", + () -> new ScreenHandlerType<>( ContainerPrinter::new ) ); + + public static final RegistryObject> PRINTOUT = CONTAINERS.register( "printout", + () -> ContainerData.toType( HeldItemContainerData::new, ContainerHeldItem::createPrintout ) ); + + public static final RegistryObject> VIEW_COMPUTER = CONTAINERS.register( "view_computer", + () -> ContainerData.toType( ViewComputerContainerData::new, ContainerViewComputer::new ) ); + } + + @SubscribeEvent + public static void registerRecipeSerializers( RegistryEvent.Register> event ) + { + event.getRegistry().registerAll( + ColourableRecipe.SERIALIZER.setRegistryName( new Identifier( ComputerCraft.MOD_ID, "colour" ) ), + ComputerUpgradeRecipe.SERIALIZER.setRegistryName( new Identifier( ComputerCraft.MOD_ID, "computer_upgrade" ) ), + PocketComputerUpgradeRecipe.SERIALIZER.setRegistryName( new Identifier( ComputerCraft.MOD_ID, "pocket_computer_upgrade" ) ), + DiskRecipe.SERIALIZER.setRegistryName( new Identifier( ComputerCraft.MOD_ID, "disk" ) ), + PrintoutRecipe.SERIALIZER.setRegistryName( new Identifier( ComputerCraft.MOD_ID, "printout" ) ), + TurtleRecipe.SERIALIZER.setRegistryName( new Identifier( ComputerCraft.MOD_ID, "turtle" ) ), + TurtleUpgradeRecipe.SERIALIZER.setRegistryName( new Identifier( ComputerCraft.MOD_ID, "turtle_upgrade" ) ), + ImpostorShapelessRecipe.SERIALIZER.setRegistryName( new Identifier( ComputerCraft.MOD_ID, "impostor_shapeless" ) ), + ImpostorRecipe.SERIALIZER.setRegistryName( new Identifier( ComputerCraft.MOD_ID, "impostor_shaped" ) ) + ); + } + + public static void setup() + { + IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus(); + ModBlocks.BLOCKS.register( bus ); + ModTiles.TILES.register( bus ); + ModItems.ITEMS.register( bus ); + ModEntities.ENTITIES.register( bus ); + ModContainers.CONTAINERS.register( bus ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/TurtlePermissions.java b/src/main/java/dan200/computercraft/shared/TurtlePermissions.java new file mode 100644 index 000000000..f7d448048 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/TurtlePermissions.java @@ -0,0 +1,40 @@ +/* + * 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; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.event.TurtleActionEvent; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID ) +public final class TurtlePermissions +{ + public static boolean isBlockEnterable( World world, BlockPos pos, PlayerEntity player ) + { + MinecraftServer server = world.getServer(); + return server == null || world.isClient || (world instanceof ServerWorld && !server.isSpawnProtected( (ServerWorld) world, pos, player )); + } + + public static boolean isBlockEditable( World world, BlockPos pos, PlayerEntity player ) + { + return isBlockEnterable( world, pos, player ); + } + + @SubscribeEvent + public static void onTurtleAction( TurtleActionEvent event ) + { + if( ComputerCraft.turtleDisabledActions.contains( event.getAction() ) ) + { + event.setCanceled( true, "Action has been disabled" ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java b/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java new file mode 100644 index 000000000..d9e4bc30f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java @@ -0,0 +1,177 @@ +/* + * 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; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.util.InventoryUtil; +import net.minecraft.item.ItemStack; +import net.minecraftforge.fml.ModLoadingContext; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Stream; + +public final class TurtleUpgrades +{ + private static class Wrapper + { + final ITurtleUpgrade upgrade; + final String id; + final String modId; + boolean enabled; + + Wrapper( ITurtleUpgrade upgrade ) + { + this.upgrade = upgrade; + this.id = upgrade.getUpgradeID().toString(); + this.modId = ModLoadingContext.get().getActiveNamespace(); + this.enabled = true; + } + } + + private static ITurtleUpgrade[] vanilla; + + private static final Map upgrades = new HashMap<>(); + private static final IdentityHashMap wrappers = new IdentityHashMap<>(); + private static boolean needsRebuild; + + private TurtleUpgrades() {} + + public static void register( @Nonnull ITurtleUpgrade upgrade ) + { + Objects.requireNonNull( upgrade, "upgrade cannot be null" ); + rebuild(); + + Wrapper wrapper = new Wrapper( upgrade ); + String id = wrapper.id; + ITurtleUpgrade existing = upgrades.get( id ); + if( existing != null ) + { + throw new IllegalStateException( "Error registering '" + upgrade.getUnlocalisedAdjective() + " Turtle'. Upgrade ID '" + id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'" ); + } + + upgrades.put( id, upgrade ); + wrappers.put( upgrade, wrapper ); + } + + @Nullable + public static ITurtleUpgrade get( String id ) + { + rebuild(); + return upgrades.get( id ); + } + + @Nullable + public static String getOwner( @Nonnull ITurtleUpgrade upgrade ) + { + Wrapper wrapper = wrappers.get( upgrade ); + return wrapper != null ? wrapper.modId : null; + } + + public static ITurtleUpgrade get( @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return null; + + for( Wrapper wrapper : wrappers.values() ) + { + if( !wrapper.enabled ) continue; + + ItemStack craftingStack = wrapper.upgrade.getCraftingItem(); + if( !craftingStack.isEmpty() && InventoryUtil.areItemsSimilar( stack, craftingStack ) ) + { + return wrapper.upgrade; + } + } + + return null; + } + + public static Stream getVanillaUpgrades() + { + if( vanilla == null ) + { + vanilla = new ITurtleUpgrade[] { + // ComputerCraft upgrades + ComputerCraft.TurtleUpgrades.wirelessModemNormal, + ComputerCraft.TurtleUpgrades.wirelessModemAdvanced, + ComputerCraft.TurtleUpgrades.speaker, + + // Vanilla Minecraft upgrades + ComputerCraft.TurtleUpgrades.diamondPickaxe, + ComputerCraft.TurtleUpgrades.diamondAxe, + ComputerCraft.TurtleUpgrades.diamondSword, + ComputerCraft.TurtleUpgrades.diamondShovel, + ComputerCraft.TurtleUpgrades.diamondHoe, + ComputerCraft.TurtleUpgrades.craftingTable, + }; + } + + return Arrays.stream( vanilla ).filter( x -> x != null && wrappers.get( x ).enabled ); + } + + public static Stream getUpgrades() + { + return wrappers.values().stream().filter( x -> x.enabled ).map( x -> x.upgrade ); + } + + public static boolean suitableForFamily( ComputerFamily family, ITurtleUpgrade upgrade ) + { + return true; + } + + /** + * Rebuild the cache of turtle upgrades. This is done before querying the cache or registering new upgrades. + */ + private static void rebuild() + { + if( !needsRebuild ) return; + + upgrades.clear(); + for( Wrapper wrapper : wrappers.values() ) + { + if( !wrapper.enabled ) continue; + + ITurtleUpgrade existing = upgrades.get( wrapper.id ); + if( existing != null ) + { + ComputerCraft.log.error( "Error registering '" + wrapper.upgrade.getUnlocalisedAdjective() + " Turtle'." + + " Upgrade ID '" + wrapper.id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'" ); + continue; + } + + upgrades.put( wrapper.id, wrapper.upgrade ); + } + + needsRebuild = false; + } + + public static void enable( ITurtleUpgrade upgrade ) + { + Wrapper wrapper = wrappers.get( upgrade ); + if( wrapper.enabled ) return; + + wrapper.enabled = true; + needsRebuild = true; + } + + public static void disable( ITurtleUpgrade upgrade ) + { + Wrapper wrapper = wrappers.get( upgrade ); + if( !wrapper.enabled ) return; + + wrapper.enabled = false; + upgrades.remove( wrapper.id ); + } + + public static void remove( ITurtleUpgrade upgrade ) + { + wrappers.remove( upgrade ); + needsRebuild = true; + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java new file mode 100644 index 000000000..ce0a004d0 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -0,0 +1,393 @@ +/* + * 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.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.computer.Computer; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.tracking.ComputerTracker; +import dan200.computercraft.core.tracking.Tracking; +import dan200.computercraft.core.tracking.TrackingContext; +import dan200.computercraft.core.tracking.TrackingField; +import dan200.computercraft.shared.command.text.TableBuilder; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.computer.inventory.ContainerViewComputer; +import dan200.computercraft.shared.network.container.ViewComputerContainerData; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.LiteralText; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import javax.annotation.Nonnull; +import java.util.*; + +import static dan200.computercraft.shared.command.CommandUtils.isPlayer; +import static dan200.computercraft.shared.command.Exceptions.*; +import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.getComputerArgument; +import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.oneComputer; +import static dan200.computercraft.shared.command.arguments.ComputersArgumentType.*; +import static dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType.trackingField; +import static dan200.computercraft.shared.command.builder.CommandBuilder.args; +import static dan200.computercraft.shared.command.builder.CommandBuilder.command; +import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.choice; +import static dan200.computercraft.shared.command.text.ChatHelpers.*; +import static net.minecraft.server.command.CommandManager.literal; + +public final class CommandComputerCraft +{ + public static final UUID SYSTEM_UUID = new UUID( 0, 0 ); + + private static final int DUMP_LIST_ID = 5373952; + private static final int DUMP_SINGLE_ID = 1844510720; + private static final int TRACK_ID = 373882880; + + private CommandComputerCraft() + { + } + + public static void register( CommandDispatcher dispatcher ) + { + dispatcher.register( choice( "computercraft" ) + .then( literal( "dump" ) + .requires( UserLevel.OWNER_OP ) + .executes( context -> { + TableBuilder table = new TableBuilder( DUMP_LIST_ID, "Computer", "On", "Position" ); + + ServerCommandSource source = context.getSource(); + List computers = new ArrayList<>( ComputerCraft.serverComputerRegistry.getComputers() ); + + // Unless we're on a server, limit the number of rows we can send. + World world = source.getWorld(); + BlockPos pos = new BlockPos( source.getPosition() ); + + computers.sort( ( a, b ) -> { + if( a.getWorld() == b.getWorld() && a.getWorld() == world ) + { + return Double.compare( a.getPosition().getSquaredDistance( pos ), b.getPosition().getSquaredDistance( pos ) ); + } + else if( a.getWorld() == world ) + { + return -1; + } + else if( b.getWorld() == world ) + { + return 1; + } + else + { + return Integer.compare( a.getInstanceID(), b.getInstanceID() ); + } + } ); + + for( ServerComputer computer : computers ) + { + table.row( + linkComputer( source, computer, computer.getID() ), + bool( computer.isOn() ), + linkPosition( source, computer ) + ); + } + + table.display( context.getSource() ); + return computers.size(); + } ) + .then( args() + .arg( "computer", oneComputer() ) + .executes( context -> { + ServerComputer computer = getComputerArgument( context, "computer" ); + + TableBuilder table = new TableBuilder( DUMP_SINGLE_ID ); + table.row( header( "Instance" ), text( Integer.toString( computer.getInstanceID() ) ) ); + table.row( header( "Id" ), text( Integer.toString( computer.getID() ) ) ); + table.row( header( "Label" ), text( computer.getLabel() ) ); + table.row( header( "On" ), bool( computer.isOn() ) ); + table.row( header( "Position" ), linkPosition( context.getSource(), computer ) ); + table.row( header( "Family" ), text( computer.getFamily().toString() ) ); + + for( ComputerSide side : ComputerSide.values() ) + { + IPeripheral peripheral = computer.getPeripheral( side ); + if( peripheral != null ) + { + table.row( header( "Peripheral " + side.getName() ), text( peripheral.getType() ) ); + } + } + + table.display( context.getSource() ); + return 1; + } ) ) ) + + .then( command( "shutdown" ) + .requires( UserLevel.OWNER_OP ) + .argManyValue( "computers", manyComputers(), s -> ComputerCraft.serverComputerRegistry.getComputers() ) + .executes( ( context, computers ) -> { + int shutdown = 0; + for( ServerComputer computer : unwrap( context.getSource(), computers ) ) + { + if( computer.isOn() ) shutdown++; + computer.shutdown(); + } + context.getSource().sendFeedback( translate( "commands.computercraft.shutdown.done", shutdown, computers.size() ), false ); + return shutdown; + } ) ) + + .then( command( "turn-on" ) + .requires( UserLevel.OWNER_OP ) + .argManyValue( "computers", manyComputers(), s -> ComputerCraft.serverComputerRegistry.getComputers() ) + .executes( ( context, computers ) -> { + int on = 0; + for( ServerComputer computer : unwrap( context.getSource(), computers ) ) + { + if( !computer.isOn() ) on++; + computer.turnOn(); + } + context.getSource().sendFeedback( translate( "commands.computercraft.turn_on.done", on, computers.size() ), false ); + return on; + } ) ) + + .then( command( "tp" ) + .requires( UserLevel.OP ) + .arg( "computer", oneComputer() ) + .executes( context -> { + ServerComputer computer = getComputerArgument( context, "computer" ); + World world = computer.getWorld(); + BlockPos pos = computer.getPosition(); + + if( world == null || pos == null ) throw TP_NOT_THERE.create(); + + Entity entity = context.getSource().getEntityOrThrow(); + if( !(entity instanceof ServerPlayerEntity) ) throw TP_NOT_PLAYER.create(); + + ServerPlayerEntity player = (ServerPlayerEntity) entity; + if( player.getEntityWorld() == world ) + { + player.networkHandler.teleportRequest( + pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0, 0, + EnumSet.noneOf( PlayerPositionLookS2CPacket.Flag.class ) + ); + } + else + { + player.teleport( (ServerWorld) world, + pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0, 0 + ); + } + + return 1; + } ) ) + + .then( command( "queue" ) + .requires( UserLevel.ANYONE ) + .arg( "computer", manyComputers() ) + .argManyValue( "args", StringArgumentType.string(), Collections.emptyList() ) + .executes( ( ctx, args ) -> { + Collection computers = getComputersArgument( ctx, "computer" ); + Object[] rest = args.toArray(); + + int queued = 0; + for( ServerComputer computer : computers ) + { + if( computer.getFamily() == ComputerFamily.COMMAND && computer.isOn() ) + { + computer.queueEvent( "computer_command", rest ); + queued++; + } + } + + return queued; + } ) ) + + .then( command( "view" ) + .requires( UserLevel.OP ) + .arg( "computer", oneComputer() ) + .executes( context -> { + ServerPlayerEntity player = context.getSource().getPlayer(); + ServerComputer computer = getComputerArgument( context, "computer" ); + new ViewComputerContainerData( computer ).open( player, new NamedScreenHandlerFactory() + { + @Nonnull + @Override + public Text getDisplayName() + { + return new TranslatableText( "gui.computercraft.view_computer" ); + } + + @Nonnull + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory player, @Nonnull PlayerEntity entity ) + { + return new ContainerViewComputer( id, computer ); + } + } ); + return 1; + } ) ) + + .then( choice( "track" ) + .then( command( "start" ) + .requires( UserLevel.OWNER_OP ) + .executes( context -> { + getTimingContext( context.getSource() ).start(); + + String stopCommand = "/computercraft track stop"; + context.getSource().sendFeedback( translate( "commands.computercraft.track.start.stop", + link( text( stopCommand ), stopCommand, translate( "commands.computercraft.track.stop.action" ) ) ), false ); + return 1; + } ) ) + + .then( command( "stop" ) + .requires( UserLevel.OWNER_OP ) + .executes( context -> { + TrackingContext timings = getTimingContext( context.getSource() ); + if( !timings.stop() ) throw NOT_TRACKING_EXCEPTION.create(); + displayTimings( context.getSource(), timings.getImmutableTimings(), TrackingField.AVERAGE_TIME, DEFAULT_FIELDS ); + return 1; + } ) ) + + .then( command( "dump" ) + .requires( UserLevel.OWNER_OP ) + .argManyValue( "fields", trackingField(), DEFAULT_FIELDS ) + .executes( ( context, fields ) -> { + TrackingField sort; + if( fields.size() == 1 && DEFAULT_FIELDS.contains( fields.get( 0 ) ) ) + { + sort = fields.get( 0 ); + fields = DEFAULT_FIELDS; + } + else + { + sort = fields.get( 0 ); + } + + return displayTimings( context.getSource(), sort, fields ); + } ) ) ) + ); + } + + private static Text linkComputer( ServerCommandSource source, ServerComputer serverComputer, int computerId ) + { + MutableText out = new LiteralText( "" ); + + // Append the computer instance + if( serverComputer == null ) + { + out.append( text( "?" ) ); + } + else + { + out.append( link( + text( Integer.toString( serverComputer.getInstanceID() ) ), + "/computercraft dump " + serverComputer.getInstanceID(), + translate( "commands.computercraft.dump.action" ) + ) ); + } + + // And ID + out.append( " (id " + computerId + ")" ); + + // And, if we're a player, some useful links + if( serverComputer != null && UserLevel.OP.test( source ) && isPlayer( source ) ) + { + out + .append( " " ) + .append( link( + text( "\u261b" ), + "/computercraft tp " + serverComputer.getInstanceID(), + translate( "commands.computercraft.tp.action" ) + ) ) + .append( " " ) + .append( link( + text( "\u20e2" ), + "/computercraft view " + serverComputer.getInstanceID(), + translate( "commands.computercraft.view.action" ) + ) ); + } + + return out; + } + + private static Text linkPosition( ServerCommandSource context, ServerComputer computer ) + { + if( UserLevel.OP.test( context ) ) + { + return link( + position( computer.getPosition() ), + "/computercraft tp " + computer.getInstanceID(), + translate( "commands.computercraft.tp.action" ) + ); + } + else + { + return position( computer.getPosition() ); + } + } + + @Nonnull + private static TrackingContext getTimingContext( ServerCommandSource source ) + { + Entity entity = source.getEntity(); + return entity instanceof PlayerEntity ? Tracking.getContext( entity.getUuid() ) : Tracking.getContext( SYSTEM_UUID ); + } + + private static final List DEFAULT_FIELDS = Arrays.asList( TrackingField.TASKS, TrackingField.TOTAL_TIME, TrackingField.AVERAGE_TIME, TrackingField.MAX_TIME ); + + private static int displayTimings( ServerCommandSource source, TrackingField sortField, List fields ) throws CommandSyntaxException + { + return displayTimings( source, getTimingContext( source ).getTimings(), sortField, fields ); + } + + private static int displayTimings( ServerCommandSource source, @Nonnull List timings, @Nonnull TrackingField sortField, @Nonnull List fields ) throws CommandSyntaxException + { + if( timings.isEmpty() ) throw NO_TIMINGS_EXCEPTION.create(); + + Map lookup = new HashMap<>(); + int maxId = 0, maxInstance = 0; + for( ServerComputer server : ComputerCraft.serverComputerRegistry.getComputers() ) + { + lookup.put( server.getComputer(), server ); + + if( server.getInstanceID() > maxInstance ) maxInstance = server.getInstanceID(); + if( server.getID() > maxId ) maxId = server.getID(); + } + + timings.sort( Comparator.comparing( x -> x.get( sortField ) ).reversed() ); + + Text[] headers = new Text[1 + fields.size()]; + headers[0] = translate( "commands.computercraft.track.dump.computer" ); + for( int i = 0; i < fields.size(); i++ ) headers[i + 1] = translate( fields.get( i ).translationKey() ); + TableBuilder table = new TableBuilder( TRACK_ID, headers ); + + for( ComputerTracker entry : timings ) + { + Computer computer = entry.getComputer(); + ServerComputer serverComputer = computer == null ? null : lookup.get( computer ); + + Text computerComponent = linkComputer( source, serverComputer, entry.getComputerId() ); + + Text[] row = new Text[1 + fields.size()]; + row[0] = computerComponent; + for( int i = 0; i < fields.size(); i++ ) row[i + 1] = text( entry.getFormatted( fields.get( i ) ) ); + table.row( row ); + } + + table.display( source ); + return timings.size(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/CommandCopy.java b/src/main/java/dan200/computercraft/shared/command/CommandCopy.java new file mode 100644 index 000000000..2d3ba40f6 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/CommandCopy.java @@ -0,0 +1,65 @@ +/* + * 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.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import dan200.computercraft.ComputerCraft; +import net.minecraft.client.MinecraftClient; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.HoverEvent; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.ClientChatEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT ) +public final class CommandCopy +{ + private static final String PREFIX = "/computercraft copy "; + + private CommandCopy() + { + } + + public static void register( CommandDispatcher registry ) + { + registry.register( literal( "computercraft" ) + .then( literal( "copy" ) ) + .then( argument( "message", StringArgumentType.greedyString() ) ) + .executes( context -> { + MinecraftClient.getInstance().keyboard.setClipboard( context.getArgument( "message", String.class ) ); + return 1; + } ) + ); + } + + @SubscribeEvent + public static void onClientSendMessage( ClientChatEvent event ) + { + // Emulate the command on the client side + if( event.getMessage().startsWith( PREFIX ) ) + { + MinecraftClient.getInstance().keyboard.setClipboard( event.getMessage().substring( PREFIX.length() ) ); + event.setCanceled( true ); + } + } + + public static Text createCopyText( String text ) + { + return new LiteralText( text ).fillStyle( Style.EMPTY + .withClickEvent( new ClickEvent( ClickEvent.Action.RUN_COMMAND, PREFIX + text ) ) + .withHoverEvent( new HoverEvent( HoverEvent.Action.SHOW_TEXT, new TranslatableText( "gui.computercraft.tooltip.copy" ) ) ) ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/CommandUtils.java b/src/main/java/dan200/computercraft/shared/command/CommandUtils.java new file mode 100644 index 000000000..7d8937591 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/CommandUtils.java @@ -0,0 +1,69 @@ +/* + * 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.command; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.entity.Entity; +import net.minecraft.server.command.CommandSource; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraftforge.common.util.FakePlayer; + +import java.util.Arrays; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public final class CommandUtils +{ + private CommandUtils() {} + + public static boolean isPlayer( ServerCommandSource output ) + { + Entity sender = output.getEntity(); + return sender instanceof ServerPlayerEntity + && !(sender instanceof FakePlayer) + && ((ServerPlayerEntity) sender).networkHandler != null; + } + + @SuppressWarnings( "unchecked" ) + public static CompletableFuture suggestOnServer( CommandContext context, SuggestionsBuilder builder, Function, CompletableFuture> supplier ) + { + Object source = context.getSource(); + if( !(source instanceof CommandSource) ) + { + return Suggestions.empty(); + } + else if( source instanceof ServerCommandSource ) + { + return supplier.apply( (CommandContext) context ); + } + else + { + return ((CommandSource) source).getCompletions( (CommandContext) context, builder ); + } + } + + public static CompletableFuture suggest( SuggestionsBuilder builder, Iterable candidates, Function toString ) + { + String remaining = builder.getRemaining().toLowerCase( Locale.ROOT ); + for( T choice : candidates ) + { + String name = toString.apply( choice ); + if( !name.toLowerCase( Locale.ROOT ).startsWith( remaining ) ) continue; + builder.suggest( name ); + } + + return builder.buildFuture(); + } + + public static CompletableFuture suggest( SuggestionsBuilder builder, T[] candidates, Function toString ) + { + return suggest( builder, Arrays.asList( candidates ), toString ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/Exceptions.java b/src/main/java/dan200/computercraft/shared/command/Exceptions.java new file mode 100644 index 000000000..0c410df16 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/Exceptions.java @@ -0,0 +1,42 @@ +/* + * 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.command; + +import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import net.minecraft.text.TranslatableText; + +public final class Exceptions +{ + public static final DynamicCommandExceptionType COMPUTER_ARG_NONE = translated1( "argument.computercraft.computer.no_matching" ); + public static final Dynamic2CommandExceptionType COMPUTER_ARG_MANY = translated2( "argument.computercraft.computer.many_matching" ); + + public static final DynamicCommandExceptionType TRACKING_FIELD_ARG_NONE = translated1( "argument.computercraft.tracking_field.no_field" ); + + static final SimpleCommandExceptionType NOT_TRACKING_EXCEPTION = translated( "commands.computercraft.track.stop.not_enabled" ); + static final SimpleCommandExceptionType NO_TIMINGS_EXCEPTION = translated( "commands.computercraft.track.dump.no_timings" ); + + static final SimpleCommandExceptionType TP_NOT_THERE = translated( "commands.computercraft.tp.not_there" ); + static final SimpleCommandExceptionType TP_NOT_PLAYER = translated( "commands.computercraft.tp.not_player" ); + + public static final SimpleCommandExceptionType ARGUMENT_EXPECTED = translated( "argument.computercraft.argument_expected" ); + + private static SimpleCommandExceptionType translated( String key ) + { + return new SimpleCommandExceptionType( new TranslatableText( key ) ); + } + + private static DynamicCommandExceptionType translated1( String key ) + { + return new DynamicCommandExceptionType( x -> new TranslatableText( key, x ) ); + } + + private static Dynamic2CommandExceptionType translated2( String key ) + { + return new Dynamic2CommandExceptionType( ( x, y ) -> new TranslatableText( key, x, y ) ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/UserLevel.java b/src/main/java/dan200/computercraft/shared/command/UserLevel.java new file mode 100644 index 000000000..425c4eaa5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/UserLevel.java @@ -0,0 +1,71 @@ +/* + * 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.command; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import java.util.function.Predicate; + +/** + * The level a user must be at in order to execute a command. + */ +public enum UserLevel implements Predicate +{ + /** + * Only can be used by the owner of the server: namely the server console or the player in SSP. + */ + OWNER, + + /** + * Can only be used by ops. + */ + OP, + + /** + * Can be used by any op, or the player in SSP. + */ + OWNER_OP, + + /** + * Can be used by anyone. + */ + ANYONE; + + public int toLevel() + { + switch( this ) + { + case OWNER: + return 4; + case OP: + case OWNER_OP: + return 2; + case ANYONE: + default: + return 0; + } + } + + @Override + public boolean test( ServerCommandSource source ) + { + if( this == ANYONE ) return true; + + // We *always* allow level 0 stuff, even if the + MinecraftServer server = source.getMinecraftServer(); + Entity sender = source.getEntity(); + + if( server.isSinglePlayer() && sender instanceof PlayerEntity && + ((PlayerEntity) sender).getGameProfile().getName().equalsIgnoreCase( server.getServerModName() ) ) + { + if( this == OWNER || this == OWNER_OP ) return true; + } + + return source.hasPermissionLevel( toLevel() ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java b/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java new file mode 100644 index 000000000..3cf9f2abb --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java @@ -0,0 +1,40 @@ +/* + * 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.command.arguments; + +import com.mojang.brigadier.arguments.ArgumentType; +import dan200.computercraft.ComputerCraft; +import net.minecraft.command.argument.ArgumentTypes; +import net.minecraft.command.argument.serialize.ArgumentSerializer; +import net.minecraft.command.argument.serialize.ConstantArgumentSerializer; +import net.minecraft.util.Identifier; + +public final class ArgumentSerializers +{ + @SuppressWarnings( "unchecked" ) + private static > void registerUnsafe( Identifier id, Class type, ArgumentSerializer serializer ) + { + ArgumentTypes.register( id.toString(), type, (ArgumentSerializer) serializer ); + } + + private static > void register( Identifier id, Class type, ArgumentSerializer serializer ) + { + ArgumentTypes.register( id.toString(), type, serializer ); + } + + private static > void register( Identifier id, T instance ) + { + registerUnsafe( id, instance.getClass(), new ConstantArgumentSerializer<>( () -> instance ) ); + } + + public static void register() + { + register( new Identifier( ComputerCraft.MOD_ID, "tracking_field" ), TrackingFieldArgumentType.trackingField() ); + register( new Identifier( ComputerCraft.MOD_ID, "computer" ), ComputerArgumentType.oneComputer() ); + register( new Identifier( ComputerCraft.MOD_ID, "computers" ), ComputersArgumentType.class, new ComputersArgumentType.Serializer() ); + registerUnsafe( new Identifier( ComputerCraft.MOD_ID, "repeat" ), RepeatArgumentType.class, new RepeatArgumentType.Serializer() ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/ChoiceArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/ChoiceArgumentType.java new file mode 100644 index 000000000..797db6f85 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/arguments/ChoiceArgumentType.java @@ -0,0 +1,74 @@ +/* + * 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.command.arguments; + +import com.mojang.brigadier.Message; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public abstract class ChoiceArgumentType implements ArgumentType +{ + private final Iterable choices; + private final Function name; + private final Function tooltip; + private final DynamicCommandExceptionType exception; + + protected ChoiceArgumentType( Iterable choices, Function name, Function tooltip, DynamicCommandExceptionType exception ) + { + this.choices = choices; + this.name = name; + this.tooltip = tooltip; + this.exception = exception; + } + + @Override + public T parse( StringReader reader ) throws CommandSyntaxException + { + int start = reader.getCursor(); + String name = reader.readUnquotedString(); + + for( T choice : choices ) + { + String choiceName = this.name.apply( choice ); + if( name.equals( choiceName ) ) return choice; + } + + reader.setCursor( start ); + throw exception.createWithContext( reader, name ); + } + + @Override + public CompletableFuture listSuggestions( CommandContext context, SuggestionsBuilder builder ) + { + String remaining = builder.getRemaining().toLowerCase( Locale.ROOT ); + for( T choice : choices ) + { + String name = this.name.apply( choice ); + if( !name.toLowerCase( Locale.ROOT ).startsWith( remaining ) ) continue; + builder.suggest( name, tooltip.apply( choice ) ); + } + + return builder.buildFuture(); + } + + @Override + public Collection getExamples() + { + List items = choices instanceof Collection ? new ArrayList<>( ((Collection) choices).size() ) : new ArrayList<>(); + for( T choice : choices ) items.add( name.apply( choice ) ); + items.sort( Comparator.naturalOrder() ); + return items; + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java new file mode 100644 index 000000000..26319566b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java @@ -0,0 +1,92 @@ +/* + * 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.command.arguments; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import dan200.computercraft.shared.command.arguments.ComputersArgumentType.ComputersSupplier; +import dan200.computercraft.shared.computer.core.ServerComputer; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import net.minecraft.server.command.ServerCommandSource; + +import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_MANY; + +public final class ComputerArgumentType implements ArgumentType +{ + private static final ComputerArgumentType INSTANCE = new ComputerArgumentType(); + + public static ComputerArgumentType oneComputer() + { + return INSTANCE; + } + + public static ServerComputer getComputerArgument( CommandContext context, String name ) throws CommandSyntaxException + { + return context.getArgument( name, ComputerSupplier.class ).unwrap( context.getSource() ); + } + + private ComputerArgumentType() + { + } + + @Override + public ComputerSupplier parse( StringReader reader ) throws CommandSyntaxException + { + int start = reader.getCursor(); + ComputersSupplier supplier = ComputersArgumentType.someComputers().parse( reader ); + String selector = reader.getString().substring( start, reader.getCursor() ); + + return s -> { + Collection computers = supplier.unwrap( s ); + + if( computers.size() == 1 ) return computers.iterator().next(); + + StringBuilder builder = new StringBuilder(); + boolean first = true; + for( ServerComputer computer : computers ) + { + if( first ) + { + first = false; + } + else + { + builder.append( ", " ); + } + + builder.append( computer.getInstanceID() ); + } + + + // We have an incorrect number of computers: reset and throw an error + reader.setCursor( start ); + throw COMPUTER_ARG_MANY.createWithContext( reader, selector, builder.toString() ); + }; + } + + @Override + public CompletableFuture listSuggestions( CommandContext context, SuggestionsBuilder builder ) + { + return ComputersArgumentType.someComputers().listSuggestions( context, builder ); + } + + @Override + public Collection getExamples() + { + return ComputersArgumentType.someComputers().getExamples(); + } + + @FunctionalInterface + public interface ComputerSupplier + { + ServerComputer unwrap( ServerCommandSource source ) throws CommandSyntaxException; + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java new file mode 100644 index 000000000..9faa9405a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java @@ -0,0 +1,208 @@ +/* + * 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.command.arguments; + +import com.google.gson.JsonObject; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ServerComputer; +import javax.annotation.Nonnull; +import net.minecraft.command.argument.serialize.ArgumentSerializer; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.server.command.ServerCommandSource; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static dan200.computercraft.shared.command.CommandUtils.suggest; +import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer; +import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_NONE; + +public final class ComputersArgumentType implements ArgumentType +{ + private static final ComputersArgumentType MANY = new ComputersArgumentType( false ); + private static final ComputersArgumentType SOME = new ComputersArgumentType( true ); + + private static final List EXAMPLES = Arrays.asList( + "0", "#0", "@Label", "~Advanced" + ); + + public static ComputersArgumentType manyComputers() + { + return MANY; + } + + public static ComputersArgumentType someComputers() + { + return SOME; + } + + public static Collection getComputersArgument( CommandContext context, String name ) throws CommandSyntaxException + { + return context.getArgument( name, ComputersSupplier.class ).unwrap( context.getSource() ); + } + + private final boolean requireSome; + + private ComputersArgumentType( boolean requireSome ) + { + this.requireSome = requireSome; + } + + @Override + public ComputersSupplier parse( StringReader reader ) throws CommandSyntaxException + { + int start = reader.getCursor(); + char kind = reader.peek(); + ComputersSupplier computers; + if( kind == '@' ) + { + reader.skip(); + String label = reader.readUnquotedString(); + computers = getComputers( x -> Objects.equals( label, x.getLabel() ) ); + } + else if( kind == '~' ) + { + reader.skip(); + String family = reader.readUnquotedString(); + computers = getComputers( x -> x.getFamily().name().equalsIgnoreCase( family ) ); + } + else if( kind == '#' ) + { + reader.skip(); + int id = reader.readInt(); + computers = getComputers( x -> x.getID() == id ); + } + else + { + int instance = reader.readInt(); + computers = s -> { + ServerComputer computer = ComputerCraft.serverComputerRegistry.get( instance ); + return computer == null ? Collections.emptyList() : Collections.singletonList( computer ); + }; + } + + if( requireSome ) + { + String selector = reader.getString().substring( start, reader.getCursor() ); + return source -> { + Collection matched = computers.unwrap( source ); + if( matched.isEmpty() ) throw COMPUTER_ARG_NONE.create( selector ); + return matched; + }; + } + else + { + return computers; + } + } + + @Override + public CompletableFuture listSuggestions( CommandContext context, SuggestionsBuilder builder ) + { + String remaining = builder.getRemaining(); + + // We can run this one on the client, for obvious reasons. + if( remaining.startsWith( "~" ) ) + { + return suggest( builder, ComputerFamily.values(), x -> "~" + x.name() ); + } + + // Verify we've a command source and we're running on the server + return suggestOnServer( context, builder, s -> { + if( remaining.startsWith( "@" ) ) + { + suggestComputers( builder, remaining, x -> { + String label = x.getLabel(); + return label == null ? null : "@" + label; + } ); + } + else if( remaining.startsWith( "#" ) ) + { + suggestComputers( builder, remaining, c -> "#" + c.getID() ); + } + else + { + suggestComputers( builder, remaining, c -> Integer.toString( c.getInstanceID() ) ); + } + + return builder.buildFuture(); + } ); + } + + @Override + public Collection getExamples() + { + return EXAMPLES; + } + + private static void suggestComputers( SuggestionsBuilder builder, String remaining, Function renderer ) + { + remaining = remaining.toLowerCase( Locale.ROOT ); + for( ServerComputer computer : ComputerCraft.serverComputerRegistry.getComputers() ) + { + String converted = renderer.apply( computer ); + if( converted != null && converted.toLowerCase( Locale.ROOT ).startsWith( remaining ) ) + { + builder.suggest( converted ); + } + } + } + + private static ComputersSupplier getComputers( Predicate predicate ) + { + return s -> Collections.unmodifiableList( ComputerCraft.serverComputerRegistry + .getComputers() + .stream() + .filter( predicate ) + .collect( Collectors.toList() ) + ); + } + + public static class Serializer implements ArgumentSerializer + { + + @Override + public void write( @Nonnull ComputersArgumentType arg, @Nonnull PacketByteBuf buf ) + { + buf.writeBoolean( arg.requireSome ); + } + + @Nonnull + @Override + public ComputersArgumentType fromPacket( @Nonnull PacketByteBuf buf ) + { + return buf.readBoolean() ? SOME : MANY; + } + + @Override + public void write( @Nonnull ComputersArgumentType arg, @Nonnull JsonObject json ) + { + json.addProperty( "requireSome", arg.requireSome ); + } + } + + @FunctionalInterface + public interface ComputersSupplier + { + Collection unwrap( ServerCommandSource source ) throws CommandSyntaxException; + } + + public static Set unwrap( ServerCommandSource source, Collection suppliers ) throws CommandSyntaxException + { + Set computers = new HashSet<>(); + for( ComputersSupplier supplier : suppliers ) computers.addAll( supplier.unwrap( source ) ); + return computers; + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java new file mode 100644 index 000000000..dfd7294ed --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java @@ -0,0 +1,164 @@ +/* + * 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.command.arguments; + +import com.google.gson.JsonObject; +import com.mojang.brigadier.Message; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import javax.annotation.Nonnull; +import net.minecraft.command.argument.ArgumentTypes; +import net.minecraft.command.argument.serialize.ArgumentSerializer; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; + +/** + * Reads one argument multiple times. + * + * Note that this must be the last element in an argument chain: in order to improve the quality of error messages, + * we will always try to consume another argument while there is input remaining. + * + * One problem with how parsers function, is that they must consume some input: and thus we + * + * @param The type of each value returned + * @param The type of the inner parser. This will normally be a {@link List} or {@code T}. + */ +public final class RepeatArgumentType implements ArgumentType> +{ + private final ArgumentType child; + private final BiConsumer, U> appender; + private final boolean flatten; + private final SimpleCommandExceptionType some; + + private RepeatArgumentType( ArgumentType child, BiConsumer, U> appender, boolean flatten, SimpleCommandExceptionType some ) + { + this.child = child; + this.appender = appender; + this.flatten = flatten; + this.some = some; + } + + public static RepeatArgumentType some( ArgumentType appender, SimpleCommandExceptionType missing ) + { + return new RepeatArgumentType<>( appender, List::add, true, missing ); + } + + public static RepeatArgumentType> someFlat( ArgumentType> appender, SimpleCommandExceptionType missing ) + { + return new RepeatArgumentType<>( appender, List::addAll, true, missing ); + } + + @Override + public List parse( StringReader reader ) throws CommandSyntaxException + { + boolean hadSome = false; + List out = new ArrayList<>(); + while( true ) + { + reader.skipWhitespace(); + if( !reader.canRead() ) break; + + int startParse = reader.getCursor(); + appender.accept( out, child.parse( reader ) ); + hadSome = true; + + if( reader.getCursor() == startParse ) + { + throw new IllegalStateException( child + " did not consume any input on " + reader.getRemaining() ); + } + } + + // Note that each child may return an empty list, we just require that some actual input + // was consumed. + // We should probably review that this is sensible in the future. + if( !hadSome ) throw some.createWithContext( reader ); + + return Collections.unmodifiableList( out ); + } + + @Override + public CompletableFuture listSuggestions( CommandContext context, SuggestionsBuilder builder ) + { + StringReader reader = new StringReader( builder.getInput() ); + reader.setCursor( builder.getStart() ); + int previous = reader.getCursor(); + while( reader.canRead() ) + { + try + { + child.parse( reader ); + } + catch( CommandSyntaxException e ) + { + break; + } + + int cursor = reader.getCursor(); + reader.skipWhitespace(); + if( cursor == reader.getCursor() ) break; + previous = reader.getCursor(); + } + + reader.setCursor( previous ); + return child.listSuggestions( context, builder.createOffset( previous ) ); + } + + @Override + public Collection getExamples() + { + return child.getExamples(); + } + + public static class Serializer implements ArgumentSerializer> + { + @Override + public void write( @Nonnull RepeatArgumentType arg, @Nonnull PacketByteBuf buf ) + { + buf.writeBoolean( arg.flatten ); + ArgumentTypes.toPacket( buf, arg.child ); + buf.writeText( getMessage( arg ) ); + } + + @Nonnull + @Override + @SuppressWarnings( { "unchecked", "rawtypes" } ) + public RepeatArgumentType fromPacket( @Nonnull PacketByteBuf buf ) + { + boolean isList = buf.readBoolean(); + ArgumentType child = ArgumentTypes.fromPacket( buf ); + Text message = buf.readText(); + BiConsumer, ?> appender = isList ? ( list, x ) -> list.addAll( (Collection) x ) : List::add; + return new RepeatArgumentType( child, appender, isList, new SimpleCommandExceptionType( message ) ); + } + + @Override + public void write( @Nonnull RepeatArgumentType arg, @Nonnull JsonObject json ) + { + json.addProperty( "flatten", arg.flatten ); + json.addProperty( "child", "<>" ); // TODO: Potentially serialize this using reflection. + json.addProperty( "error", Text.Serializer.toJson( getMessage( arg ) ) ); + } + + private static Text getMessage( RepeatArgumentType arg ) + { + Message message = arg.some.create().getRawMessage(); + if( message instanceof Text ) return (Text) message; + return new LiteralText( message.getString() ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/TrackingFieldArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/TrackingFieldArgumentType.java new file mode 100644 index 000000000..6391fd3ef --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/arguments/TrackingFieldArgumentType.java @@ -0,0 +1,26 @@ +/* + * 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.command.arguments; + +import dan200.computercraft.core.tracking.TrackingField; +import dan200.computercraft.shared.command.Exceptions; + +import static dan200.computercraft.shared.command.text.ChatHelpers.translate; + +public final class TrackingFieldArgumentType extends ChoiceArgumentType +{ + private static final TrackingFieldArgumentType INSTANCE = new TrackingFieldArgumentType(); + + private TrackingFieldArgumentType() + { + super( TrackingField.fields().values(), TrackingField::id, x -> translate( x.translationKey() ), Exceptions.TRACKING_FIELD_ARG_NONE ); + } + + public static TrackingFieldArgumentType trackingField() + { + return INSTANCE; + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/builder/ArgCommand.java b/src/main/java/dan200/computercraft/shared/command/builder/ArgCommand.java new file mode 100644 index 000000000..f2dcc4efa --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/builder/ArgCommand.java @@ -0,0 +1,22 @@ +/* + * 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.command.builder; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +/** + * A {@link Command} which accepts an argument. + * + * @param The command source we consume. + * @param The argument given to this command when executed. + */ +@FunctionalInterface +public interface ArgCommand +{ + int run( CommandContext ctx, T arg ) throws CommandSyntaxException; +} diff --git a/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java b/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java new file mode 100644 index 000000000..b0fe0d332 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java @@ -0,0 +1,126 @@ +/* + * 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.command.builder; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.CommandNode; +import dan200.computercraft.shared.command.arguments.RepeatArgumentType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; +import net.minecraft.server.command.ServerCommandSource; + +import static dan200.computercraft.shared.command.Exceptions.ARGUMENT_EXPECTED; +import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.literal; + +/** + * An alternative way of building command nodes, so one does not have to nest. + * {@link ArgumentBuilder#then(CommandNode)}s. + * + * @param The command source we consume. + */ +public class CommandBuilder implements CommandNodeBuilder> +{ + private List> args = new ArrayList<>(); + private Predicate requires; + + public static CommandBuilder args() + { + return new CommandBuilder<>(); + } + + public static CommandBuilder command( String literal ) + { + CommandBuilder builder = new CommandBuilder<>(); + builder.args.add( literal( literal ) ); + return builder; + } + + public CommandBuilder requires( Predicate predicate ) + { + requires = requires == null ? predicate : requires.and( predicate ); + return this; + } + + public CommandBuilder arg( String name, ArgumentType type ) + { + args.add( RequiredArgumentBuilder.argument( name, type ) ); + return this; + } + + public CommandNodeBuilder>> argManyValue( String name, ArgumentType type, List empty ) + { + return argMany( name, type, () -> empty ); + } + + public CommandNodeBuilder>> argManyValue( String name, ArgumentType type, T defaultValue ) + { + return argManyValue( name, type, Collections.singletonList( defaultValue ) ); + } + + public CommandNodeBuilder>> argMany( String name, ArgumentType type, Supplier> empty ) + { + return argMany( name, RepeatArgumentType.some( type, ARGUMENT_EXPECTED ), empty ); + } + + public CommandNodeBuilder>> argManyFlatten( String name, ArgumentType> type, Supplier> empty ) + { + return argMany( name, RepeatArgumentType.someFlat( type, ARGUMENT_EXPECTED ), empty ); + } + + private CommandNodeBuilder>> argMany( String name, RepeatArgumentType type, Supplier> empty ) + { + if( args.isEmpty() ) throw new IllegalStateException( "Cannot have empty arg chain builder" ); + + return command -> { + // The node for no arguments + ArgumentBuilder tail = tail( ctx -> command.run( ctx, empty.get() ) ); + + // The node for one or more arguments + ArgumentBuilder moreArg = RequiredArgumentBuilder + .>argument( name, type ) + .executes( ctx -> command.run( ctx, getList( ctx, name ) ) ); + + // Chain all of them together! + tail.then( moreArg ); + return link( tail ); + }; + } + + @SuppressWarnings( "unchecked" ) + private static List getList( CommandContext context, String name ) + { + return (List) context.getArgument( name, List.class ); + } + + @Override + public CommandNode executes( Command command ) + { + if( args.isEmpty() ) throw new IllegalStateException( "Cannot have empty arg chain builder" ); + + return link( tail( command ) ); + } + + private ArgumentBuilder tail( Command command ) + { + ArgumentBuilder defaultTail = args.get( args.size() - 1 ); + defaultTail.executes( command ); + if( requires != null ) defaultTail.requires( requires ); + return defaultTail; + } + + private CommandNode link( ArgumentBuilder tail ) + { + for( int i = args.size() - 2; i >= 0; i-- ) tail = args.get( i ).then( tail ); + return tail.build(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/builder/CommandNodeBuilder.java b/src/main/java/dan200/computercraft/shared/command/builder/CommandNodeBuilder.java new file mode 100644 index 000000000..638a42806 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/builder/CommandNodeBuilder.java @@ -0,0 +1,26 @@ +/* + * 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.command.builder; + +import com.mojang.brigadier.tree.CommandNode; + +/** + * A builder which generates a {@link CommandNode} from the provided action. + * + * @param The command source we consume. + * @param The type of action to execute when this command is run. + */ +@FunctionalInterface +public interface CommandNodeBuilder +{ + /** + * Generate a command node which executes this command. + * + * @param command The command to run + * @return The constructed node. + */ + CommandNode executes( T command ); +} diff --git a/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java b/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java new file mode 100644 index 000000000..d7024c5ff --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java @@ -0,0 +1,204 @@ +/* + * 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.command.builder; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import javax.annotation.Nonnull; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.LiteralText; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import java.util.ArrayList; +import java.util.Collection; + +import static dan200.computercraft.shared.command.text.ChatHelpers.coloured; +import static dan200.computercraft.shared.command.text.ChatHelpers.translate; + +/** + * An alternative to {@link LiteralArgumentBuilder} which also provides a {@code /... help} command, and defaults + * to that command when no arguments are given. + */ +public final class HelpingArgumentBuilder extends LiteralArgumentBuilder +{ + private final Collection children = new ArrayList<>(); + + private HelpingArgumentBuilder( String literal ) + { + super( literal ); + } + + public static HelpingArgumentBuilder choice( String literal ) + { + return new HelpingArgumentBuilder( literal ); + } + + @Override + public LiteralArgumentBuilder executes( final Command command ) + { + throw new IllegalStateException( "Cannot use executes on a HelpingArgumentBuilder" ); + } + + @Override + public LiteralArgumentBuilder then( final ArgumentBuilder argument ) + { + if( getRedirect() != null ) throw new IllegalStateException( "Cannot add children to a redirected node" ); + + if( argument instanceof HelpingArgumentBuilder ) + { + children.add( (HelpingArgumentBuilder) argument ); + } + else if( argument instanceof LiteralArgumentBuilder ) + { + super.then( argument ); + } + else + { + throw new IllegalStateException( "HelpingArgumentBuilder can only accept literal children" ); + } + + return this; + } + + @Override + public LiteralArgumentBuilder then( CommandNode argument ) + { + if( !(argument instanceof LiteralCommandNode) ) + { + throw new IllegalStateException( "HelpingArgumentBuilder can only accept literal children" ); + } + return super.then( argument ); + } + + @Override + public LiteralCommandNode build() + { + return buildImpl( getLiteral().replace( '-', '_' ), getLiteral() ); + } + + private LiteralCommandNode build( @Nonnull String id, @Nonnull String command ) + { + return buildImpl( id + "." + getLiteral().replace( '-', '_' ), command + " " + getLiteral() ); + } + + private LiteralCommandNode buildImpl( String id, String command ) + { + HelpCommand helpCommand = new HelpCommand( id, command ); + LiteralCommandNode node = new LiteralCommandNode<>( getLiteral(), helpCommand, getRequirement(), getRedirect(), getRedirectModifier(), isFork() ); + helpCommand.node = node; + + // Set up a /... help command + LiteralArgumentBuilder helpNode = LiteralArgumentBuilder.literal( "help" ) + .requires( x -> getArguments().stream().anyMatch( y -> y.getRequirement().test( x ) ) ) + .executes( helpCommand ); + + // Add all normal command children to this and the help node + for( CommandNode child : getArguments() ) + { + node.addChild( child ); + + helpNode.then( LiteralArgumentBuilder.literal( child.getName() ) + .requires( child.getRequirement() ) + .executes( helpForChild( child, id, command ) ) + .build() + ); + } + + // And add alternative versions of which forward instead + for( HelpingArgumentBuilder childBuilder : children ) + { + LiteralCommandNode child = childBuilder.build( id, command ); + node.addChild( child ); + helpNode.then( LiteralArgumentBuilder.literal( child.getName() ) + .requires( child.getRequirement() ) + .executes( helpForChild( child, id, command ) ) + .redirect( child.getChild( "help" ) ) + .build() + ); + } + + node.addChild( helpNode.build() ); + + return node; + } + + private static final Formatting HEADER = Formatting.LIGHT_PURPLE; + private static final Formatting SYNOPSIS = Formatting.AQUA; + private static final Formatting NAME = Formatting.GREEN; + + private static final class HelpCommand implements Command + { + private final String id; + private final String command; + LiteralCommandNode node; + + private HelpCommand( String id, String command ) + { + this.id = id; + this.command = command; + } + + @Override + public int run( CommandContext context ) + { + context.getSource().sendFeedback( getHelp( context, node, id, command ), false ); + return 0; + } + } + + private static Command helpForChild( CommandNode node, String id, String command ) + { + return context -> { + context.getSource().sendFeedback( getHelp( context, node, id + "." + node.getName().replace( '-', '_' ), command + " " + node.getName() ), false ); + return 0; + }; + } + + private static Text getHelp( CommandContext context, CommandNode node, String id, String command ) + { + // An ugly hack to extract usage information from the dispatcher. We generate a temporary node, generate + // the shorthand usage, and emit that. + CommandDispatcher dispatcher = context.getSource().getMinecraftServer().getCommandManager().getDispatcher(); + CommandNode temp = new LiteralCommandNode<>( "_", null, x -> true, null, null, false ); + temp.addChild( node ); + String usage = dispatcher.getSmartUsage( temp, context.getSource() ).get( node ).substring( node.getName().length() ); + + MutableText output = new LiteralText( "" ) + .append( coloured( "/" + command + usage, HEADER ) ) + .append( " " ) + .append( coloured( translate( "commands." + id + ".synopsis" ), SYNOPSIS ) ) + .append( "\n" ) + .append( translate( "commands." + id + ".desc" ) ); + + for( CommandNode child : node.getChildren() ) + { + if( !child.getRequirement().test( context.getSource() ) || !(child instanceof LiteralCommandNode) ) + { + continue; + } + + output.append( "\n" ); + + MutableText component = coloured( child.getName(), NAME ); + component.getStyle().withClickEvent( new ClickEvent( + ClickEvent.Action.SUGGEST_COMMAND, + "/" + command + " " + child.getName() + ) ); + output.append( component ); + + output.append( " - " ).append( translate( "commands." + id + "." + child.getName() + ".synopsis" ) ); + } + + return output; + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java b/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java new file mode 100644 index 000000000..be41f40b6 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java @@ -0,0 +1,92 @@ +/* + * 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.command.text; + +import net.minecraft.text.ClickEvent; +import net.minecraft.text.HoverEvent; +import net.minecraft.text.LiteralText; +import net.minecraft.text.MutableText; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.text.*; + +/** + * Various helpers for building chat messages. + */ +public final class ChatHelpers +{ + private static final Formatting HEADER = Formatting.LIGHT_PURPLE; + + private ChatHelpers() {} + + public static MutableText coloured( String text, Formatting colour ) + { + return new LiteralText( text == null ? "" : text ).formatted( colour ); + } + + public static T coloured( T component, Formatting colour ) + { + component.formatted( colour ); + return component; + } + + public static MutableText text( String text ) + { + return new LiteralText( text == null ? "" : text ); + } + + public static MutableText translate( String text ) + { + return new TranslatableText( text == null ? "" : text ); + } + + public static MutableText translate( String text, Object... args ) + { + return new TranslatableText( text == null ? "" : text, args ); + } + + public static MutableText list( Text... children ) + { + MutableText component = new LiteralText( "" ); + for( Text child : children ) + { + component.append( child ); + } + return component; + } + + public static MutableText position( BlockPos pos ) + { + if( pos == null ) return translate( "commands.computercraft.generic.no_position" ); + return translate( "commands.computercraft.generic.position", pos.getX(), pos.getY(), pos.getZ() ); + } + + public static MutableText bool( boolean value ) + { + return value + ? coloured( translate( "commands.computercraft.generic.yes" ), Formatting.GREEN ) + : coloured( translate( "commands.computercraft.generic.no" ), Formatting.RED ); + } + + public static MutableText link( MutableText component, String command, Text toolTip ) + { + Style style = component.getStyle(); + + if( style.getColor() == null ) style = style.withColor( Formatting.YELLOW ); + style = style.withClickEvent( new ClickEvent( ClickEvent.Action.RUN_COMMAND, command ) ); + style = style.withHoverEvent( new HoverEvent( HoverEvent.Action.SHOW_TEXT, toolTip ) ); + + return component.setStyle( style ); + } + + public static MutableText header( String text ) + { + return coloured( text, HEADER ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/text/ServerTableFormatter.java b/src/main/java/dan200/computercraft/shared/command/text/ServerTableFormatter.java new file mode 100644 index 000000000..1b1a17c45 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/text/ServerTableFormatter.java @@ -0,0 +1,50 @@ +/* + * 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.command.text; + +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Nullable; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; + +public class ServerTableFormatter implements TableFormatter +{ + private final ServerCommandSource source; + + public ServerTableFormatter( ServerCommandSource source ) + { + this.source = source; + } + + @Override + @Nullable + public Text getPadding( Text component, int width ) + { + int extraWidth = width - getWidth( component ); + if( extraWidth <= 0 ) return null; + return new LiteralText( StringUtils.repeat( ' ', extraWidth ) ); + } + + @Override + public int getColumnPadding() + { + return 1; + } + + @Override + public int getWidth( Text component ) + { + return component.getString().length(); + } + + @Override + public void writeLine( int id, Text component ) + { + source.sendFeedback( component, false ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java b/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java new file mode 100644 index 000000000..61e1a888d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java @@ -0,0 +1,134 @@ +/* + * 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.command.text; + +import dan200.computercraft.shared.command.CommandUtils; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.client.ChatTableClientMessage; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import java.util.ArrayList; +import java.util.List; + +public class TableBuilder +{ + private final int id; + private int columns = -1; + private final Text[] headers; + private final ArrayList rows = new ArrayList<>(); + private int additional; + + public TableBuilder( int id, @Nonnull Text... headers ) + { + if( id < 0 ) throw new IllegalArgumentException( "ID must be positive" ); + this.id = id; + this.headers = headers; + columns = headers.length; + } + + public TableBuilder( int id ) + { + if( id < 0 ) throw new IllegalArgumentException( "ID must be positive" ); + this.id = id; + headers = null; + } + + public TableBuilder( int id, @Nonnull String... headers ) + { + if( id < 0 ) throw new IllegalArgumentException( "ID must be positive" ); + this.id = id; + this.headers = new Text[headers.length]; + columns = headers.length; + + for( int i = 0; i < headers.length; i++ ) this.headers[i] = ChatHelpers.header( headers[i] ); + } + + public void row( @Nonnull Text... row ) + { + if( columns == -1 ) columns = row.length; + if( row.length != columns ) throw new IllegalArgumentException( "Row is the incorrect length" ); + rows.add( row ); + } + + /** + * Get the unique identifier for this table type. + * + * When showing a table within Minecraft, previous instances of this table with + * the same ID will be removed from chat. + * + * @return This table's type. + */ + public int getId() + { + return id; + } + + /** + * Get the number of columns for this table. + * + * This will be the same as {@link #getHeaders()}'s length if it is is non-{@code null}, + * otherwise the length of the first column. + * + * @return The number of columns. + */ + public int getColumns() + { + return columns; + } + + @Nullable + public Text[] getHeaders() + { + return headers; + } + + @Nonnull + public List getRows() + { + return rows; + } + + public int getAdditional() + { + return additional; + } + + public void setAdditional( int additional ) + { + this.additional = additional; + } + + /** + * Trim this table to a given height. + * + * @param height The desired height. + */ + public void trim( int height ) + { + if( rows.size() > height ) + { + additional += rows.size() - height - 1; + rows.subList( height - 1, rows.size() ).clear(); + } + } + + public void display( ServerCommandSource source ) + { + if( CommandUtils.isPlayer( source ) ) + { + trim( 18 ); + NetworkHandler.sendToPlayer( (ServerPlayerEntity) source.getEntity(), new ChatTableClientMessage( this ) ); + } + else + { + trim( 100 ); + new ServerTableFormatter( source ).display( this ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/text/TableFormatter.java b/src/main/java/dan200/computercraft/shared/command/text/TableFormatter.java new file mode 100644 index 000000000..360805d84 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/text/TableFormatter.java @@ -0,0 +1,119 @@ +/* + * 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.command.text; + +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Nullable; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import static dan200.computercraft.shared.command.text.ChatHelpers.coloured; +import static dan200.computercraft.shared.command.text.ChatHelpers.translate; + +public interface TableFormatter +{ + Text SEPARATOR = coloured( "| ", Formatting.GRAY ); + Text HEADER = coloured( "=", Formatting.GRAY ); + + /** + * Get additional padding for the component. + * + * @param component The component to pad + * @param width The desired width for the component + * @return The padding for this component, or {@code null} if none is needed. + */ + @Nullable + Text getPadding( Text component, int width ); + + /** + * Get the minimum padding between each column. + * + * @return The minimum padding. + */ + int getColumnPadding(); + + int getWidth( Text component ); + + void writeLine( int id, Text component ); + + default int display( TableBuilder table ) + { + if( table.getColumns() <= 0 ) return 0; + + int rowId = table.getId(); + int columns = table.getColumns(); + int[] maxWidths = new int[columns]; + + Text[] headers = table.getHeaders(); + if( headers != null ) + { + for( int i = 0; i < columns; i++ ) maxWidths[i] = getWidth( headers[i] ); + } + + for( Text[] row : table.getRows() ) + { + for( int i = 0; i < row.length; i++ ) + { + int width = getWidth( row[i] ); + if( width > maxWidths[i] ) maxWidths[i] = width; + } + } + + // Add a small amount of padding after each column + { + int padding = getColumnPadding(); + for( int i = 0; i < maxWidths.length - 1; i++ ) maxWidths[i] += padding; + } + + // And compute the total width + int totalWidth = (columns - 1) * getWidth( SEPARATOR ); + for( int x : maxWidths ) totalWidth += x; + + if( headers != null ) + { + LiteralText line = new LiteralText( "" ); + for( int i = 0; i < columns - 1; i++ ) + { + line.append( headers[i] ); + Text padding = getPadding( headers[i], maxWidths[i] ); + if( padding != null ) line.append( padding ); + line.append( SEPARATOR ); + } + line.append( headers[columns - 1] ); + + writeLine( rowId++, line ); + + // Write a separator line. We round the width up rather than down to make + // it a tad prettier. + int rowCharWidth = getWidth( HEADER ); + int rowWidth = totalWidth / rowCharWidth + (totalWidth % rowCharWidth == 0 ? 0 : 1); + writeLine( rowId++, coloured( StringUtils.repeat( HEADER.getString(), rowWidth ), Formatting.GRAY ) ); + } + + for( Text[] row : table.getRows() ) + { + LiteralText line = new LiteralText( "" ); + for( int i = 0; i < columns - 1; i++ ) + { + line.append( row[i] ); + Text padding = getPadding( row[i], maxWidths[i] ); + if( padding != null ) line.append( padding ); + line.append( SEPARATOR ); + } + line.append( row[columns - 1] ); + writeLine( rowId++, line ); + } + + if( table.getAdditional() > 0 ) + { + writeLine( rowId++, coloured( translate( "commands.computercraft.generic.additional_rows", table.getAdditional() ), Formatting.AQUA ) ); + } + + return rowId - table.getId(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/common/BlockGeneric.java b/src/main/java/dan200/computercraft/shared/common/BlockGeneric.java new file mode 100644 index 000000000..2de987572 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/common/BlockGeneric.java @@ -0,0 +1,99 @@ +/* + * 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.common; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.BlockView; +import net.minecraft.world.World; +import net.minecraft.world.WorldView; +import net.minecraftforge.fml.RegistryObject; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Random; + +public abstract class BlockGeneric extends Block +{ + private final RegistryObject> type; + + public BlockGeneric( Settings settings, RegistryObject> type ) + { + super( settings ); + this.type = type; + } + + @Override + @Deprecated + public final void onStateReplaced( @Nonnull BlockState block, @Nonnull World world, @Nonnull BlockPos pos, BlockState replace, boolean bool ) + { + if( block.getBlock() == replace.getBlock() ) return; + + BlockEntity tile = world.getBlockEntity( pos ); + super.onStateReplaced( block, world, pos, replace, bool ); + world.removeBlockEntity( pos ); + if( tile instanceof TileGeneric ) ((TileGeneric) tile).destroy(); + } + + @Nonnull + @Override + @Deprecated + public final ActionResult onUse( @Nonnull BlockState state, World world, @Nonnull BlockPos pos, @Nonnull PlayerEntity player, @Nonnull Hand hand, @Nonnull BlockHitResult hit ) + { + BlockEntity tile = world.getBlockEntity( pos ); + return tile instanceof TileGeneric ? ((TileGeneric) tile).onActivate( player, hand, hit ) : ActionResult.PASS; + } + + @Override + @Deprecated + public final void neighborUpdate( @Nonnull BlockState state, World world, @Nonnull BlockPos pos, @Nonnull Block neighbourBlock, @Nonnull BlockPos neighbourPos, boolean isMoving ) + { + BlockEntity tile = world.getBlockEntity( pos ); + if( tile instanceof TileGeneric ) ((TileGeneric) tile).onNeighbourChange( neighbourPos ); + } + + @Override + public final void onNeighborChange( BlockState state, WorldView world, BlockPos pos, BlockPos neighbour ) + { + BlockEntity tile = world.getBlockEntity( pos ); + if( tile instanceof TileGeneric ) ((TileGeneric) tile).onNeighbourTileEntityChange( neighbour ); + } + + @Override + @Deprecated + public void scheduledTick( @Nonnull BlockState state, ServerWorld world, @Nonnull BlockPos pos, @Nonnull Random rand ) + { + BlockEntity te = world.getBlockEntity( pos ); + if( te instanceof TileGeneric ) ((TileGeneric) te).blockTick(); + } + + @Override + public boolean hasTileEntity( BlockState state ) + { + return true; + } + + @Nullable + @Override + public BlockEntity createTileEntity( @Nonnull BlockState state, @Nonnull BlockView world ) + { + return type.get().instantiate(); + } + + @Override + public boolean canBeReplacedByLeaves( BlockState state, WorldView world, BlockPos pos ) + { + return false; + } +} diff --git a/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java b/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java new file mode 100644 index 000000000..73b6d6e9a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java @@ -0,0 +1,80 @@ +/* + * 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.common; + +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.network.client.TerminalState; + +public class ClientTerminal implements ITerminal +{ + private boolean m_colour; + private Terminal m_terminal; + private boolean m_terminalChanged; + + public ClientTerminal( boolean colour ) + { + m_colour = colour; + m_terminal = null; + m_terminalChanged = false; + } + + public boolean pollTerminalChanged() + { + boolean changed = m_terminalChanged; + m_terminalChanged = false; + return changed; + } + + // ITerminal implementation + + @Override + public Terminal getTerminal() + { + return m_terminal; + } + + @Override + public boolean isColour() + { + return m_colour; + } + + public void read( TerminalState state ) + { + m_colour = state.colour; + if( state.hasTerminal() ) + { + resizeTerminal( state.width, state.height ); + state.apply( m_terminal ); + } + else + { + deleteTerminal(); + } + } + + private void resizeTerminal( int width, int height ) + { + if( m_terminal == null ) + { + m_terminal = new Terminal( width, height, () -> m_terminalChanged = true ); + m_terminalChanged = true; + } + else + { + m_terminal.resize( width, height ); + } + } + + private void deleteTerminal() + { + if( m_terminal != null ) + { + m_terminal = null; + m_terminalChanged = true; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java b/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java new file mode 100644 index 000000000..2b3f4bb5f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java @@ -0,0 +1,103 @@ +/* + * 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.common; + +import dan200.computercraft.shared.util.Colour; +import dan200.computercraft.shared.util.ColourTracker; +import dan200.computercraft.shared.util.ColourUtils; +import net.minecraft.inventory.CraftingInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.SpecialCraftingRecipe; +import net.minecraft.recipe.SpecialRecipeSerializer; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public final class ColourableRecipe extends SpecialCraftingRecipe +{ + private ColourableRecipe( Identifier id ) + { + super( id ); + } + + @Override + public boolean matches( @Nonnull CraftingInventory inv, @Nonnull World world ) + { + boolean hasColourable = false; + boolean hasDye = false; + for( int i = 0; i < inv.size(); i++ ) + { + ItemStack stack = inv.getStack( i ); + if( stack.isEmpty() ) continue; + + if( stack.getItem() instanceof IColouredItem ) + { + if( hasColourable ) return false; + hasColourable = true; + } + else if( ColourUtils.getStackColour( stack ) != null ) + { + hasDye = true; + } + else + { + return false; + } + } + + return hasColourable && hasDye; + } + + @Nonnull + @Override + public ItemStack getCraftingResult( @Nonnull CraftingInventory inv ) + { + ItemStack colourable = ItemStack.EMPTY; + + ColourTracker tracker = new ColourTracker(); + + for( int i = 0; i < inv.size(); i++ ) + { + ItemStack stack = inv.getStack( i ); + + if( stack.isEmpty() ) continue; + + if( stack.getItem() instanceof IColouredItem ) + { + colourable = stack; + } + else + { + DyeColor dye = ColourUtils.getStackColour( stack ); + if( dye == null ) continue; + + Colour colour = Colour.fromInt( 15 - dye.getId() ); + tracker.addColour( colour.getR(), colour.getG(), colour.getB() ); + } + } + + if( colourable.isEmpty() ) return ItemStack.EMPTY; + return ((IColouredItem) colourable.getItem()).withColour( colourable, tracker.getColour() ); + } + + @Override + public boolean fits( int x, int y ) + { + return x >= 2 && y >= 2; + } + + @Override + @Nonnull + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + public static final RecipeSerializer SERIALIZER = new SpecialRecipeSerializer<>( ColourableRecipe::new ); +} diff --git a/src/main/java/dan200/computercraft/shared/common/ContainerHeldItem.java b/src/main/java/dan200/computercraft/shared/common/ContainerHeldItem.java new file mode 100644 index 000000000..3819a17ba --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/common/ContainerHeldItem.java @@ -0,0 +1,81 @@ +/* + * 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.common; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.network.container.HeldItemContainerData; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.text.Text; +import net.minecraft.util.Hand; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class ContainerHeldItem extends ScreenHandler +{ + private final ItemStack stack; + private final Hand hand; + + public ContainerHeldItem( ScreenHandlerType type, int id, PlayerEntity player, Hand hand ) + { + super( type, id ); + + this.hand = hand; + stack = player.getStackInHand( hand ).copy(); + } + + public static ContainerHeldItem createPrintout( int id, PlayerInventory inventory, HeldItemContainerData data ) + { + return new ContainerHeldItem( Registry.ModContainers.PRINTOUT.get(), id, inventory.player, data.getHand() ); + } + + @Nonnull + public ItemStack getStack() + { + return stack; + } + + @Override + public boolean canUse( @Nonnull PlayerEntity player ) + { + if( !player.isAlive() ) return false; + + ItemStack stack = player.getStackInHand( hand ); + return stack == this.stack || !stack.isEmpty() && !this.stack.isEmpty() && stack.getItem() == this.stack.getItem(); + } + + public static class Factory implements NamedScreenHandlerFactory + { + private final ScreenHandlerType type; + private final Text name; + private final Hand hand; + + public Factory( ScreenHandlerType type, ItemStack stack, Hand hand ) + { + this.type = type; + this.name = stack.getName(); + this.hand = hand; + } + + @Nonnull + @Override + public Text getDisplayName() + { + return name; + } + + @Nullable + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory inventory, @Nonnull PlayerEntity player ) + { + return new ContainerHeldItem( type, id, player, hand ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/common/DefaultBundledRedstoneProvider.java b/src/main/java/dan200/computercraft/shared/common/DefaultBundledRedstoneProvider.java new file mode 100644 index 000000000..a971ff03e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/common/DefaultBundledRedstoneProvider.java @@ -0,0 +1,37 @@ +/* + * 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.common; + +import dan200.computercraft.api.redstone.IBundledRedstoneProvider; +import net.minecraft.block.Block; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public class DefaultBundledRedstoneProvider implements IBundledRedstoneProvider +{ + @Override + public int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + return getDefaultBundledRedstoneOutput( world, pos, side ); + } + + public static int getDefaultBundledRedstoneOutput( World world, BlockPos pos, Direction side ) + { + Block block = world.getBlockState( pos ).getBlock(); + if( block instanceof IBundledRedstoneBlock ) + { + IBundledRedstoneBlock generic = (IBundledRedstoneBlock) block; + if( generic.getBundledRedstoneConnectivity( world, pos, side ) ) + { + return generic.getBundledRedstoneOutput( world, pos, side ); + } + } + return -1; + } +} diff --git a/src/main/java/dan200/computercraft/shared/common/IBundledRedstoneBlock.java b/src/main/java/dan200/computercraft/shared/common/IBundledRedstoneBlock.java new file mode 100644 index 000000000..94d642701 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/common/IBundledRedstoneBlock.java @@ -0,0 +1,17 @@ +/* + * 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.common; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +public interface IBundledRedstoneBlock +{ + boolean getBundledRedstoneConnectivity( World world, BlockPos pos, Direction side ); + + int getBundledRedstoneOutput( World world, BlockPos pos, Direction side ); +} diff --git a/src/main/java/dan200/computercraft/shared/common/IColouredItem.java b/src/main/java/dan200/computercraft/shared/common/IColouredItem.java new file mode 100644 index 000000000..6a75bb253 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/common/IColouredItem.java @@ -0,0 +1,45 @@ +/* + * 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.common; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; + +public interface IColouredItem +{ + String NBT_COLOUR = "Color"; + + default int getColour( ItemStack stack ) + { + return getColourBasic( stack ); + } + + default ItemStack withColour( ItemStack stack, int colour ) + { + ItemStack copy = stack.copy(); + setColourBasic( copy, colour ); + return copy; + } + + static int getColourBasic( ItemStack stack ) + { + CompoundTag tag = stack.getTag(); + return tag != null && tag.contains( NBT_COLOUR ) ? tag.getInt( NBT_COLOUR ) : -1; + } + + static void setColourBasic( ItemStack stack, int colour ) + { + if( colour == -1 ) + { + CompoundTag tag = stack.getTag(); + if( tag != null ) tag.remove( NBT_COLOUR ); + } + else + { + stack.getOrCreateTag().putInt( NBT_COLOUR, colour ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/common/ITerminal.java b/src/main/java/dan200/computercraft/shared/common/ITerminal.java new file mode 100644 index 000000000..2e91b85b4 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/common/ITerminal.java @@ -0,0 +1,15 @@ +/* + * 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.common; + +import dan200.computercraft.core.terminal.Terminal; + +public interface ITerminal +{ + Terminal getTerminal(); + + boolean isColour(); +} diff --git a/src/main/java/dan200/computercraft/shared/common/ServerTerminal.java b/src/main/java/dan200/computercraft/shared/common/ServerTerminal.java new file mode 100644 index 000000000..4f2445ce5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/common/ServerTerminal.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.common; + +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.network.client.TerminalState; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class ServerTerminal implements ITerminal +{ + private final boolean m_colour; + private Terminal m_terminal; + private final AtomicBoolean m_terminalChanged = new AtomicBoolean( false ); + private boolean m_terminalChangedLastFrame = false; + + public ServerTerminal( boolean colour ) + { + m_colour = colour; + m_terminal = null; + } + + public ServerTerminal( boolean colour, int terminalWidth, int terminalHeight ) + { + m_colour = colour; + m_terminal = new Terminal( terminalWidth, terminalHeight, this::markTerminalChanged ); + } + + protected void resize( int width, int height ) + { + if( m_terminal == null ) + { + m_terminal = new Terminal( width, height, this::markTerminalChanged ); + markTerminalChanged(); + } + else + { + m_terminal.resize( width, height ); + } + } + + public void delete() + { + if( m_terminal != null ) + { + m_terminal = null; + markTerminalChanged(); + } + } + + protected void markTerminalChanged() + { + m_terminalChanged.set( true ); + } + + public void update() + { + m_terminalChangedLastFrame = m_terminalChanged.getAndSet( false ); + } + + public boolean hasTerminalChanged() + { + return m_terminalChangedLastFrame; + } + + @Override + public Terminal getTerminal() + { + return m_terminal; + } + + @Override + public boolean isColour() + { + return m_colour; + } + + public TerminalState write() + { + return new TerminalState( m_colour, m_terminal ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/common/TileGeneric.java b/src/main/java/dan200/computercraft/shared/common/TileGeneric.java new file mode 100644 index 000000000..7c721be3f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/common/TileGeneric.java @@ -0,0 +1,112 @@ +/* + * 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.common; + +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import javax.annotation.Nonnull; + +public abstract class TileGeneric extends BlockEntity +{ + public TileGeneric( BlockEntityType type ) + { + super( type ); + } + + public void destroy() + { + } + + public final void updateBlock() + { + markDirty(); + BlockPos pos = getPos(); + BlockState state = getCachedState(); + getWorld().updateListeners( pos, state, state, 3 ); + } + + @Nonnull + public ActionResult onActivate( PlayerEntity player, Hand hand, BlockHitResult hit ) + { + return ActionResult.PASS; + } + + public void onNeighbourChange( @Nonnull BlockPos neighbour ) + { + } + + public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour ) + { + } + + protected void blockTick() + { + } + + protected double getInteractRange( PlayerEntity player ) + { + return 8.0; + } + + public boolean isUsable( PlayerEntity player, boolean ignoreRange ) + { + if( player == null || !player.isAlive() || getWorld().getBlockEntity( getPos() ) != this ) return false; + if( ignoreRange ) return true; + + double range = getInteractRange( player ); + BlockPos pos = getPos(); + return player.getEntityWorld() == getWorld() && + player.squaredDistanceTo( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ) <= range * range; + } + + protected void writeDescription( @Nonnull CompoundTag nbt ) + { + } + + protected void readDescription( @Nonnull CompoundTag nbt ) + { + } + + @Nonnull + @Override + public final BlockEntityUpdateS2CPacket toUpdatePacket() + { + CompoundTag nbt = new CompoundTag(); + writeDescription( nbt ); + return new BlockEntityUpdateS2CPacket( pos, 0, nbt ); + } + + @Override + public final void onDataPacket( ClientConnection net, BlockEntityUpdateS2CPacket packet ) + { + if( packet.getBlockEntityType() == 0 ) readDescription( packet.getCompoundTag() ); + } + + @Nonnull + @Override + public CompoundTag toInitialChunkDataTag() + { + CompoundTag tag = super.toInitialChunkDataTag(); + writeDescription( tag ); + return tag; + } + + @Override + public void handleUpdateTag( @Nonnull BlockState state, @Nonnull CompoundTag tag ) + { + super.handleUpdateTag( state, tag ); + readDescription( tag ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java new file mode 100644 index 000000000..f4636e3bf --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java @@ -0,0 +1,265 @@ +/* + * 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.computer.apis; + +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.*; +import dan200.computercraft.shared.computer.blocks.TileCommandComputer; +import dan200.computercraft.shared.peripheral.generic.data.BlockData; +import dan200.computercraft.shared.util.NBTUtil; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import java.util.*; + +/** + * @cc.module commands + */ +public class CommandAPI implements ILuaAPI +{ + private final TileCommandComputer computer; + + public CommandAPI( TileCommandComputer computer ) + { + this.computer = computer; + } + + @Override + public String[] getNames() + { + return new String[] { "commands" }; + } + + private static Object createOutput( String output ) + { + return new Object[] { output }; + } + + private Object[] doCommand( String command ) + { + MinecraftServer server = computer.getWorld().getServer(); + if( server == null || !server.areCommandBlocksEnabled() ) + { + return new Object[] { false, createOutput( "Command blocks disabled by server" ) }; + } + + CommandManager commandManager = server.getCommandManager(); + TileCommandComputer.CommandReceiver receiver = computer.getReceiver(); + try + { + receiver.clearOutput(); + int result = commandManager.execute( computer.getSource(), command ); + return new Object[] { result > 0, receiver.copyOutput(), result }; + } + catch( Throwable t ) + { + if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error running command.", t ); + return new Object[] { false, createOutput( "Java Exception Thrown: " + t ) }; + } + } + + private static Map getBlockInfo( World world, BlockPos pos ) + { + // Get the details of the block + BlockState state = world.getBlockState( pos ); + Map table = BlockData.fill( new HashMap<>(), state ); + + BlockEntity tile = world.getBlockEntity( pos ); + if( tile != null ) table.put( "nbt", NBTUtil.toLua( tile.toTag( new CompoundTag() ) ) ); + + return table; + } + + /** + * Execute a specific command. + * + * @param command The command to execute. + * @return See {@code cc.treturn}. + * @cc.treturn boolean Whether the command executed successfully. + * @cc.treturn { string... } The output of this command, as a list of lines. + * @cc.treturn number|nil The number of "affected" objects, or `nil` if the command failed. The definition of this + * varies from command to command. + * @cc.usage Set the block above the command computer to stone. + *
+     * commands.exec("setblock ~ ~1 ~ minecraft:stone")
+     * 
+ */ + @LuaFunction( mainThread = true ) + public final Object[] exec( String command ) + { + return doCommand( command ); + } + + /** + * Asynchronously execute a command. + * + * Unlike {@link #exec}, this will immediately return, instead of waiting for the + * command to execute. This allows you to run multiple commands at the same + * time. + * + * When this command has finished executing, it will queue a `task_complete` + * event containing the result of executing this command (what {@link #exec} would + * return). + * + * @param context The context this command executes under. + * @param command The command to execute. + * @return The "task id". When this command has been executed, it will queue a `task_complete` event with a matching id. + * @throws LuaException (hidden) If the task cannot be created. + * @cc.tparam string command The command to execute. + * @cc.usage Asynchronously sets the block above the computer to stone. + *
+     * commands.execAsync("~ ~1 ~ minecraft:stone")
+     * 
+ * @cc.see parallel One may also use the parallel API to run multiple commands at once. + */ + @LuaFunction + public final long execAsync( ILuaContext context, String command ) throws LuaException + { + return context.issueMainThreadTask( () -> doCommand( command ) ); + } + + /** + * List all available commands which the computer has permission to execute. + * + * @param args Arguments to this function. + * @return A list of all available commands + * @throws LuaException (hidden) On non-string arguments. + * @cc.tparam string ... The sub-command to complete. + */ + @LuaFunction( mainThread = true ) + public final List list( IArguments args ) throws LuaException + { + MinecraftServer server = computer.getWorld().getServer(); + + if( server == null ) return Collections.emptyList(); + CommandNode node = server.getCommandManager().getDispatcher().getRoot(); + for( int j = 0; j < args.count(); j++ ) + { + String name = args.getString( j ); + node = node.getChild( name ); + if( !(node instanceof LiteralCommandNode) ) return Collections.emptyList(); + } + + List result = new ArrayList<>(); + for( CommandNode child : node.getChildren() ) + { + if( child instanceof LiteralCommandNode ) result.add( child.getName() ); + } + return result; + } + + /** + * Get the position of the current command computer. + * + * @return The block's position. + * @cc.treturn number This computer's x position. + * @cc.treturn number This computer's y position. + * @cc.treturn number This computer's z position. + * @cc.see gps.locate To get the position of a non-command computer. + */ + @LuaFunction + public final Object[] getBlockPosition() + { + // This is probably safe to do on the Lua thread. Probably. + BlockPos pos = computer.getPos(); + return new Object[] { pos.getX(), pos.getY(), pos.getZ() }; + } + + /** + * Get information about a range of blocks. + * + * This returns the same information as @{getBlockInfo}, just for multiple + * blocks at once. + * + * Blocks are traversed by ascending y level, followed by z and x - the returned + * table may be indexed using `x + z*width + y*depth*depth`. + * + * @param minX The start x coordinate of the range to query. + * @param minY The start y coordinate of the range to query. + * @param minZ The start z coordinate of the range to query. + * @param maxX The end x coordinate of the range to query. + * @param maxY The end y coordinate of the range to query. + * @param maxZ The end z coordinate of the range to query. + * @return A list of information about each block. + * @throws LuaException If the coordinates are not within the world. + * @throws LuaException If trying to get information about more than 4096 blocks. + */ + @LuaFunction( mainThread = true ) + public final List> getBlockInfos( int minX, int minY, int minZ, int maxX, int maxY, int maxZ ) throws LuaException + { + // Get the details of the block + World world = computer.getWorld(); + BlockPos min = new BlockPos( + Math.min( minX, maxX ), + Math.min( minY, maxY ), + Math.min( minZ, maxZ ) + ); + BlockPos max = new BlockPos( + Math.max( minX, maxX ), + Math.max( minY, maxY ), + Math.max( minZ, maxZ ) + ); + if( !World.method_24794( min ) || !World.method_24794( max ) ) + { + throw new LuaException( "Co-ordinates out of range" ); + } + + int blocks = (max.getX() - min.getX() + 1) * (max.getY() - min.getY() + 1) * (max.getZ() - min.getZ() + 1); + if( blocks > 4096 ) throw new LuaException( "Too many blocks" ); + + List> results = new ArrayList<>( blocks ); + for( int y = min.getY(); y <= max.getY(); y++ ) + { + for( int z = min.getZ(); z <= max.getZ(); z++ ) + { + for( int x = min.getX(); x <= max.getX(); x++ ) + { + BlockPos pos = new BlockPos( x, y, z ); + results.add( getBlockInfo( world, pos ) ); + } + } + } + + return results; + } + + /** + * Get some basic information about a block. + * + * The returned table contains the current name, metadata and block state (as + * with @{turtle.inspect}). If there is a tile entity for that block, its NBT + * will also be returned. + * + * @param x The x position of the block to query. + * @param y The y position of the block to query. + * @param z The z position of the block to query. + * @return The given block's information. + * @throws LuaException If the coordinates are not within the world, or are not currently loaded. + */ + @LuaFunction( mainThread = true ) + public final Map getBlockInfo( int x, int y, int z ) throws LuaException + { + // Get the details of the block + World world = computer.getWorld(); + BlockPos position = new BlockPos( x, y, z ); + if( World.method_24794( position ) ) + { + return getBlockInfo( world, position ); + } + else + { + throw new LuaException( "Co-ordinates out of range" ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputer.java b/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputer.java new file mode 100644 index 000000000..52cbb4e4f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputer.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.computer.blocks; + +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ComputerState; +import dan200.computercraft.shared.computer.items.ComputerItemFactory; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.DirectionProperty; +import net.minecraft.state.property.EnumProperty; +import net.minecraft.state.property.Properties; +import net.minecraft.util.math.Direction; +import net.minecraftforge.fml.RegistryObject; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class BlockComputer extends BlockComputerBase +{ + public static final EnumProperty STATE = EnumProperty.of( "state", ComputerState.class ); + public static final DirectionProperty FACING = Properties.HORIZONTAL_FACING; + + public BlockComputer( Settings settings, ComputerFamily family, RegistryObject> type ) + { + super( settings, family, type ); + setDefaultState( getDefaultState() + .with( FACING, Direction.NORTH ) + .with( STATE, ComputerState.OFF ) + ); + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( FACING, STATE ); + } + + @Nullable + @Override + public BlockState getPlacementState( ItemPlacementContext placement ) + { + return getDefaultState().with( FACING, placement.getPlayerFacing().getOpposite() ); + } + + @Nonnull + @Override + protected ItemStack getItem( TileComputerBase tile ) + { + return tile instanceof TileComputer ? ComputerItemFactory.create( (TileComputer) tile ) : ItemStack.EMPTY; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputerBase.java new file mode 100644 index 000000000..9fd767725 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputerBase.java @@ -0,0 +1,185 @@ +/* + * 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.computer.blocks; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.shared.common.BlockGeneric; +import dan200.computercraft.shared.common.IBundledRedstoneBlock; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.computer.items.IComputerItem; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.context.LootContextParameters; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.stat.Stats; +import net.minecraft.util.Identifier; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.BlockView; +import net.minecraft.world.World; +import net.minecraftforge.fml.RegistryObject; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public abstract class BlockComputerBase extends BlockGeneric implements IBundledRedstoneBlock +{ + private static final Identifier DROP = new Identifier( ComputerCraft.MOD_ID, "computer" ); + + private final ComputerFamily family; + + protected BlockComputerBase( Settings settings, ComputerFamily family, RegistryObject> type ) + { + super( settings, type ); + this.family = family; + } + + @Override + @Deprecated + public void onBlockAdded( @Nonnull BlockState state, @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState oldState, boolean isMoving ) + { + super.onBlockAdded( state, world, pos, oldState, isMoving ); + + BlockEntity tile = world.getBlockEntity( pos ); + if( tile instanceof TileComputerBase ) ((TileComputerBase) tile).updateInput(); + } + + @Override + @Deprecated + public boolean emitsRedstonePower( @Nonnull BlockState state ) + { + return true; + } + + @Override + @Deprecated + public int getStrongRedstonePower( @Nonnull BlockState state, BlockView world, @Nonnull BlockPos pos, @Nonnull Direction incomingSide ) + { + BlockEntity entity = world.getBlockEntity( pos ); + if( !(entity instanceof TileComputerBase) ) return 0; + + TileComputerBase computerEntity = (TileComputerBase) entity; + ServerComputer computer = computerEntity.getServerComputer(); + if( computer == null ) return 0; + + ComputerSide localSide = computerEntity.remapToLocalSide( incomingSide.getOpposite() ); + return computer.getRedstoneOutput( localSide ); + } + + @Nonnull + protected abstract ItemStack getItem( TileComputerBase tile ); + + public ComputerFamily getFamily() + { + return family; + } + + @Override + @Deprecated + public int getWeakRedstonePower( @Nonnull BlockState state, @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction incomingSide ) + { + return getStrongRedstonePower( state, world, pos, incomingSide ); + } + + @Override + public boolean getBundledRedstoneConnectivity( World world, BlockPos pos, Direction side ) + { + return true; + } + + @Override + public int getBundledRedstoneOutput( World world, BlockPos pos, Direction side ) + { + BlockEntity entity = world.getBlockEntity( pos ); + if( !(entity instanceof TileComputerBase) ) return 0; + + TileComputerBase computerEntity = (TileComputerBase) entity; + ServerComputer computer = computerEntity.getServerComputer(); + if( computer == null ) return 0; + + ComputerSide localSide = computerEntity.remapToLocalSide( side ); + return computer.getBundledRedstoneOutput( localSide ); + } + + @Nonnull + @Override + public ItemStack getPickBlock( BlockState state, HitResult target, BlockView world, BlockPos pos, PlayerEntity player ) + { + BlockEntity tile = world.getBlockEntity( pos ); + if( tile instanceof TileComputerBase ) + { + ItemStack result = getItem( (TileComputerBase) tile ); + if( !result.isEmpty() ) return result; + } + + return super.getPickBlock( state, target, world, pos, player ); + } + + @Override + public void afterBreak( @Nonnull World world, PlayerEntity player, @Nonnull BlockPos pos, @Nonnull BlockState state, @Nullable BlockEntity tile, @Nonnull ItemStack tool ) + { + // Don't drop blocks here - see onBlockHarvested. + player.incrementStat( Stats.MINED.getOrCreateStat( this ) ); + player.addExhaustion( 0.005F ); + } + + @Override + public void onBreak( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState state, @Nonnull PlayerEntity player ) + { + if( !(world instanceof ServerWorld) ) return; + ServerWorld serverWorld = (ServerWorld) world; + + // We drop the item here instead of doing it in the harvest method, as we should + // drop computers for creative players too. + + BlockEntity tile = world.getBlockEntity( pos ); + if( tile instanceof TileComputerBase ) + { + TileComputerBase computer = (TileComputerBase) tile; + LootContext.Builder context = new LootContext.Builder( serverWorld ) + .random( world.random ) + .parameter( LootContextParameters.ORIGIN, Vec3d.ofCenter( pos ) ) + .parameter( LootContextParameters.TOOL, player.getMainHandStack() ) + .parameter( LootContextParameters.THIS_ENTITY, player ) + .parameter( LootContextParameters.BLOCK_ENTITY, tile ) + .putDrop( DROP, ( ctx, out ) -> out.accept( getItem( computer ) ) ); + for( ItemStack item : state.getDroppedStacks( context ) ) + { + dropStack( world, pos, item ); + } + + state.onStacksDropped( serverWorld, pos, player.getMainHandStack() ); + } + } + + @Override + public void onPlaced( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState state, LivingEntity placer, @Nonnull ItemStack stack ) + { + super.onPlaced( world, pos, state, placer, stack ); + + BlockEntity tile = world.getBlockEntity( pos ); + if( !world.isClient && tile instanceof IComputerTile && stack.getItem() instanceof IComputerItem ) + { + IComputerTile computer = (IComputerTile) tile; + IComputerItem item = (IComputerItem) stack.getItem(); + + int id = item.getComputerID( stack ); + if( id != -1 ) computer.setComputerID( id ); + + String label = item.getLabel( stack ); + if( label != null ) computer.setLabel( label ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java b/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java new file mode 100644 index 000000000..8e527be7d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java @@ -0,0 +1,116 @@ +/* + * 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.computer.blocks; + +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.apis.OSAPI; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A computer or turtle wrapped as a peripheral. + * + * This allows for basic interaction with adjacent computers. Computers wrapped as peripherals will have the type + * {@code computer} while turtles will be {@code turtle}. + * + * @cc.module computer + */ +public class ComputerPeripheral implements IPeripheral +{ + private final String type; + private final ComputerProxy computer; + + public ComputerPeripheral( String type, ComputerProxy computer ) + { + this.type = type; + this.computer = computer; + } + + @Nonnull + @Override + public String getType() + { + return type; + } + + /** + * Turn the other computer on. + */ + @LuaFunction + public final void turnOn() + { + computer.turnOn(); + } + + /** + * Shutdown the other computer. + */ + @LuaFunction + public final void shutdown() + { + computer.shutdown(); + } + + /** + * Reboot or turn on the other computer. + */ + @LuaFunction + public final void reboot() + { + computer.reboot(); + } + + /** + * Get the other computer's ID. + * + * @return The computer's ID. + * @see OSAPI#getComputerID() To get your computer's ID. + */ + @LuaFunction + public final int getID() + { + return computer.assignID(); + } + + /** + * Determine if the other computer is on. + * + * @return If the computer is on. + */ + @LuaFunction + public final boolean isOn() + { + return computer.isOn(); + } + + /** + * Get the other computer's label. + * + * @return The computer's label. + * @see OSAPI#getComputerLabel() To get your label. + */ + @Nullable + @LuaFunction + public final String getLabel() + { + return computer.getLabel(); + } + + @Override + public boolean equals( IPeripheral other ) + { + return other instanceof ComputerPeripheral && computer == ((ComputerPeripheral) other).computer; + } + + @Nonnull + @Override + public Object getTarget() + { + return computer.getTile(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerProxy.java b/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerProxy.java new file mode 100644 index 000000000..205161aeb --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerProxy.java @@ -0,0 +1,91 @@ +/* + * 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.computer.blocks; + +import dan200.computercraft.shared.computer.core.IComputer; +import dan200.computercraft.shared.computer.core.ServerComputer; + +import java.util.function.Supplier; + +/** + * A proxy object for computer objects, delegating to {@link IComputer} or {@link TileComputer} where appropriate. + */ +public final class ComputerProxy +{ + private final Supplier get; + + public ComputerProxy( Supplier get ) + { + this.get = get; + } + + protected TileComputerBase getTile() + { + return get.get(); + } + + public void turnOn() + { + TileComputerBase tile = getTile(); + ServerComputer computer = tile.getServerComputer(); + if( computer == null ) + { + tile.m_startOn = true; + } + else + { + computer.turnOn(); + } + } + + public void shutdown() + { + TileComputerBase tile = getTile(); + ServerComputer computer = tile.getServerComputer(); + if( computer == null ) + { + tile.m_startOn = false; + } + else + { + computer.shutdown(); + } + } + + public void reboot() + { + TileComputerBase tile = getTile(); + ServerComputer computer = tile.getServerComputer(); + if( computer == null ) + { + tile.m_startOn = true; + } + else + { + computer.reboot(); + } + } + + public int assignID() + { + TileComputerBase tile = getTile(); + ServerComputer computer = tile.getServerComputer(); + return computer == null ? tile.getComputerID() : computer.getID(); + } + + public boolean isOn() + { + ServerComputer computer = getTile().getServerComputer(); + return computer != null && computer.isOn(); + } + + public String getLabel() + { + TileComputerBase tile = getTile(); + ServerComputer computer = tile.getServerComputer(); + return computer == null ? tile.getLabel() : computer.getLabel(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/IComputerTile.java b/src/main/java/dan200/computercraft/shared/computer/blocks/IComputerTile.java new file mode 100644 index 000000000..7adb7c0a8 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/IComputerTile.java @@ -0,0 +1,21 @@ +/* + * 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.computer.blocks; + +import dan200.computercraft.shared.computer.core.ComputerFamily; + +public interface IComputerTile +{ + int getComputerID(); + + void setComputerID( int id ); + + String getLabel(); + + void setLabel( String label ); + + ComputerFamily getFamily(); +} diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java b/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java new file mode 100644 index 000000000..05a9b04ec --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java @@ -0,0 +1,136 @@ +/* + * 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.computer.blocks; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.apis.CommandAPI; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ServerComputer; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandOutput; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.math.Vec2f; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.GameRules; +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class TileCommandComputer extends TileComputer +{ + public class CommandReceiver implements CommandOutput + { + private final Map output = new HashMap<>(); + + public void clearOutput() + { + output.clear(); + } + + public Map getOutput() + { + return output; + } + + public Map copyOutput() + { + return new HashMap<>( output ); + } + + @Override + public void sendSystemMessage( @Nonnull Text textComponent, @Nonnull UUID id ) + { + output.put( output.size() + 1, textComponent.getString() ); + } + + @Override + public boolean shouldReceiveFeedback() + { + return getWorld().getGameRules().getBoolean( GameRules.SEND_COMMAND_FEEDBACK ); + } + + @Override + public boolean shouldTrackOutput() + { + return true; + } + + @Override + public boolean shouldBroadcastConsoleToOps() + { + return getWorld().getGameRules().getBoolean( GameRules.COMMAND_BLOCK_OUTPUT ); + } + } + + private final CommandReceiver receiver; + + public TileCommandComputer( ComputerFamily family, BlockEntityType type ) + { + super( family, type ); + receiver = new CommandReceiver(); + } + + public CommandReceiver getReceiver() + { + return receiver; + } + + public ServerCommandSource getSource() + { + ServerComputer computer = getServerComputer(); + String name = "@"; + if( computer != null ) + { + String label = computer.getLabel(); + if( label != null ) name = label; + } + + return new ServerCommandSource( receiver, + new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ), Vec2f.ZERO, + (ServerWorld) getWorld(), 2, + name, new LiteralText( name ), + getWorld().getServer(), null + ); + } + + @Override + protected ServerComputer createComputer( int instanceID, int id ) + { + ServerComputer computer = super.createComputer( instanceID, id ); + computer.addAPI( new CommandAPI( this ) ); + return computer; + } + + @Override + public boolean isUsable( PlayerEntity player, boolean ignoreRange ) + { + return isUsable( player ) && super.isUsable( player, ignoreRange ); + } + + public static boolean isUsable( PlayerEntity player ) + { + MinecraftServer server = player.getServer(); + if( server == null || !server.areCommandBlocksEnabled() ) + { + player.sendMessage( new TranslatableText( "advMode.notEnabled" ), true ); + return false; + } + else if( ComputerCraft.commandRequireCreative ? !player.isCreativeLevelTwoOp() : !server.getPlayerManager().isOperator( player.getGameProfile() ) ) + { + player.sendMessage( new TranslatableText( "advMode.notAllowed" ), true ); + return false; + } + + return true; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java new file mode 100644 index 000000000..b71d32e95 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java @@ -0,0 +1,116 @@ +/* + * 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.computer.blocks; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ComputerState; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.computer.inventory.ContainerComputer; +import dan200.computercraft.shared.util.CapabilityUtil; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.util.math.Direction; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; + +public class TileComputer extends TileComputerBase +{ + private ComputerProxy proxy; + private LazyOptional peripheral; + + public TileComputer( ComputerFamily family, BlockEntityType type ) + { + super( type, family ); + } + + @Override + protected ServerComputer createComputer( int instanceID, int id ) + { + ComputerFamily family = getFamily(); + ServerComputer computer = new ServerComputer( + getWorld(), id, label, instanceID, family, + ComputerCraft.computerTermWidth, + ComputerCraft.computerTermHeight + ); + computer.setPosition( getPos() ); + return computer; + } + + public boolean isUsableByPlayer( PlayerEntity player ) + { + return isUsable( player, false ); + } + + @Override + public Direction getDirection() + { + return getCachedState().get( BlockComputer.FACING ); + } + + @Override + protected void updateBlockState( ComputerState newState ) + { + BlockState existing = getCachedState(); + if( existing.get( BlockComputer.STATE ) != newState ) + { + getWorld().setBlockState( getPos(), existing.with( BlockComputer.STATE, newState ), 3 ); + } + } + + @Override + protected ComputerSide remapLocalSide( ComputerSide localSide ) + { + // For legacy reasons, computers invert the meaning of "left" and "right". A computer's front is facing + // towards you, but a turtle's front is facing the other way. + if( localSide == ComputerSide.RIGHT ) return ComputerSide.LEFT; + if( localSide == ComputerSide.LEFT ) return ComputerSide.RIGHT; + return localSide; + } + + @Nullable + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory inventory, @Nonnull PlayerEntity player ) + { + return new ContainerComputer( id, this ); + } + + @Nonnull + @Override + public LazyOptional getCapability( @Nonnull Capability cap, @Nullable Direction side ) + { + if( cap == CAPABILITY_PERIPHERAL ) + { + if( peripheral == null ) + { + peripheral = LazyOptional.of( () -> { + if( proxy == null ) proxy = new ComputerProxy( () -> this ); + return new ComputerPeripheral( "computer", proxy ); + } ); + } + return peripheral.cast(); + } + + return super.getCapability( cap, side ); + } + + @Override + protected void invalidateCaps() + { + super.invalidateCaps(); + peripheral = CapabilityUtil.invalidate( peripheral ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java new file mode 100644 index 000000000..8c9cf0518 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java @@ -0,0 +1,441 @@ +/* + * 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.computer.blocks; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.shared.BundledRedstone; +import dan200.computercraft.shared.Peripherals; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ComputerState; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.network.container.ComputerContainerData; +import dan200.computercraft.shared.util.DirectionUtil; +import dan200.computercraft.shared.util.RedstoneUtil; +import joptsimple.internal.Strings; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.block.RedstoneWireBlock; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.Nameable; +import net.minecraft.util.Tickable; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +public abstract class TileComputerBase extends TileGeneric implements IComputerTile, Tickable, Nameable, NamedScreenHandlerFactory +{ + private static final String NBT_ID = "ComputerId"; + private static final String NBT_LABEL = "Label"; + private static final String NBT_ON = "On"; + + private int m_instanceID = -1; + private int m_computerID = -1; + protected String label = null; + private boolean m_on = false; + boolean m_startOn = false; + private boolean m_fresh = false; + + private final ComputerFamily family; + + public TileComputerBase( BlockEntityType type, ComputerFamily family ) + { + super( type ); + this.family = family; + } + + protected void unload() + { + if( m_instanceID >= 0 ) + { + if( !getWorld().isClient ) ComputerCraft.serverComputerRegistry.remove( m_instanceID ); + m_instanceID = -1; + } + } + + @Override + public void destroy() + { + unload(); + for( Direction dir : DirectionUtil.FACINGS ) + { + RedstoneUtil.propagateRedstoneOutput( getWorld(), getPos(), dir ); + } + } + + @Override + public void onChunkUnloaded() + { + unload(); + } + + @Override + public void markRemoved() + { + unload(); + super.markRemoved(); + } + + protected boolean canNameWithTag( PlayerEntity player ) + { + return false; + } + + @Nonnull + @Override + public ActionResult onActivate( PlayerEntity player, Hand hand, BlockHitResult hit ) + { + ItemStack currentItem = player.getStackInHand( hand ); + if( !currentItem.isEmpty() && currentItem.getItem() == Items.NAME_TAG && canNameWithTag( player ) && currentItem.hasCustomName() ) + { + // Label to rename computer + if( !getWorld().isClient ) + { + setLabel( currentItem.getName().getString() ); + currentItem.decrement( 1 ); + } + return ActionResult.SUCCESS; + } + else if( !player.isInSneakingPose() ) + { + // Regular right click to activate computer + if( !getWorld().isClient && isUsable( player, false ) ) + { + createServerComputer().turnOn(); + new ComputerContainerData( createServerComputer() ).open( player, this ); + } + return ActionResult.SUCCESS; + } + return ActionResult.PASS; + } + + @Override + public void onNeighbourChange( @Nonnull BlockPos neighbour ) + { + updateInput( neighbour ); + } + + @Override + public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour ) + { + updateInput( neighbour ); + } + + @Override + public void tick() + { + if( !getWorld().isClient ) + { + ServerComputer computer = createServerComputer(); + if( computer == null ) return; + + // If the computer isn't on and should be, then turn it on + if( m_startOn || (m_fresh && m_on) ) + { + computer.turnOn(); + m_startOn = false; + } + + computer.keepAlive(); + + m_fresh = false; + m_computerID = computer.getID(); + label = computer.getLabel(); + m_on = computer.isOn(); + + if( computer.hasOutputChanged() ) updateOutput(); + + // Update the block state if needed. We don't fire a block update intentionally, + // as this only really is needed on the client side. + updateBlockState( computer.getState() ); + + if( computer.hasOutputChanged() ) updateOutput(); + } + } + + protected abstract void updateBlockState( ComputerState newState ); + + @Nonnull + @Override + public CompoundTag toTag( @Nonnull CompoundTag nbt ) + { + // Save ID, label and power state + if( m_computerID >= 0 ) nbt.putInt( NBT_ID, m_computerID ); + if( label != null ) nbt.putString( NBT_LABEL, label ); + nbt.putBoolean( NBT_ON, m_on ); + + return super.toTag( nbt ); + } + + @Override + public void fromTag( @Nonnull BlockState state, @Nonnull CompoundTag nbt ) + { + super.fromTag( state, nbt ); + + // Load ID, label and power state + m_computerID = nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1; + label = nbt.contains( NBT_LABEL ) ? nbt.getString( NBT_LABEL ) : null; + m_on = m_startOn = nbt.getBoolean( NBT_ON ); + } + + protected boolean isPeripheralBlockedOnSide( ComputerSide localSide ) + { + return false; + } + + protected abstract Direction getDirection(); + + protected ComputerSide remapToLocalSide( Direction globalSide ) + { + return remapLocalSide( DirectionUtil.toLocal( getDirection(), globalSide ) ); + } + + protected ComputerSide remapLocalSide( ComputerSide localSide ) + { + return localSide; + } + + private void updateSideInput( ServerComputer computer, Direction dir, BlockPos offset ) + { + Direction offsetSide = dir.getOpposite(); + ComputerSide localDir = remapToLocalSide( dir ); + + computer.setRedstoneInput( localDir, getRedstoneInput( world, offset, dir ) ); + computer.setBundledRedstoneInput( localDir, BundledRedstone.getOutput( getWorld(), offset, offsetSide ) ); + if( !isPeripheralBlockedOnSide( localDir ) ) + { + IPeripheral peripheral = Peripherals.getPeripheral( getWorld(), offset, offsetSide, o -> updateInput( dir ) ); + computer.setPeripheral( localDir, peripheral ); + } + } + + /** + * Gets the redstone input for an adjacent block. + * + * @param world The world we exist in + * @param pos The position of the neighbour + * @param side The side we are reading from + * @return The effective redstone power + * @see RedstoneDiodeBlock#calculateInputStrength(World, BlockPos, BlockState) + */ + protected static int getRedstoneInput( World world, BlockPos pos, Direction side ) + { + int power = world.getEmittedRedstonePower( pos, side ); + if( power >= 15 ) return power; + + BlockState neighbour = world.getBlockState( pos ); + return neighbour.getBlock() == Blocks.REDSTONE_WIRE + ? Math.max( power, neighbour.get( RedstoneWireBlock.POWER ) ) + : power; + } + + public void updateInput() + { + if( getWorld() == null || getWorld().isClient ) return; + + // Update all sides + ServerComputer computer = getServerComputer(); + if( computer == null ) return; + + BlockPos pos = computer.getPosition(); + for( Direction dir : DirectionUtil.FACINGS ) + { + updateSideInput( computer, dir, pos.offset( dir ) ); + } + } + + private void updateInput( BlockPos neighbour ) + { + if( getWorld() == null || getWorld().isClient ) return; + + ServerComputer computer = getServerComputer(); + if( computer == null ) return; + + for( Direction dir : DirectionUtil.FACINGS ) + { + BlockPos offset = pos.offset( dir ); + if( offset.equals( neighbour ) ) + { + updateSideInput( computer, dir, offset ); + return; + } + } + + // If the position is not any adjacent one, update all inputs. + updateInput(); + } + + private void updateInput( Direction dir ) + { + if( getWorld() == null || getWorld().isClient ) return; + + ServerComputer computer = getServerComputer(); + if( computer == null ) return; + + updateSideInput( computer, dir, pos.offset( dir ) ); + } + + public void updateOutput() + { + // Update redstone + updateBlock(); + for( Direction dir : DirectionUtil.FACINGS ) + { + RedstoneUtil.propagateRedstoneOutput( getWorld(), getPos(), dir ); + } + } + + protected abstract ServerComputer createComputer( int instanceID, int id ); + + @Override + public final int getComputerID() + { + return m_computerID; + } + + @Override + public final String getLabel() + { + return label; + } + + @Override + public final void setComputerID( int id ) + { + if( getWorld().isClient || m_computerID == id ) return; + + m_computerID = id; + ServerComputer computer = getServerComputer(); + if( computer != null ) computer.setID( m_computerID ); + markDirty(); + } + + @Override + public final void setLabel( String label ) + { + if( getWorld().isClient || Objects.equals( this.label, label ) ) return; + + this.label = label; + ServerComputer computer = getServerComputer(); + if( computer != null ) computer.setLabel( label ); + markDirty(); + } + + @Override + public ComputerFamily getFamily() + { + return family; + } + + public ServerComputer createServerComputer() + { + if( getWorld().isClient ) return null; + + boolean changed = false; + if( m_instanceID < 0 ) + { + m_instanceID = ComputerCraft.serverComputerRegistry.getUnusedInstanceID(); + changed = true; + } + if( !ComputerCraft.serverComputerRegistry.contains( m_instanceID ) ) + { + ServerComputer computer = createComputer( m_instanceID, m_computerID ); + ComputerCraft.serverComputerRegistry.add( m_instanceID, computer ); + m_fresh = true; + changed = true; + } + if( changed ) + { + updateBlock(); + updateInput(); + } + return ComputerCraft.serverComputerRegistry.get( m_instanceID ); + } + + public ServerComputer getServerComputer() + { + return getWorld().isClient ? null : ComputerCraft.serverComputerRegistry.get( m_instanceID ); + } + + // Networking stuff + + @Override + protected void writeDescription( @Nonnull CompoundTag nbt ) + { + super.writeDescription( nbt ); + if( label != null ) nbt.putString( NBT_LABEL, label ); + if( m_computerID >= 0 ) nbt.putInt( NBT_ID, m_computerID ); + } + + @Override + protected void readDescription( @Nonnull CompoundTag nbt ) + { + super.readDescription( nbt ); + label = nbt.contains( NBT_LABEL ) ? nbt.getString( NBT_LABEL ) : null; + m_computerID = nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1; + } + + protected void transferStateFrom( TileComputerBase copy ) + { + if( copy.m_computerID != m_computerID || copy.m_instanceID != m_instanceID ) + { + unload(); + m_instanceID = copy.m_instanceID; + m_computerID = copy.m_computerID; + label = copy.label; + m_on = copy.m_on; + m_startOn = copy.m_startOn; + updateBlock(); + } + copy.m_instanceID = -1; + } + + @Nonnull + @Override + public Text getName() + { + return hasCustomName() + ? new LiteralText( label ) + : new TranslatableText( getCachedState().getBlock().getTranslationKey() ); + } + + @Override + public boolean hasCustomName() + { + return !Strings.isNullOrEmpty( label ); + } + + @Nullable + @Override + public Text getCustomName() + { + return hasCustomName() ? new LiteralText( label ) : null; + } + + @Nonnull + @Override + public Text getDisplayName() + { + return Nameable.super.getDisplayName(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ClientComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ClientComputer.java new file mode 100644 index 000000000..707eead39 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/ClientComputer.java @@ -0,0 +1,129 @@ +/* + * 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.computer.core; + +import dan200.computercraft.shared.common.ClientTerminal; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.server.*; +import net.minecraft.nbt.CompoundTag; + +public class ClientComputer extends ClientTerminal implements IComputer +{ + private final int m_instanceID; + + private boolean m_on = false; + private boolean m_blinking = false; + private CompoundTag m_userData = null; + + + public ClientComputer( int instanceID ) + { + super( false ); + m_instanceID = instanceID; + } + + public CompoundTag getUserData() + { + return m_userData; + } + + public void requestState() + { + // Request state from server + NetworkHandler.sendToServer( new RequestComputerMessage( getInstanceID() ) ); + } + + // IComputer + + @Override + public int getInstanceID() + { + return m_instanceID; + } + + @Override + public boolean isOn() + { + return m_on; + } + + @Override + public boolean isCursorDisplayed() + { + return m_on && m_blinking; + } + + @Override + public void turnOn() + { + // Send turnOn to server + NetworkHandler.sendToServer( new ComputerActionServerMessage( m_instanceID, ComputerActionServerMessage.Action.TURN_ON ) ); + } + + @Override + public void shutdown() + { + // Send shutdown to server + NetworkHandler.sendToServer( new ComputerActionServerMessage( m_instanceID, ComputerActionServerMessage.Action.SHUTDOWN ) ); + } + + @Override + public void reboot() + { + // Send reboot to server + NetworkHandler.sendToServer( new ComputerActionServerMessage( m_instanceID, ComputerActionServerMessage.Action.REBOOT ) ); + } + + @Override + public void queueEvent( String event, Object[] arguments ) + { + // Send event to server + NetworkHandler.sendToServer( new QueueEventServerMessage( m_instanceID, event, arguments ) ); + } + + @Override + public void keyDown( int key, boolean repeat ) + { + NetworkHandler.sendToServer( new KeyEventServerMessage( m_instanceID, repeat ? KeyEventServerMessage.TYPE_REPEAT : KeyEventServerMessage.TYPE_DOWN, key ) ); + } + + @Override + public void keyUp( int key ) + { + NetworkHandler.sendToServer( new KeyEventServerMessage( m_instanceID, KeyEventServerMessage.TYPE_UP, key ) ); + } + + @Override + public void mouseClick( int button, int x, int y ) + { + NetworkHandler.sendToServer( new MouseEventServerMessage( m_instanceID, MouseEventServerMessage.TYPE_CLICK, button, x, y ) ); + } + + @Override + public void mouseUp( int button, int x, int y ) + { + NetworkHandler.sendToServer( new MouseEventServerMessage( m_instanceID, MouseEventServerMessage.TYPE_UP, button, x, y ) ); + } + + @Override + public void mouseDrag( int button, int x, int y ) + { + NetworkHandler.sendToServer( new MouseEventServerMessage( m_instanceID, MouseEventServerMessage.TYPE_DRAG, button, x, y ) ); + } + + @Override + public void mouseScroll( int direction, int x, int y ) + { + NetworkHandler.sendToServer( new MouseEventServerMessage( m_instanceID, MouseEventServerMessage.TYPE_SCROLL, direction, x, y ) ); + } + + public void setState( ComputerState state, CompoundTag userData ) + { + m_on = state != ComputerState.OFF; + m_blinking = state == ComputerState.BLINKING; + m_userData = userData; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ClientComputerRegistry.java b/src/main/java/dan200/computercraft/shared/computer/core/ClientComputerRegistry.java new file mode 100644 index 000000000..fe4cf0292 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/ClientComputerRegistry.java @@ -0,0 +1,16 @@ +/* + * 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.computer.core; + +public class ClientComputerRegistry extends ComputerRegistry +{ + @Override + public void add( int instanceID, ClientComputer computer ) + { + super.add( instanceID, computer ); + computer.requestState(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ComputerFamily.java b/src/main/java/dan200/computercraft/shared/computer/core/ComputerFamily.java new file mode 100644 index 000000000..c5accf370 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/ComputerFamily.java @@ -0,0 +1,13 @@ +/* + * 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.computer.core; + +public enum ComputerFamily +{ + NORMAL, + ADVANCED, + COMMAND +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ComputerRegistry.java b/src/main/java/dan200/computercraft/shared/computer/core/ComputerRegistry.java new file mode 100644 index 000000000..10ad45731 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/ComputerRegistry.java @@ -0,0 +1,78 @@ +/* + * 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.computer.core; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class ComputerRegistry +{ + private Map m_computers; + private int m_nextUnusedInstanceID; + private int m_sessionID; + + protected ComputerRegistry() + { + m_computers = new HashMap<>(); + reset(); + } + + public int getSessionID() + { + return m_sessionID; + } + + public int getUnusedInstanceID() + { + return m_nextUnusedInstanceID++; + } + + public Collection getComputers() + { + return m_computers.values(); + } + + public T get( int instanceID ) + { + if( instanceID >= 0 ) + { + if( m_computers.containsKey( instanceID ) ) + { + return m_computers.get( instanceID ); + } + } + return null; + } + + public boolean contains( int instanceID ) + { + return m_computers.containsKey( instanceID ); + } + + public void add( int instanceID, T computer ) + { + if( m_computers.containsKey( instanceID ) ) + { + remove( instanceID ); + } + m_computers.put( instanceID, computer ); + m_nextUnusedInstanceID = Math.max( m_nextUnusedInstanceID, instanceID + 1 ); + } + + public void remove( int instanceID ) + { + m_computers.remove( instanceID ); + } + + public void reset() + { + m_computers.clear(); + m_nextUnusedInstanceID = 0; + m_sessionID = new Random().nextInt(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java b/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java new file mode 100644 index 000000000..4757350e7 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java @@ -0,0 +1,36 @@ +/* + * 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.computer.core; + +import javax.annotation.Nonnull; +import net.minecraft.util.StringIdentifiable; + +public enum ComputerState implements StringIdentifiable +{ + OFF( "off" ), + ON( "on" ), + BLINKING( "blinking" ); + + private final String name; + + ComputerState( String name ) + { + this.name = name; + } + + @Nonnull + @Override + public String asString() + { + return name; + } + + @Override + public String toString() + { + return name; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/IComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/IComputer.java new file mode 100644 index 000000000..11b3af51f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/IComputer.java @@ -0,0 +1,37 @@ +/* + * 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.computer.core; + +import dan200.computercraft.shared.common.ITerminal; + +public interface IComputer extends ITerminal, InputHandler +{ + int getInstanceID(); + + boolean isOn(); + + boolean isCursorDisplayed(); + + void turnOn(); + + void shutdown(); + + void reboot(); + + @Override + void queueEvent( String event, Object[] arguments ); + + default void queueEvent( String event ) + { + queueEvent( event, null ); + } + + default ComputerState getState() + { + if( !isOn() ) return ComputerState.OFF; + return isCursorDisplayed() ? ComputerState.BLINKING : ComputerState.ON; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/IContainerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/IContainerComputer.java new file mode 100644 index 000000000..67ee5d74e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/IContainerComputer.java @@ -0,0 +1,38 @@ +/* + * 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.computer.core; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * An instance of {@link Container} which provides a computer. You should implement this + * if you provide custom computers/GUIs to interact with them. + */ +@FunctionalInterface +public interface IContainerComputer +{ + /** + * Get the computer you are interacting with. + * + * This will only be called on the server. + * + * @return The computer you are interacting with. + */ + @Nullable + IComputer getComputer(); + + /** + * Get the input controller for this container. + * + * @return This container's input. + */ + @Nonnull + default InputState getInput() + { + return new InputState( this ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/InputHandler.java b/src/main/java/dan200/computercraft/shared/computer/core/InputHandler.java new file mode 100644 index 000000000..efc401070 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/InputHandler.java @@ -0,0 +1,47 @@ +/* + * 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.computer.core; + +/** + * Receives some input and forwards it to a computer. + * + * @see InputState + * @see IComputer + */ +public interface InputHandler +{ + void queueEvent( String event, Object[] arguments ); + + default void keyDown( int key, boolean repeat ) + { + queueEvent( "key", new Object[] { key, repeat } ); + } + + default void keyUp( int key ) + { + queueEvent( "key_up", new Object[] { key } ); + } + + default void mouseClick( int button, int x, int y ) + { + queueEvent( "mouse_click", new Object[] { button, x, y } ); + } + + default void mouseUp( int button, int x, int y ) + { + queueEvent( "mouse_up", new Object[] { button, x, y } ); + } + + default void mouseDrag( int button, int x, int y ) + { + queueEvent( "mouse_drag", new Object[] { button, x, y } ); + } + + default void mouseScroll( int direction, int x, int y ) + { + queueEvent( "mouse_scroll", new Object[] { direction, x, y } ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/InputState.java b/src/main/java/dan200/computercraft/shared/computer/core/InputState.java new file mode 100644 index 000000000..832bc91f2 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/InputState.java @@ -0,0 +1,110 @@ +/* + * 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.computer.core; + +import it.unimi.dsi.fastutil.ints.IntIterator; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; + +/** + * An {@link InputHandler} which keeps track of the current key and mouse state, and releases them when the container + * is closed. + */ +public class InputState implements InputHandler +{ + private final IContainerComputer owner; + private final IntSet keysDown = new IntOpenHashSet( 4 ); + + private int lastMouseX; + private int lastMouseY; + private int lastMouseDown = -1; + + public InputState( IContainerComputer owner ) + { + this.owner = owner; + } + + @Override + public void queueEvent( String event, Object[] arguments ) + { + IComputer computer = owner.getComputer(); + if( computer != null ) computer.queueEvent( event, arguments ); + } + + @Override + public void keyDown( int key, boolean repeat ) + { + keysDown.add( key ); + IComputer computer = owner.getComputer(); + if( computer != null ) computer.keyDown( key, repeat ); + } + + @Override + public void keyUp( int key ) + { + keysDown.remove( key ); + IComputer computer = owner.getComputer(); + if( computer != null ) computer.keyUp( key ); + } + + @Override + public void mouseClick( int button, int x, int y ) + { + lastMouseX = x; + lastMouseY = y; + lastMouseDown = button; + + IComputer computer = owner.getComputer(); + if( computer != null ) computer.mouseClick( button, x, y ); + } + + @Override + public void mouseUp( int button, int x, int y ) + { + lastMouseX = x; + lastMouseY = y; + lastMouseDown = -1; + + IComputer computer = owner.getComputer(); + if( computer != null ) computer.mouseUp( button, x, y ); + } + + @Override + public void mouseDrag( int button, int x, int y ) + { + lastMouseX = x; + lastMouseY = y; + lastMouseDown = button; + + IComputer computer = owner.getComputer(); + if( computer != null ) computer.mouseDrag( button, x, y ); + } + + @Override + public void mouseScroll( int direction, int x, int y ) + { + lastMouseX = x; + lastMouseY = y; + + IComputer computer = owner.getComputer(); + if( computer != null ) computer.mouseScroll( direction, x, y ); + } + + public void close() + { + IComputer computer = owner.getComputer(); + if( computer != null ) + { + IntIterator keys = keysDown.iterator(); + while( keys.hasNext() ) computer.keyUp( keys.nextInt() ); + + if( lastMouseDown != -1 ) computer.mouseUp( lastMouseDown, lastMouseX, lastMouseY ); + } + + keysDown.clear(); + lastMouseDown = -1; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java new file mode 100644 index 000000000..be969f4d2 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -0,0 +1,382 @@ +/* + * 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.computer.core; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.ComputerCraftAPIImpl; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.computer.Computer; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.computer.IComputerEnvironment; +import dan200.computercraft.shared.common.ServerTerminal; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.NetworkMessage; +import dan200.computercraft.shared.network.client.ComputerDataClientMessage; +import dan200.computercraft.shared.network.client.ComputerDeletedClientMessage; +import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.fml.server.ServerLifecycleHooks; +import net.minecraftforge.versions.mcp.MCPVersion; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.InputStream; + +public class ServerComputer extends ServerTerminal implements IComputer, IComputerEnvironment +{ + private final int m_instanceID; + + private World m_world; + private BlockPos m_position; + + private final ComputerFamily m_family; + private final Computer m_computer; + private CompoundTag m_userData; + private boolean m_changed; + + private boolean m_changedLastFrame; + private int m_ticksSincePing; + + public ServerComputer( World world, int computerID, String label, int instanceID, ComputerFamily family, int terminalWidth, int terminalHeight ) + { + super( family != ComputerFamily.NORMAL, terminalWidth, terminalHeight ); + m_instanceID = instanceID; + + m_world = world; + m_position = null; + + m_family = family; + m_computer = new Computer( this, getTerminal(), computerID ); + m_computer.setLabel( label ); + m_userData = null; + m_changed = false; + + m_changedLastFrame = false; + m_ticksSincePing = 0; + } + + public ComputerFamily getFamily() + { + return m_family; + } + + public World getWorld() + { + return m_world; + } + + public void setWorld( World world ) + { + m_world = world; + } + + public BlockPos getPosition() + { + return m_position; + } + + public void setPosition( BlockPos pos ) + { + m_position = new BlockPos( pos ); + } + + public IAPIEnvironment getAPIEnvironment() + { + return m_computer.getAPIEnvironment(); + } + + public Computer getComputer() + { + return m_computer; + } + + @Override + public void update() + { + super.update(); + m_computer.tick(); + + m_changedLastFrame = m_computer.pollAndResetChanged() || m_changed; + m_changed = false; + + m_ticksSincePing++; + } + + public void keepAlive() + { + m_ticksSincePing = 0; + } + + public boolean hasTimedOut() + { + return m_ticksSincePing > 100; + } + + public boolean hasOutputChanged() + { + return m_changedLastFrame; + } + + public void unload() + { + m_computer.unload(); + } + + public CompoundTag getUserData() + { + if( m_userData == null ) + { + m_userData = new CompoundTag(); + } + return m_userData; + } + + public void updateUserData() + { + m_changed = true; + } + + private NetworkMessage createComputerPacket() + { + return new ComputerDataClientMessage( this ); + } + + protected NetworkMessage createTerminalPacket() + { + return new ComputerTerminalClientMessage( getInstanceID(), write() ); + } + + public void broadcastState( boolean force ) + { + if( hasOutputChanged() || force ) + { + // Send computer state to all clients + NetworkHandler.sendToAllPlayers( createComputerPacket() ); + } + + if( hasTerminalChanged() || force ) + { + // Send terminal state to clients who are currently interacting with the computer. + MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); + + NetworkMessage packet = null; + for( PlayerEntity player : server.getPlayerManager().getPlayerList() ) + { + if( isInteracting( player ) ) + { + if( packet == null ) packet = createTerminalPacket(); + NetworkHandler.sendToPlayer( player, packet ); + } + } + } + } + + public void sendComputerState( PlayerEntity player ) + { + // Send state to client + NetworkHandler.sendToPlayer( player, createComputerPacket() ); + } + + public void sendTerminalState( PlayerEntity player ) + { + // Send terminal state to client + NetworkHandler.sendToPlayer( player, createTerminalPacket() ); + } + + public void broadcastDelete() + { + // Send deletion to client + NetworkHandler.sendToAllPlayers( new ComputerDeletedClientMessage( getInstanceID() ) ); + } + + public void setID( int id ) + { + m_computer.setID( id ); + } + + // IComputer + + @Override + public int getInstanceID() + { + return m_instanceID; + } + + public int getID() + { + return m_computer.getID(); + } + + public String getLabel() + { + return m_computer.getLabel(); + } + + @Override + public boolean isOn() + { + return m_computer.isOn(); + } + + @Override + public boolean isCursorDisplayed() + { + return m_computer.isOn() && m_computer.isBlinking(); + } + + @Override + public void turnOn() + { + // Turn on + m_computer.turnOn(); + } + + @Override + public void shutdown() + { + // Shutdown + m_computer.shutdown(); + } + + @Override + public void reboot() + { + // Reboot + m_computer.reboot(); + } + + @Override + public void queueEvent( String event, Object[] arguments ) + { + // Queue event + m_computer.queueEvent( event, arguments ); + } + + public int getRedstoneOutput( ComputerSide side ) + { + return m_computer.getEnvironment().getExternalRedstoneOutput( side ); + } + + public void setRedstoneInput( ComputerSide side, int level ) + { + m_computer.getEnvironment().setRedstoneInput( side, level ); + } + + public int getBundledRedstoneOutput( ComputerSide side ) + { + return m_computer.getEnvironment().getExternalBundledRedstoneOutput( side ); + } + + public void setBundledRedstoneInput( ComputerSide side, int combination ) + { + m_computer.getEnvironment().setBundledRedstoneInput( side, combination ); + } + + public void addAPI( ILuaAPI api ) + { + m_computer.addApi( api ); + } + + public void setPeripheral( ComputerSide side, IPeripheral peripheral ) + { + m_computer.getEnvironment().setPeripheral( side, peripheral ); + } + + public IPeripheral getPeripheral( ComputerSide side ) + { + return m_computer.getEnvironment().getPeripheral( side ); + } + + public void setLabel( String label ) + { + m_computer.setLabel( label ); + } + + // IComputerEnvironment implementation + + @Override + public double getTimeOfDay() + { + return (m_world.getTimeOfDay() + 6000) % 24000 / 1000.0; + } + + @Override + public int getDay() + { + return (int) ((m_world.getTimeOfDay() + 6000) / 24000) + 1; + } + + @Override + public IWritableMount createSaveDirMount( String subPath, long capacity ) + { + return ComputerCraftAPI.createSaveDirMount( m_world, subPath, capacity ); + } + + @Override + public IMount createResourceMount( String domain, String subPath ) + { + return ComputerCraftAPI.createResourceMount( domain, subPath ); + } + + @Override + public InputStream createResourceFile( String domain, String subPath ) + { + return ComputerCraftAPIImpl.getResourceFile( domain, subPath ); + } + + @Override + public long getComputerSpaceLimit() + { + return ComputerCraft.computerSpaceLimit; + } + + @Nonnull + @Override + public String getHostString() + { + return String.format( "ComputerCraft %s (Minecraft %s)", ComputerCraftAPI.getInstalledVersion(), MCPVersion.getMCVersion() ); + } + + @Nonnull + @Override + public String getUserAgent() + { + return ComputerCraft.MOD_ID + "/" + ComputerCraftAPI.getInstalledVersion(); + } + + @Override + public int assignNewID() + { + return ComputerCraftAPI.createUniqueNumberedSaveDir( m_world, "computer" ); + } + + @Nullable + public IContainerComputer getContainer( PlayerEntity player ) + { + if( player == null ) return null; + + ScreenHandler container = player.currentScreenHandler; + if( !(container instanceof IContainerComputer) ) return null; + + IContainerComputer computerContainer = (IContainerComputer) container; + return computerContainer.getComputer() != this ? null : computerContainer; + } + + protected boolean isInteracting( PlayerEntity player ) + { + return getContainer( player ) != null; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java new file mode 100644 index 000000000..ab605420c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java @@ -0,0 +1,82 @@ +/* + * 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.computer.core; + +import java.util.Iterator; + +public class ServerComputerRegistry extends ComputerRegistry +{ + public void update() + { + Iterator it = getComputers().iterator(); + while( it.hasNext() ) + { + ServerComputer computer = it.next(); + if( computer.hasTimedOut() ) + { + //System.out.println( "TIMED OUT SERVER COMPUTER " + computer.getInstanceID() ); + computer.unload(); + computer.broadcastDelete(); + it.remove(); + //System.out.println( getComputers().size() + " SERVER COMPUTERS" ); + } + else + { + computer.update(); + if( computer.hasTerminalChanged() || computer.hasOutputChanged() ) + { + computer.broadcastState( false ); + } + } + } + } + + @Override + public void add( int instanceID, ServerComputer computer ) + { + //System.out.println( "ADD SERVER COMPUTER " + instanceID ); + super.add( instanceID, computer ); + computer.broadcastState( true ); + //System.out.println( getComputers().size() + " SERVER COMPUTERS" ); + } + + @Override + public void remove( int instanceID ) + { + //System.out.println( "REMOVE SERVER COMPUTER " + instanceID ); + ServerComputer computer = get( instanceID ); + if( computer != null ) + { + computer.unload(); + computer.broadcastDelete(); + } + super.remove( instanceID ); + //System.out.println( getComputers().size() + " SERVER COMPUTERS" ); + } + + @Override + public void reset() + { + //System.out.println( "RESET SERVER COMPUTERS" ); + for( ServerComputer computer : getComputers() ) + { + computer.unload(); + } + super.reset(); + //System.out.println( getComputers().size() + " SERVER COMPUTERS" ); + } + + public ServerComputer lookup( int computerID ) + { + if( computerID < 0 ) return null; + + for( ServerComputer computer : getComputers() ) + { + if( computer.getID() == computerID ) return computer; + } + return null; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputer.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputer.java new file mode 100644 index 000000000..9ce948931 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputer.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.computer.inventory; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.computer.blocks.TileComputer; +import dan200.computercraft.shared.network.container.ComputerContainerData; +import net.minecraft.entity.player.PlayerInventory; + +public class ContainerComputer extends ContainerComputerBase +{ + public ContainerComputer( int id, TileComputer tile ) + { + super( Registry.ModContainers.COMPUTER.get(), id, tile::isUsableByPlayer, tile.createServerComputer(), tile.getFamily() ); + } + + public ContainerComputer( int id, PlayerInventory player, ComputerContainerData data ) + { + super( Registry.ModContainers.COMPUTER.get(), id, player, data ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java new file mode 100644 index 000000000..82f2cc760 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java @@ -0,0 +1,81 @@ +/* + * 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.computer.inventory; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.*; +import dan200.computercraft.shared.network.container.ComputerContainerData; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.function.Predicate; + +public class ContainerComputerBase extends ScreenHandler implements IContainerComputer +{ + private final Predicate canUse; + private final IComputer computer; + private final ComputerFamily family; + private final InputState input = new InputState( this ); + + protected ContainerComputerBase( ScreenHandlerType type, int id, Predicate canUse, IComputer computer, ComputerFamily family ) + { + super( type, id ); + this.canUse = canUse; + this.computer = computer; + this.family = family; + } + + protected ContainerComputerBase( ScreenHandlerType type, int id, PlayerInventory player, ComputerContainerData data ) + { + this( type, id, x -> true, getComputer( player, data ), data.getFamily() ); + } + + protected static IComputer getComputer( PlayerInventory player, ComputerContainerData data ) + { + int id = data.getInstanceId(); + if( !player.player.world.isClient ) return ComputerCraft.serverComputerRegistry.get( id ); + + ClientComputer computer = ComputerCraft.clientComputerRegistry.get( id ); + if( computer == null ) ComputerCraft.clientComputerRegistry.add( id, computer = new ClientComputer( id ) ); + return computer; + } + + @Override + public boolean canUse( @Nonnull PlayerEntity player ) + { + return canUse.test( player ); + } + + @Nonnull + public ComputerFamily getFamily() + { + return family; + } + + @Nullable + @Override + public IComputer getComputer() + { + return computer; + } + + @Nonnull + @Override + public InputState getInput() + { + return input; + } + + @Override + public void close( @Nonnull PlayerEntity player ) + { + super.close( player ); + input.close(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java new file mode 100644 index 000000000..ccc8b24d9 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java @@ -0,0 +1,64 @@ +/* + * 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.computer.inventory; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.computer.blocks.TileCommandComputer; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.IContainerComputer; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.network.container.ViewComputerContainerData; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; + +import javax.annotation.Nonnull; + +public class ContainerViewComputer extends ContainerComputerBase implements IContainerComputer +{ + private final int width; + private final int height; + + public ContainerViewComputer( int id, ServerComputer computer ) + { + super( Registry.ModContainers.VIEW_COMPUTER.get(), id, player -> canInteractWith( computer, player ), computer, computer.getFamily() ); + this.width = this.height = 0; + } + + public ContainerViewComputer( int id, PlayerInventory player, ViewComputerContainerData data ) + { + super( Registry.ModContainers.VIEW_COMPUTER.get(), id, player, data ); + this.width = data.getWidth(); + this.height = data.getHeight(); + } + + private static boolean canInteractWith( @Nonnull ServerComputer computer, @Nonnull PlayerEntity player ) + { + // If this computer no longer exists then discard it. + if( ComputerCraft.serverComputerRegistry.get( computer.getInstanceID() ) != computer ) + { + return false; + } + + // If we're a command computer then ensure we're in creative + if( computer.getFamily() == ComputerFamily.COMMAND && !TileCommandComputer.isUsable( player ) ) + { + return false; + } + + return true; + } + + public int getWidth() + { + return width; + } + + public int getHeight() + { + return height; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/items/ComputerItemFactory.java b/src/main/java/dan200/computercraft/shared/computer/items/ComputerItemFactory.java new file mode 100644 index 000000000..3be415994 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/items/ComputerItemFactory.java @@ -0,0 +1,40 @@ +/* + * 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.computer.items; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.computer.blocks.TileComputer; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; + +public final class ComputerItemFactory +{ + private ComputerItemFactory() {} + + @Nonnull + public static ItemStack create( TileComputer tile ) + { + return create( tile.getComputerID(), tile.getLabel(), tile.getFamily() ); + } + + @Nonnull + public static ItemStack create( int id, String label, ComputerFamily family ) + { + switch( family ) + { + case NORMAL: + return Registry.ModItems.COMPUTER_NORMAL.get().create( id, label ); + case ADVANCED: + return Registry.ModItems.COMPUTER_ADVANCED.get().create( id, label ); + case COMMAND: + return Registry.ModItems.COMPUTER_COMMAND.get().create( id, label ); + default: + return ItemStack.EMPTY; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/items/IComputerItem.java b/src/main/java/dan200/computercraft/shared/computer/items/IComputerItem.java new file mode 100644 index 000000000..98acc0ee2 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/items/IComputerItem.java @@ -0,0 +1,31 @@ +/* + * 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.computer.items; + +import dan200.computercraft.shared.computer.core.ComputerFamily; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import javax.annotation.Nonnull; + +public interface IComputerItem +{ + String NBT_ID = "ComputerId"; + + default int getComputerID( @Nonnull ItemStack stack ) + { + CompoundTag nbt = stack.getTag(); + return nbt != null && nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1; + } + + default String getLabel( @Nonnull ItemStack stack ) + { + return stack.hasCustomName() ? stack.getName().getString() : null; + } + + ComputerFamily getFamily(); + + ItemStack withFamily( @Nonnull ItemStack stack, @Nonnull ComputerFamily family ); +} diff --git a/src/main/java/dan200/computercraft/shared/computer/items/ItemComputer.java b/src/main/java/dan200/computercraft/shared/computer/items/ItemComputer.java new file mode 100644 index 000000000..739e497a8 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/items/ItemComputer.java @@ -0,0 +1,36 @@ +/* + * 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.computer.items; + +import dan200.computercraft.shared.computer.blocks.BlockComputer; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import net.minecraft.item.ItemStack; +import net.minecraft.text.LiteralText; +import javax.annotation.Nonnull; + +public class ItemComputer extends ItemComputerBase +{ + public ItemComputer( BlockComputer block, Settings settings ) + { + super( block, settings ); + } + + public ItemStack create( int id, String label ) + { + ItemStack result = new ItemStack( this ); + if( id >= 0 ) result.getOrCreateTag().putInt( NBT_ID, id ); + if( label != null ) result.setCustomName( new LiteralText( label ) ); + return result; + } + + @Override + public ItemStack withFamily( @Nonnull ItemStack stack, @Nonnull ComputerFamily family ) + { + ItemStack result = ComputerItemFactory.create( getComputerID( stack ), null, family ); + if( stack.hasCustomName() ) result.setCustomName( stack.getName() ); + return result; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/items/ItemComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/items/ItemComputerBase.java new file mode 100644 index 000000000..3089d4168 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/items/ItemComputerBase.java @@ -0,0 +1,93 @@ +/* + * 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.computer.items; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.shared.computer.blocks.BlockComputerBase; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.item.BlockItem; +import net.minecraft.item.ItemStack; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; + +public abstract class ItemComputerBase extends BlockItem implements IComputerItem, IMedia +{ + private final ComputerFamily family; + + public ItemComputerBase( BlockComputerBase block, Settings settings ) + { + super( block, settings ); + family = block.getFamily(); + } + + @Override + public void appendTooltip( @Nonnull ItemStack stack, @Nullable World world, @Nonnull List list, @Nonnull TooltipContext options ) + { + if( options.isAdvanced() || getLabel( stack ) == null ) + { + int id = getComputerID( stack ); + if( id >= 0 ) + { + list.add( new TranslatableText( "gui.computercraft.tooltip.computer_id", id ) + .formatted( Formatting.GRAY ) ); + } + } + } + + @Override + public String getLabel( @Nonnull ItemStack stack ) + { + return IComputerItem.super.getLabel( stack ); + } + + @Override + public final ComputerFamily getFamily() + { + return family; + } + + // IMedia implementation + + @Override + public boolean setLabel( @Nonnull ItemStack stack, String label ) + { + if( label != null ) + { + stack.setCustomName( new LiteralText( label ) ); + } + else + { + stack.removeCustomName(); + } + return true; + } + + @Override + public IMount createDataMount( @Nonnull ItemStack stack, @Nonnull World world ) + { + ComputerFamily family = getFamily(); + if( family != ComputerFamily.COMMAND ) + { + int id = getComputerID( stack ); + if( id >= 0 ) + { + return ComputerCraftAPI.createSaveDirMount( world, "computer/" + id, ComputerCraft.computerSpaceLimit ); + } + } + return null; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerConvertRecipe.java b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerConvertRecipe.java new file mode 100644 index 000000000..1a13ce31c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerConvertRecipe.java @@ -0,0 +1,68 @@ +/* + * 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.computer.recipe; + +import dan200.computercraft.shared.computer.items.IComputerItem; +import net.minecraft.inventory.CraftingInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.ShapedRecipe; +import net.minecraft.util.Identifier; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +/** + * Represents a recipe which converts a computer from one form into another. + */ +public abstract class ComputerConvertRecipe extends ShapedRecipe +{ + private final String group; + + public ComputerConvertRecipe( Identifier identifier, String group, int width, int height, DefaultedList ingredients, ItemStack result ) + { + super( identifier, group, width, height, ingredients, result ); + this.group = group; + } + + @Nonnull + protected abstract ItemStack convert( @Nonnull IComputerItem item, @Nonnull ItemStack stack ); + + @Override + public boolean matches( @Nonnull CraftingInventory inventory, @Nonnull World world ) + { + if( !super.matches( inventory, world ) ) return false; + + for( int i = 0; i < inventory.size(); i++ ) + { + if( inventory.getStack( i ).getItem() instanceof IComputerItem ) return true; + } + + return false; + } + + @Nonnull + @Override + public ItemStack craft( @Nonnull CraftingInventory inventory ) + { + // Find our computer item and convert it. + for( int i = 0; i < inventory.size(); i++ ) + { + ItemStack stack = inventory.getStack( i ); + if( stack.getItem() instanceof IComputerItem ) return convert( (IComputerItem) stack.getItem(), stack ); + } + + return ItemStack.EMPTY; + } + + @Nonnull + @Override + public String getGroup() + { + return group; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerFamilyRecipe.java b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerFamilyRecipe.java new file mode 100644 index 000000000..4d37488dd --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerFamilyRecipe.java @@ -0,0 +1,79 @@ +/* + * 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.computer.recipe; + +import com.google.gson.JsonObject; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.util.BasicRecipeSerializer; +import dan200.computercraft.shared.util.RecipeUtil; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.recipe.Ingredient; +import net.minecraft.util.Identifier; +import net.minecraft.util.JsonHelper; +import net.minecraft.util.collection.DefaultedList; +import javax.annotation.Nonnull; + +public abstract class ComputerFamilyRecipe extends ComputerConvertRecipe +{ + private final ComputerFamily family; + + public ComputerFamilyRecipe( Identifier identifier, String group, int width, int height, DefaultedList ingredients, ItemStack result, ComputerFamily family ) + { + super( identifier, group, width, height, ingredients, result ); + this.family = family; + } + + public ComputerFamily getFamily() + { + return family; + } + + public abstract static class Serializer extends BasicRecipeSerializer + { + protected abstract T create( Identifier identifier, String group, int width, int height, DefaultedList ingredients, ItemStack result, ComputerFamily family ); + + @Nonnull + @Override + public T read( @Nonnull Identifier identifier, @Nonnull JsonObject json ) + { + String group = JsonHelper.getString( json, "group", "" ); + ComputerFamily family = RecipeUtil.getFamily( json, "family" ); + + RecipeUtil.ShapedTemplate template = RecipeUtil.getTemplate( json ); + ItemStack result = getItemStack( JsonHelper.getObject( json, "result" ) ); + + return create( identifier, group, template.width, template.height, template.ingredients, result, family ); + } + + @Nonnull + @Override + public T read( @Nonnull Identifier identifier, @Nonnull PacketByteBuf buf ) + { + int width = buf.readVarInt(); + int height = buf.readVarInt(); + String group = buf.readString( Short.MAX_VALUE ); + + DefaultedList ingredients = DefaultedList.ofSize( width * height, Ingredient.EMPTY ); + for( int i = 0; i < ingredients.size(); i++ ) ingredients.set( i, Ingredient.fromPacket( buf ) ); + + ItemStack result = buf.readItemStack(); + ComputerFamily family = buf.readEnumConstant( ComputerFamily.class ); + return create( identifier, group, width, height, ingredients, result, family ); + } + + @Override + public void write( @Nonnull PacketByteBuf buf, @Nonnull T recipe ) + { + buf.writeVarInt( recipe.getWidth() ); + buf.writeVarInt( recipe.getHeight() ); + buf.writeString( recipe.getGroup() ); + for( Ingredient ingredient : recipe.getPreviewInputs() ) ingredient.write( buf ); + buf.writeItemStack( recipe.getOutput() ); + buf.writeEnumConstant( recipe.getFamily() ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerUpgradeRecipe.java b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerUpgradeRecipe.java new file mode 100644 index 000000000..10b519a12 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerUpgradeRecipe.java @@ -0,0 +1,46 @@ +/* + * 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.computer.recipe; + +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.items.IComputerItem; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.util.Identifier; +import net.minecraft.util.collection.DefaultedList; +import javax.annotation.Nonnull; + +public class ComputerUpgradeRecipe extends ComputerFamilyRecipe +{ + public ComputerUpgradeRecipe( Identifier identifier, String group, int width, int height, DefaultedList ingredients, ItemStack result, ComputerFamily family ) + { + super( identifier, group, width, height, ingredients, result, family ); + } + + @Nonnull + @Override + protected ItemStack convert( @Nonnull IComputerItem item, @Nonnull ItemStack stack ) + { + return item.withFamily( stack, getFamily() ); + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + public static final RecipeSerializer SERIALIZER = new dan200.computercraft.shared.computer.recipe.ComputerFamilyRecipe.Serializer() + { + @Override + protected ComputerUpgradeRecipe create( Identifier identifier, String group, int width, int height, DefaultedList ingredients, ItemStack result, ComputerFamily family ) + { + return new ComputerUpgradeRecipe( identifier, group, width, height, ingredients, result, family ); + } + }; +} diff --git a/src/main/java/dan200/computercraft/shared/data/BlockNamedEntityLootCondition.java b/src/main/java/dan200/computercraft/shared/data/BlockNamedEntityLootCondition.java new file mode 100644 index 000000000..9a1dea7d9 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/data/BlockNamedEntityLootCondition.java @@ -0,0 +1,52 @@ +/* + * 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.data; + +import javax.annotation.Nonnull; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.loot.condition.LootCondition; +import net.minecraft.loot.condition.LootConditionType; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.context.LootContextParameter; +import net.minecraft.loot.context.LootContextParameters; +import net.minecraft.util.Nameable; +import java.util.Collections; +import java.util.Set; + +/** + * A loot condition which checks if the tile entity has a name. + */ +public final class BlockNamedEntityLootCondition implements LootCondition +{ + public static final BlockNamedEntityLootCondition INSTANCE = new BlockNamedEntityLootCondition(); + public static final LootConditionType TYPE = ConstantLootConditionSerializer.type( INSTANCE ); + public static final Builder BUILDER = () -> INSTANCE; + + private BlockNamedEntityLootCondition() + { + } + + @Override + public boolean test( LootContext lootContext ) + { + BlockEntity tile = lootContext.get( LootContextParameters.BLOCK_ENTITY ); + return tile instanceof Nameable && ((Nameable) tile).hasCustomName(); + } + + @Nonnull + @Override + public Set> getRequiredParameters() + { + return Collections.singleton( LootContextParameters.BLOCK_ENTITY ); + } + + @Override + @Nonnull + public LootConditionType getType() + { + return TYPE; + } +} diff --git a/src/main/java/dan200/computercraft/shared/data/ConstantLootConditionSerializer.java b/src/main/java/dan200/computercraft/shared/data/ConstantLootConditionSerializer.java new file mode 100644 index 000000000..664628406 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/data/ConstantLootConditionSerializer.java @@ -0,0 +1,41 @@ +/* + * 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.data; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import javax.annotation.Nonnull; +import net.minecraft.loot.condition.LootCondition; +import net.minecraft.loot.condition.LootConditionType; +import net.minecraft.util.JsonSerializer; + +public final class ConstantLootConditionSerializer implements JsonSerializer +{ + private final T instance; + + public ConstantLootConditionSerializer( T instance ) + { + this.instance = instance; + } + + public static LootConditionType type( T condition ) + { + return new LootConditionType( new ConstantLootConditionSerializer<>( condition ) ); + } + + @Override + public void func_230424_a_( @Nonnull JsonObject json, @Nonnull T object, @Nonnull JsonSerializationContext context ) + { + } + + @Nonnull + @Override + public T fromJson( @Nonnull JsonObject json, @Nonnull JsonDeserializationContext context ) + { + return instance; + } +} diff --git a/src/main/java/dan200/computercraft/shared/data/HasComputerIdLootCondition.java b/src/main/java/dan200/computercraft/shared/data/HasComputerIdLootCondition.java new file mode 100644 index 000000000..cebe22841 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/data/HasComputerIdLootCondition.java @@ -0,0 +1,52 @@ +/* + * 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.data; + +import dan200.computercraft.shared.computer.blocks.IComputerTile; +import javax.annotation.Nonnull; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.loot.condition.LootCondition; +import net.minecraft.loot.condition.LootConditionType; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.context.LootContextParameter; +import net.minecraft.loot.context.LootContextParameters; +import java.util.Collections; +import java.util.Set; + +/** + * A loot condition which checks if the tile entity has has a non-0 ID. + */ +public final class HasComputerIdLootCondition implements LootCondition +{ + public static final HasComputerIdLootCondition INSTANCE = new HasComputerIdLootCondition(); + public static final LootConditionType TYPE = ConstantLootConditionSerializer.type( INSTANCE ); + public static final Builder BUILDER = () -> INSTANCE; + + private HasComputerIdLootCondition() + { + } + + @Override + public boolean test( LootContext lootContext ) + { + BlockEntity tile = lootContext.get( LootContextParameters.BLOCK_ENTITY ); + return tile instanceof IComputerTile && ((IComputerTile) tile).getComputerID() >= 0; + } + + @Nonnull + @Override + public Set> getRequiredParameters() + { + return Collections.singleton( LootContextParameters.BLOCK_ENTITY ); + } + + @Override + @Nonnull + public LootConditionType getType() + { + return TYPE; + } +} diff --git a/src/main/java/dan200/computercraft/shared/data/PlayerCreativeLootCondition.java b/src/main/java/dan200/computercraft/shared/data/PlayerCreativeLootCondition.java new file mode 100644 index 000000000..056b181df --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/data/PlayerCreativeLootCondition.java @@ -0,0 +1,52 @@ +/* + * 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.data; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.loot.condition.LootCondition; +import net.minecraft.loot.condition.LootConditionType; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.context.LootContextParameter; +import net.minecraft.loot.context.LootContextParameters; +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Set; + +/** + * A loot condition which checks if the entity is in creative mode. + */ +public final class PlayerCreativeLootCondition implements LootCondition +{ + public static final PlayerCreativeLootCondition INSTANCE = new PlayerCreativeLootCondition(); + public static final LootConditionType TYPE = ConstantLootConditionSerializer.type( INSTANCE ); + public static final Builder BUILDER = () -> INSTANCE; + + private PlayerCreativeLootCondition() + { + } + + @Override + public boolean test( LootContext lootContext ) + { + Entity entity = lootContext.get( LootContextParameters.THIS_ENTITY ); + return entity instanceof PlayerEntity && ((PlayerEntity) entity).abilities.creativeMode; + } + + @Nonnull + @Override + public Set> getRequiredParameters() + { + return Collections.singleton( LootContextParameters.THIS_ENTITY ); + } + + @Override + @Nonnull + public LootConditionType getType() + { + return TYPE; + } +} diff --git a/src/main/java/dan200/computercraft/shared/integration/crafttweaker/TrackingLogger.java b/src/main/java/dan200/computercraft/shared/integration/crafttweaker/TrackingLogger.java new file mode 100644 index 000000000..07733df89 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/integration/crafttweaker/TrackingLogger.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.integration.crafttweaker; + +import com.blamejared.crafttweaker.api.logger.ILogger; + +/** + * Logger which tracks if it has any messages. + */ +public final class TrackingLogger +{ + private final ILogger logger; + private boolean ok = true; + + public TrackingLogger( ILogger logger ) + { + this.logger = logger; + } + + public boolean isOk() + { + return ok; + } + + public void warning( String message ) + { + ok = false; + logger.warning( message ); + } + + public void error( String message ) + { + ok = false; + logger.error( message ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/integration/crafttweaker/TurtleTweaker.java b/src/main/java/dan200/computercraft/shared/integration/crafttweaker/TurtleTweaker.java new file mode 100644 index 000000000..d3b9345d4 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/integration/crafttweaker/TurtleTweaker.java @@ -0,0 +1,71 @@ +/* + * 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.integration.crafttweaker; + +import com.blamejared.crafttweaker.api.CraftTweakerAPI; +import com.blamejared.crafttweaker.api.annotations.ZenRegister; +import com.blamejared.crafttweaker.api.item.IItemStack; +import dan200.computercraft.shared.integration.crafttweaker.actions.AddTurtleTool; +import dan200.computercraft.shared.integration.crafttweaker.actions.RemoveTurtleUpgradeByItem; +import dan200.computercraft.shared.integration.crafttweaker.actions.RemoveTurtleUpgradeByName; +import org.openzen.zencode.java.ZenCodeType; + +@ZenRegister +@ZenCodeType.Name( "dan200.computercraft.turtle" ) +public class TurtleTweaker +{ + /** + * Remove a turtle upgrade with the given id. + * + * @param upgrade The ID of the to remove + */ + @ZenCodeType.Method + public static void removeUpgrade( String upgrade ) + { + CraftTweakerAPI.apply( new RemoveTurtleUpgradeByName( upgrade ) ); + } + + /** + * Remove a turtle upgrade crafted with the given item stack". + * + * @param stack The stack with which the upgrade is crafted. + */ + @ZenCodeType.Method + public static void removeUpgrade( IItemStack stack ) + { + CraftTweakerAPI.apply( new RemoveTurtleUpgradeByItem( stack.getInternal() ) ); + } + + /** + * Add a new turtle tool with the given id, which crafts and acts using the given stack. + * + * @param id The new upgrade's ID + * @param stack The stack used for crafting the upgrade and used by the turtle as a tool. + */ + @ZenCodeType.Method + public static void addTool( String id, IItemStack stack ) + { + addTool( id, stack, stack, "tool" ); + } + + @ZenCodeType.Method + public static void addTool( String id, IItemStack craftingStack, IItemStack toolStack ) + { + addTool( id, craftingStack, toolStack, "tool" ); + } + + @ZenCodeType.Method + public static void addTool( String id, IItemStack stack, String kind ) + { + addTool( id, stack, stack, kind ); + } + + @ZenCodeType.Method + public static void addTool( String id, IItemStack craftingStack, IItemStack toolStack, String kind ) + { + CraftTweakerAPI.apply( new AddTurtleTool( id, craftingStack.getInternal(), toolStack.getInternal(), kind ) ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/integration/crafttweaker/actions/AddTurtleTool.java b/src/main/java/dan200/computercraft/shared/integration/crafttweaker/actions/AddTurtleTool.java new file mode 100644 index 000000000..e61e91b90 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/integration/crafttweaker/actions/AddTurtleTool.java @@ -0,0 +1,126 @@ +/* + * 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.integration.crafttweaker.actions; + +import com.blamejared.crafttweaker.api.actions.IUndoableAction; +import com.blamejared.crafttweaker.api.logger.ILogger; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.shared.TurtleUpgrades; +import dan200.computercraft.shared.integration.crafttweaker.TrackingLogger; +import dan200.computercraft.shared.turtle.upgrades.*; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraftforge.fml.LogicalSide; + +import java.util.HashMap; +import java.util.Map; + +/** + * Register a new turtle tool. + */ +public class AddTurtleTool implements IUndoableAction +{ + private interface Factory + { + TurtleTool create( Identifier location, ItemStack craftItem, ItemStack toolItem ); + } + + private static final Map kinds = new HashMap<>(); + + static + { + kinds.put( "tool", TurtleTool::new ); + kinds.put( "axe", TurtleAxe::new ); + kinds.put( "hoe", TurtleHoe::new ); + kinds.put( "shovel", TurtleShovel::new ); + kinds.put( "sword", TurtleSword::new ); + } + + private final String id; + private final ItemStack craftItem; + private final ItemStack toolItem; + private final String kind; + + private ITurtleUpgrade upgrade; + + public AddTurtleTool( String id, ItemStack craftItem, ItemStack toolItem, String kind ) + { + this.id = id; + this.craftItem = craftItem; + this.toolItem = toolItem; + this.kind = kind; + } + + @Override + public void apply() + { + ITurtleUpgrade upgrade = this.upgrade; + if( upgrade == null ) + { + Factory factory = kinds.get( kind ); + if( factory == null ) + { + ComputerCraft.log.error( "Unknown turtle upgrade kind '{}' (this should have been rejected by verify!)", kind ); + return; + } + + upgrade = this.upgrade = factory.create( new Identifier( id ), craftItem, toolItem ); + } + + try + { + TurtleUpgrades.register( upgrade ); + } + catch( RuntimeException e ) + { + ComputerCraft.log.error( "Registration of turtle tool failed", e ); + } + } + + @Override + public String describe() + { + return String.format( "Add new turtle %s '%s' (crafted with '%s', uses a '%s')", kind, id, craftItem, toolItem ); + } + + @Override + public void undo() + { + if( upgrade != null ) TurtleUpgrades.remove( upgrade ); + } + + @Override + public String describeUndo() + { + return String.format( "Removing turtle upgrade %s.", id ); + } + + public boolean validate( ILogger logger ) + { + TrackingLogger trackLog = new TrackingLogger( logger ); + + if( craftItem.isEmpty() ) trackLog.warning( "Crafting item stack is empty." ); + + if( craftItem.hasTag() && !craftItem.getTag().isEmpty() ) trackLog.warning( "Crafting item has NBT." ); + if( toolItem.isEmpty() ) trackLog.error( "Tool item stack is empty." ); + + if( !kinds.containsKey( kind ) ) trackLog.error( String.format( "Unknown kind '%s'.", kind ) ); + + if( TurtleUpgrades.get( id ) != null ) + { + trackLog.error( String.format( "An upgrade with the same name ('%s') has already been registered.", id ) ); + } + + return trackLog.isOk(); + } + + @Override + public boolean shouldApplyOn( LogicalSide side ) + { + return true; + } +} diff --git a/src/main/java/dan200/computercraft/shared/integration/crafttweaker/actions/RemoveTurtleUpgradeByItem.java b/src/main/java/dan200/computercraft/shared/integration/crafttweaker/actions/RemoveTurtleUpgradeByItem.java new file mode 100644 index 000000000..5c1cc0aec --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/integration/crafttweaker/actions/RemoveTurtleUpgradeByItem.java @@ -0,0 +1,70 @@ +/* + * 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.integration.crafttweaker.actions; + +import com.blamejared.crafttweaker.api.actions.IUndoableAction; +import com.blamejared.crafttweaker.api.logger.ILogger; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.shared.TurtleUpgrades; +import net.minecraft.item.ItemStack; +import net.minecraftforge.fml.LogicalSide; + +/** + * Removes a turtle upgrade crafted with the given stack. + */ +public class RemoveTurtleUpgradeByItem implements IUndoableAction +{ + private final ItemStack stack; + private ITurtleUpgrade upgrade; + + public RemoveTurtleUpgradeByItem( ItemStack stack ) + { + this.stack = stack; + } + + @Override + public void apply() + { + ITurtleUpgrade upgrade = this.upgrade = TurtleUpgrades.get( stack ); + if( upgrade != null ) TurtleUpgrades.disable( upgrade ); + } + + @Override + public String describe() + { + return String.format( "Remove turtle upgrades crafted with '%s'", stack ); + } + + @Override + public void undo() + { + if( this.upgrade != null ) TurtleUpgrades.enable( upgrade ); + } + + @Override + public String describeUndo() + { + return String.format( "Adding back turtle upgrades crafted with '%s'", stack ); + } + + @Override + public boolean validate( ILogger logger ) + { + if( TurtleUpgrades.get( stack ) == null ) + { + logger.error( String.format( "Unknown turtle upgrade crafted with '%s'.", stack ) ); + return false; + } + + return true; + } + + @Override + public boolean shouldApplyOn( LogicalSide side ) + { + return true; + } +} diff --git a/src/main/java/dan200/computercraft/shared/integration/crafttweaker/actions/RemoveTurtleUpgradeByName.java b/src/main/java/dan200/computercraft/shared/integration/crafttweaker/actions/RemoveTurtleUpgradeByName.java new file mode 100644 index 000000000..47158be7f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/integration/crafttweaker/actions/RemoveTurtleUpgradeByName.java @@ -0,0 +1,69 @@ +/* + * 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.integration.crafttweaker.actions; + +import com.blamejared.crafttweaker.api.actions.IUndoableAction; +import com.blamejared.crafttweaker.api.logger.ILogger; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.shared.TurtleUpgrades; +import net.minecraftforge.fml.LogicalSide; + +/** + * Removes a turtle upgrade with the given id. + */ +public class RemoveTurtleUpgradeByName implements IUndoableAction +{ + private final String id; + private ITurtleUpgrade upgrade; + + public RemoveTurtleUpgradeByName( String id ) + { + this.id = id; + } + + @Override + public void apply() + { + ITurtleUpgrade upgrade = this.upgrade = TurtleUpgrades.get( id ); + if( upgrade != null ) TurtleUpgrades.disable( upgrade ); + } + + @Override + public String describe() + { + return String.format( "Remove turtle upgrade '%s'", id ); + } + + @Override + public void undo() + { + if( this.upgrade != null ) TurtleUpgrades.enable( upgrade ); + } + + @Override + public String describeUndo() + { + return String.format( "Adding back turtle upgrade '%s'", id ); + } + + @Override + public boolean validate( ILogger logger ) + { + if( TurtleUpgrades.get( id ) == null ) + { + logger.error( String.format( "Unknown turtle upgrade '%s'.", id ) ); + return false; + } + + return true; + } + + @Override + public boolean shouldApplyOn( LogicalSide side ) + { + return true; + } +} diff --git a/src/main/java/dan200/computercraft/shared/integration/jei/JEIComputerCraft.java b/src/main/java/dan200/computercraft/shared/integration/jei/JEIComputerCraft.java new file mode 100644 index 000000000..5457f2b2a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/integration/jei/JEIComputerCraft.java @@ -0,0 +1,156 @@ +/* + * 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.integration.jei; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.PocketUpgrades; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.TurtleUpgrades; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.media.items.ItemDisk; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import dan200.computercraft.shared.pocket.items.PocketComputerItemFactory; +import dan200.computercraft.shared.turtle.items.ITurtleItem; +import dan200.computercraft.shared.turtle.items.TurtleItemFactory; +import mezz.jei.api.IModPlugin; +import mezz.jei.api.JeiPlugin; +import mezz.jei.api.constants.VanillaRecipeCategoryUid; +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.ingredients.subtypes.ISubtypeInterpreter; +import mezz.jei.api.recipe.IRecipeManager; +import mezz.jei.api.recipe.category.IRecipeCategory; +import mezz.jei.api.registration.IAdvancedRegistration; +import mezz.jei.api.registration.ISubtypeRegistration; +import mezz.jei.api.runtime.IJeiRuntime; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.Recipe; +import net.minecraft.util.Identifier; +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +import static dan200.computercraft.shared.integration.jei.RecipeResolver.MAIN_FAMILIES; + +@JeiPlugin +public class JEIComputerCraft implements IModPlugin +{ + @Nonnull + @Override + public Identifier getPluginUid() + { + return new Identifier( ComputerCraft.MOD_ID, "jei" ); + } + + @Override + public void registerItemSubtypes( ISubtypeRegistration subtypeRegistry ) + { + subtypeRegistry.registerSubtypeInterpreter( Registry.ModItems.TURTLE_NORMAL.get(), turtleSubtype ); + subtypeRegistry.registerSubtypeInterpreter( Registry.ModItems.TURTLE_ADVANCED.get(), turtleSubtype ); + + subtypeRegistry.registerSubtypeInterpreter( Registry.ModItems.POCKET_COMPUTER_NORMAL.get(), pocketSubtype ); + subtypeRegistry.registerSubtypeInterpreter( Registry.ModItems.POCKET_COMPUTER_ADVANCED.get(), pocketSubtype ); + + subtypeRegistry.registerSubtypeInterpreter( Registry.ModItems.DISK.get(), diskSubtype ); + } + + @Override + public void registerAdvanced( IAdvancedRegistration registry ) + { + registry.addRecipeManagerPlugin( new RecipeResolver() ); + } + + @Override + public void onRuntimeAvailable( IJeiRuntime runtime ) + { + IRecipeManager registry = runtime.getRecipeManager(); + + // Register all turtles/pocket computers (not just vanilla upgrades) as upgrades on JEI. + List upgradeItems = new ArrayList<>(); + for( ComputerFamily family : MAIN_FAMILIES ) + { + TurtleUpgrades.getUpgrades() + .filter( x -> TurtleUpgrades.suitableForFamily( family, x ) ) + .map( x -> TurtleItemFactory.create( -1, null, -1, family, null, x, 0, null ) ) + .forEach( upgradeItems::add ); + + for( IPocketUpgrade upgrade : PocketUpgrades.getUpgrades() ) + { + upgradeItems.add( PocketComputerItemFactory.create( -1, null, -1, family, upgrade ) ); + } + } + + runtime.getIngredientManager().addIngredientsAtRuntime( VanillaTypes.ITEM, upgradeItems ); + + // Hide all upgrade recipes + IRecipeCategory category = (IRecipeCategory) registry.getRecipeCategory( VanillaRecipeCategoryUid.CRAFTING ); + if( category != null ) + { + for( Object wrapper : registry.getRecipes( category ) ) + { + if( !(wrapper instanceof Recipe) ) continue; + Identifier id = ((Recipe) wrapper).getId(); + if( id.getNamespace().equals( ComputerCraft.MOD_ID ) + && (id.getPath().startsWith( "generated/turtle_" ) || id.getPath().startsWith( "generated/pocket_" )) ) + { + registry.hideRecipe( wrapper, VanillaRecipeCategoryUid.CRAFTING ); + } + } + } + } + + /** + * Distinguishes turtles by upgrades and family. + */ + private static final ISubtypeInterpreter turtleSubtype = stack -> { + Item item = stack.getItem(); + if( !(item instanceof ITurtleItem) ) return ""; + + ITurtleItem turtle = (ITurtleItem) item; + StringBuilder name = new StringBuilder(); + + // Add left and right upgrades to the identifier + ITurtleUpgrade left = turtle.getUpgrade( stack, TurtleSide.LEFT ); + ITurtleUpgrade right = turtle.getUpgrade( stack, TurtleSide.RIGHT ); + if( left != null ) name.append( left.getUpgradeID() ); + if( left != null && right != null ) name.append( '|' ); + if( right != null ) name.append( right.getUpgradeID() ); + + return name.toString(); + }; + + /** + * Distinguishes pocket computers by upgrade and family. + */ + private static final ISubtypeInterpreter pocketSubtype = stack -> { + Item item = stack.getItem(); + if( !(item instanceof ItemPocketComputer) ) return ""; + + StringBuilder name = new StringBuilder(); + + // Add the upgrade to the identifier + IPocketUpgrade upgrade = ItemPocketComputer.getUpgrade( stack ); + if( upgrade != null ) name.append( upgrade.getUpgradeID() ); + + return name.toString(); + }; + + /** + * Distinguishes disks by colour. + */ + private static final ISubtypeInterpreter diskSubtype = stack -> { + Item item = stack.getItem(); + if( !(item instanceof ItemDisk) ) return ""; + + ItemDisk disk = (ItemDisk) item; + + int colour = disk.getColour( stack ); + return colour == -1 ? "" : String.format( "%06x", colour ); + }; +} diff --git a/src/main/java/dan200/computercraft/shared/integration/jei/RecipeResolver.java b/src/main/java/dan200/computercraft/shared/integration/jei/RecipeResolver.java new file mode 100644 index 000000000..9050bb375 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/integration/jei/RecipeResolver.java @@ -0,0 +1,401 @@ +/* + * 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.integration.jei; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.PocketUpgrades; +import dan200.computercraft.shared.TurtleUpgrades; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import dan200.computercraft.shared.pocket.items.PocketComputerItemFactory; +import dan200.computercraft.shared.turtle.items.ITurtleItem; +import dan200.computercraft.shared.turtle.items.TurtleItemFactory; +import dan200.computercraft.shared.util.InventoryUtil; +import mezz.jei.api.constants.VanillaRecipeCategoryUid; +import mezz.jei.api.recipe.IFocus; +import mezz.jei.api.recipe.advanced.IRecipeManagerPlugin; +import mezz.jei.api.recipe.category.IRecipeCategory; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.ShapedRecipe; +import net.minecraft.util.Identifier; +import net.minecraft.util.collection.DefaultedList; +import javax.annotation.Nonnull; +import java.util.*; + +import static net.minecraft.recipe.Ingredient.ofStacks; +import static net.minecraft.util.collection.DefaultedList.copyOf; + +class RecipeResolver implements IRecipeManagerPlugin +{ + static final ComputerFamily[] MAIN_FAMILIES = new ComputerFamily[] { ComputerFamily.NORMAL, ComputerFamily.ADVANCED }; + + private final Map> upgradeItemLookup = new HashMap<>(); + private final List pocketUpgrades = new ArrayList<>(); + private final List turtleUpgrades = new ArrayList<>(); + private boolean initialised = false; + + /** + * Build a cache of items which are used for turtle and pocket computer upgrades. + */ + private void setupCache() + { + if( initialised ) return; + initialised = true; + + TurtleUpgrades.getUpgrades().forEach( upgrade -> { + ItemStack stack = upgrade.getCraftingItem(); + if( stack.isEmpty() ) return; + + UpgradeInfo info = new UpgradeInfo( stack, upgrade ); + upgradeItemLookup.computeIfAbsent( stack.getItem(), k -> new ArrayList<>( 1 ) ).add( info ); + turtleUpgrades.add( info ); + } ); + + for( IPocketUpgrade upgrade : PocketUpgrades.getUpgrades() ) + { + ItemStack stack = upgrade.getCraftingItem(); + if( stack.isEmpty() ) continue; + + UpgradeInfo info = new UpgradeInfo( stack, upgrade ); + upgradeItemLookup.computeIfAbsent( stack.getItem(), k -> new ArrayList<>( 1 ) ).add( info ); + pocketUpgrades.add( info ); + } + } + + private boolean hasUpgrade( @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return false; + + setupCache(); + List upgrades = upgradeItemLookup.get( stack.getItem() ); + if( upgrades == null ) return false; + + for( UpgradeInfo upgrade : upgrades ) + { + ItemStack craftingStack = upgrade.stack; + if( !craftingStack.isEmpty() && InventoryUtil.areItemsSimilar( stack, craftingStack ) ) return true; + } + + return false; + } + + @Nonnull + @Override + public List getRecipeCategoryUids( @Nonnull IFocus focus ) + { + V value = focus.getValue(); + if( !(value instanceof ItemStack) ) return Collections.emptyList(); + + ItemStack stack = (ItemStack) value; + switch( focus.getMode() ) + { + case INPUT: + return stack.getItem() instanceof ITurtleItem || stack.getItem() instanceof ItemPocketComputer || + hasUpgrade( stack ) + ? Collections.singletonList( VanillaRecipeCategoryUid.CRAFTING ) + : Collections.emptyList(); + case OUTPUT: + return stack.getItem() instanceof ITurtleItem || stack.getItem() instanceof ItemPocketComputer + ? Collections.singletonList( VanillaRecipeCategoryUid.CRAFTING ) + : Collections.emptyList(); + default: + return Collections.emptyList(); + } + } + + @Nonnull + @Override + public List getRecipes( @Nonnull IRecipeCategory recipeCategory, @Nonnull IFocus focus ) + { + if( !(focus.getValue() instanceof ItemStack) || !recipeCategory.getUid().equals( VanillaRecipeCategoryUid.CRAFTING ) ) + { + return Collections.emptyList(); + } + + ItemStack stack = (ItemStack) focus.getValue(); + switch( focus.getMode() ) + { + case INPUT: + return cast( findRecipesWithInput( stack ) ); + case OUTPUT: + return cast( findRecipesWithOutput( stack ) ); + default: + return Collections.emptyList(); + } + } + + @Nonnull + @Override + public List getRecipes( @Nonnull IRecipeCategory recipeCategory ) + { + return Collections.emptyList(); + } + + @Nonnull + private List findRecipesWithInput( @Nonnull ItemStack stack ) + { + setupCache(); + + if( stack.getItem() instanceof ITurtleItem ) + { + // Suggest possible upgrades which can be applied to this turtle + ITurtleItem item = (ITurtleItem) stack.getItem(); + ITurtleUpgrade left = item.getUpgrade( stack, TurtleSide.LEFT ); + ITurtleUpgrade right = item.getUpgrade( stack, TurtleSide.RIGHT ); + if( left != null && right != null ) return Collections.emptyList(); + + List recipes = new ArrayList<>(); + Ingredient ingredient = ofStacks( stack ); + for( UpgradeInfo upgrade : turtleUpgrades ) + { + // The turtle is facing towards us, so upgrades on the left are actually crafted on the right. + if( left == null ) + { + recipes.add( horizontal( copyOf( Ingredient.EMPTY, ingredient, upgrade.ingredient ), turtleWith( stack, upgrade.turtle, right ) ) ); + } + + if( right == null ) + { + recipes.add( horizontal( copyOf( Ingredient.EMPTY, upgrade.ingredient, ingredient ), turtleWith( stack, left, upgrade.turtle ) ) ); + } + } + + return cast( recipes ); + } + else if( stack.getItem() instanceof ItemPocketComputer ) + { + // Suggest possible upgrades which can be applied to this turtle + IPocketUpgrade back = ItemPocketComputer.getUpgrade( stack ); + if( back != null ) return Collections.emptyList(); + + List recipes = new ArrayList<>(); + Ingredient ingredient = ofStacks( stack ); + for( UpgradeInfo upgrade : pocketUpgrades ) + { + recipes.add( vertical( copyOf( Ingredient.EMPTY, ingredient, upgrade.ingredient ), pocketWith( stack, upgrade.pocket ) ) ); + } + + return recipes; + } + else + { + List upgrades = upgradeItemLookup.get( stack.getItem() ); + if( upgrades == null ) return Collections.emptyList(); + + List recipes = null; + boolean multiple = false; + Ingredient ingredient = ofStacks( stack ); + for( UpgradeInfo upgrade : upgrades ) + { + ItemStack craftingStack = upgrade.stack; + if( craftingStack.isEmpty() || !InventoryUtil.areItemsSimilar( stack, craftingStack ) ) + { + continue; + } + + if( recipes == null ) + { + recipes = upgrade.getRecipes(); + } + else + { + if( !multiple ) + { + multiple = true; + recipes = new ArrayList<>( recipes ); + } + recipes.addAll( upgrade.getRecipes() ); + } + } + + return recipes == null ? Collections.emptyList() : recipes; + } + } + + @Nonnull + private static List findRecipesWithOutput( @Nonnull ItemStack stack ) + { + // Find which upgrade this item currently has, an so how we could build it. + if( stack.getItem() instanceof ITurtleItem ) + { + ITurtleItem item = (ITurtleItem) stack.getItem(); + List recipes = new ArrayList<>( 0 ); + + ITurtleUpgrade left = item.getUpgrade( stack, TurtleSide.LEFT ); + ITurtleUpgrade right = item.getUpgrade( stack, TurtleSide.RIGHT ); + + // The turtle is facing towards us, so upgrades on the left are actually crafted on the right. + if( left != null ) + { + recipes.add( horizontal( + copyOf( Ingredient.EMPTY, ofStacks( turtleWith( stack, null, right ) ), ofStacks( left.getCraftingItem() ) ), + stack + ) ); + } + + if( right != null ) + { + recipes.add( horizontal( + copyOf( Ingredient.EMPTY, ofStacks( right.getCraftingItem() ), ofStacks( turtleWith( stack, left, null ) ) ), + stack + ) ); + } + + return cast( recipes ); + } + else if( stack.getItem() instanceof ItemPocketComputer ) + { + List recipes = new ArrayList<>( 0 ); + + IPocketUpgrade back = ItemPocketComputer.getUpgrade( stack ); + if( back != null ) + { + recipes.add( vertical( + copyOf( Ingredient.EMPTY, ofStacks( back.getCraftingItem() ), ofStacks( pocketWith( stack, null ) ) ), + stack + ) ); + } + + return cast( recipes ); + } + else + { + return Collections.emptyList(); + } + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + private static List cast( List from ) + { + return (List) from; + } + + private static ItemStack turtleWith( ItemStack stack, ITurtleUpgrade left, ITurtleUpgrade right ) + { + ITurtleItem item = (ITurtleItem) stack.getItem(); + return TurtleItemFactory.create( + item.getComputerID( stack ), item.getLabel( stack ), item.getColour( stack ), item.getFamily(), + left, right, item.getFuelLevel( stack ), item.getOverlay( stack ) + ); + } + + private static ItemStack pocketWith( ItemStack stack, IPocketUpgrade back ) + { + ItemPocketComputer item = (ItemPocketComputer) stack.getItem(); + return PocketComputerItemFactory.create( + item.getComputerID( stack ), item.getLabel( stack ), item.getColour( stack ), item.getFamily(), + back + ); + } + + private static Shaped vertical( DefaultedList input, ItemStack result ) + { + return new Shaped( 1, input.size(), input, result ); + } + + private static Shaped horizontal( DefaultedList input, ItemStack result ) + { + return new Shaped( input.size(), 1, input, result ); + } + + private static class Shaped extends ShapedRecipe + { + private static final Identifier ID = new Identifier( ComputerCraft.MOD_ID, "impostor" ); + + Shaped( int width, int height, DefaultedList input, ItemStack output ) + { + super( ID, null, width, height, input, output ); + } + + @Nonnull + @Override + public Identifier getId() + { + return null; + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + throw new IllegalStateException( "Should not serialise the JEI recipe" ); + } + } + + private static final class Upgrade + { + final T upgrade; + final ItemStack stack; + final Ingredient ingredient; + + private Upgrade( T upgrade, ItemStack stack ) + { + this.upgrade = upgrade; + this.stack = stack; + ingredient = ofStacks( stack ); + } + } + + private static class UpgradeInfo + { + final ItemStack stack; + final Ingredient ingredient; + final ITurtleUpgrade turtle; + final IPocketUpgrade pocket; + ArrayList recipes; + + UpgradeInfo( ItemStack stack, ITurtleUpgrade turtle ) + { + this.stack = stack; + ingredient = ofStacks( stack ); + this.turtle = turtle; + pocket = null; + } + + UpgradeInfo( ItemStack stack, IPocketUpgrade pocket ) + { + this.stack = stack; + ingredient = ofStacks( stack ); + turtle = null; + this.pocket = pocket; + } + + List getRecipes() + { + ArrayList recipes = this.recipes; + if( recipes != null ) return recipes; + + recipes = this.recipes = new ArrayList<>( 4 ); + for( ComputerFamily family : MAIN_FAMILIES ) + { + if( turtle != null && TurtleUpgrades.suitableForFamily( family, turtle ) ) + { + recipes.add( horizontal( + copyOf( Ingredient.EMPTY, ingredient, ofStacks( TurtleItemFactory.create( -1, null, -1, family, null, null, 0, null ) ) ), + TurtleItemFactory.create( -1, null, -1, family, null, turtle, 0, null ) + ) ); + } + + if( pocket != null ) + { + recipes.add( vertical( + copyOf( Ingredient.EMPTY, ingredient, ofStacks( PocketComputerItemFactory.create( -1, null, -1, family, null ) ) ), + PocketComputerItemFactory.create( -1, null, -1, family, pocket ) + ) ); + } + } + + recipes.trimToSize(); + return recipes; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/media/items/ItemDisk.java b/src/main/java/dan200/computercraft/shared/media/items/ItemDisk.java new file mode 100644 index 000000000..84ab0e941 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/media/items/ItemDisk.java @@ -0,0 +1,131 @@ +/* + * 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.media.items; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.common.IColouredItem; +import dan200.computercraft.shared.util.Colour; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.WorldView; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; + +public class ItemDisk extends Item implements IMedia, IColouredItem +{ + private static final String NBT_ID = "DiskId"; + + public ItemDisk( Settings settings ) + { + super( settings ); + } + + @Nonnull + public static ItemStack createFromIDAndColour( int id, String label, int colour ) + { + ItemStack stack = new ItemStack( Registry.ModItems.DISK.get() ); + setDiskID( stack, id ); + Registry.ModItems.DISK.get().setLabel( stack, label ); + IColouredItem.setColourBasic( stack, colour ); + return stack; + } + + @Override + public void appendStacks( @Nonnull ItemGroup tabs, @Nonnull DefaultedList list ) + { + if( !isIn( tabs ) ) return; + for( int colour = 0; colour < 16; colour++ ) + { + list.add( createFromIDAndColour( -1, null, Colour.VALUES[colour].getHex() ) ); + } + } + + @Override + public void appendTooltip( @Nonnull ItemStack stack, @Nullable World world, @Nonnull List list, TooltipContext options ) + { + if( options.isAdvanced() ) + { + int id = getDiskID( stack ); + if( id >= 0 ) + { + list.add( new TranslatableText( "gui.computercraft.tooltip.disk_id", id ) + .formatted( Formatting.GRAY ) ); + } + } + } + + @Override + public boolean doesSneakBypassUse( ItemStack stack, WorldView world, BlockPos pos, PlayerEntity player ) + { + return true; + } + + @Override + public String getLabel( @Nonnull ItemStack stack ) + { + return stack.hasCustomName() ? stack.getName().getString() : null; + } + + @Override + public boolean setLabel( @Nonnull ItemStack stack, String label ) + { + if( label != null ) + { + stack.setCustomName( new LiteralText( label ) ); + } + else + { + stack.removeCustomName(); + } + return true; + } + + @Override + public IMount createDataMount( @Nonnull ItemStack stack, @Nonnull World world ) + { + int diskID = getDiskID( stack ); + if( diskID < 0 ) + { + diskID = ComputerCraftAPI.createUniqueNumberedSaveDir( world, "disk" ); + setDiskID( stack, diskID ); + } + return ComputerCraftAPI.createSaveDirMount( world, "disk/" + diskID, ComputerCraft.floppySpaceLimit ); + } + + public static int getDiskID( @Nonnull ItemStack stack ) + { + CompoundTag nbt = stack.getTag(); + return nbt != null && nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1; + } + + private static void setDiskID( @Nonnull ItemStack stack, int id ) + { + if( id >= 0 ) stack.getOrCreateTag().putInt( NBT_ID, id ); + } + + @Override + public int getColour( @Nonnull ItemStack stack ) + { + int colour = IColouredItem.getColourBasic( stack ); + return colour == -1 ? Colour.WHITE.getHex() : colour; + } +} diff --git a/src/main/java/dan200/computercraft/shared/media/items/ItemPrintout.java b/src/main/java/dan200/computercraft/shared/media/items/ItemPrintout.java new file mode 100644 index 000000000..597724bb5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/media/items/ItemPrintout.java @@ -0,0 +1,156 @@ +/* + * 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.media.items; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.common.ContainerHeldItem; +import dan200.computercraft.shared.network.container.HeldItemContainerData; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.TypedActionResult; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import java.util.List; + +public class ItemPrintout extends Item +{ + private static final String NBT_TITLE = "Title"; + private static final String NBT_PAGES = "Pages"; + private static final String NBT_LINE_TEXT = "Text"; + private static final String NBT_LINE_COLOUR = "Color"; + + public static final int LINES_PER_PAGE = 21; + public static final int LINE_MAX_LENGTH = 25; + public static final int MAX_PAGES = 16; + + public enum Type + { + PAGE, + PAGES, + BOOK + } + + private final Type type; + + public ItemPrintout( Settings settings, Type type ) + { + super( settings ); + this.type = type; + } + + @Override + public void appendTooltip( @Nonnull ItemStack stack, World world, @Nonnull List list, @Nonnull TooltipContext options ) + { + String title = getTitle( stack ); + if( title != null && !title.isEmpty() ) list.add( new LiteralText( title ) ); + } + + @Nonnull + @Override + public TypedActionResult use( World world, @Nonnull PlayerEntity player, @Nonnull Hand hand ) + { + if( !world.isClient ) + { + new HeldItemContainerData( hand ) + .open( player, new ContainerHeldItem.Factory( Registry.ModContainers.PRINTOUT.get(), player.getStackInHand( hand ), hand ) ); + } + return new TypedActionResult<>( ActionResult.SUCCESS, player.getStackInHand( hand ) ); + } + + @Nonnull + private ItemStack createFromTitleAndText( String title, String[] text, String[] colours ) + { + ItemStack stack = new ItemStack( this ); + + // Build NBT + if( title != null ) stack.getOrCreateTag().putString( NBT_TITLE, title ); + if( text != null ) + { + CompoundTag tag = stack.getOrCreateTag(); + tag.putInt( NBT_PAGES, text.length / LINES_PER_PAGE ); + for( int i = 0; i < text.length; i++ ) + { + if( text[i] != null ) tag.putString( NBT_LINE_TEXT + i, text[i] ); + } + } + if( colours != null ) + { + CompoundTag tag = stack.getOrCreateTag(); + for( int i = 0; i < colours.length; i++ ) + { + if( colours[i] != null ) tag.putString( NBT_LINE_COLOUR + i, colours[i] ); + } + } + + + return stack; + } + + @Nonnull + public static ItemStack createSingleFromTitleAndText( String title, String[] text, String[] colours ) + { + return Registry.ModItems.PRINTED_PAGE.get().createFromTitleAndText( title, text, colours ); + } + + @Nonnull + public static ItemStack createMultipleFromTitleAndText( String title, String[] text, String[] colours ) + { + return Registry.ModItems.PRINTED_PAGES.get().createFromTitleAndText( title, text, colours ); + } + + @Nonnull + public static ItemStack createBookFromTitleAndText( String title, String[] text, String[] colours ) + { + return Registry.ModItems.PRINTED_BOOK.get().createFromTitleAndText( title, text, colours ); + } + + public Type getType() + { + return type; + } + + public static String getTitle( @Nonnull ItemStack stack ) + { + CompoundTag nbt = stack.getTag(); + return nbt != null && nbt.contains( NBT_TITLE ) ? nbt.getString( NBT_TITLE ) : null; + } + + public static int getPageCount( @Nonnull ItemStack stack ) + { + CompoundTag nbt = stack.getTag(); + return nbt != null && nbt.contains( NBT_PAGES ) ? nbt.getInt( NBT_PAGES ) : 1; + } + + public static String[] getText( @Nonnull ItemStack stack ) + { + return getLines( stack, NBT_LINE_TEXT ); + } + + public static String[] getColours( @Nonnull ItemStack stack ) + { + return getLines( stack, NBT_LINE_COLOUR ); + } + + private static String[] getLines( @Nonnull ItemStack stack, String prefix ) + { + CompoundTag nbt = stack.getTag(); + int numLines = getPageCount( stack ) * LINES_PER_PAGE; + String[] lines = new String[numLines]; + for( int i = 0; i < lines.length; i++ ) + { + lines[i] = nbt != null ? nbt.getString( prefix + i ) : ""; + } + return lines; + } +} diff --git a/src/main/java/dan200/computercraft/shared/media/items/ItemTreasureDisk.java b/src/main/java/dan200/computercraft/shared/media/items/ItemTreasureDisk.java new file mode 100644 index 000000000..692f30f5c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/media/items/ItemTreasureDisk.java @@ -0,0 +1,138 @@ +/* + * 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.media.items; + +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.core.filesystem.SubMount; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.util.Colour; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.WorldView; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.List; + +public class ItemTreasureDisk extends Item implements IMedia +{ + private static final String NBT_TITLE = "Title"; + private static final String NBT_COLOUR = "Colour"; + private static final String NBT_SUB_PATH = "SubPath"; + + public ItemTreasureDisk( Settings settings ) + { + super( settings ); + } + + @Override + public void appendStacks( @Nonnull ItemGroup group, @Nonnull DefaultedList stacks ) + { + } + + @Override + public void appendTooltip( @Nonnull ItemStack stack, @Nullable World world, @Nonnull List list, @Nonnull TooltipContext tooltipOptions ) + { + String label = getTitle( stack ); + if( !label.isEmpty() ) list.add( new LiteralText( label ) ); + } + + @Override + public boolean doesSneakBypassUse( @Nonnull ItemStack stack, WorldView world, BlockPos pos, PlayerEntity player ) + { + return true; + } + + @Override + public String getLabel( @Nonnull ItemStack stack ) + { + return getTitle( stack ); + } + + @Override + public IMount createDataMount( @Nonnull ItemStack stack, @Nonnull World world ) + { + IMount rootTreasure = getTreasureMount(); + String subPath = getSubPath( stack ); + try + { + if( rootTreasure.exists( subPath ) ) + { + return new SubMount( rootTreasure, subPath ); + } + else if( rootTreasure.exists( "deprecated/" + subPath ) ) + { + return new SubMount( rootTreasure, "deprecated/" + subPath ); + } + else + { + return null; + } + } + catch( IOException e ) + { + return null; + } + } + + public static ItemStack create( String subPath, int colourIndex ) + { + ItemStack result = new ItemStack( Registry.ModItems.TREASURE_DISK.get() ); + CompoundTag nbt = result.getOrCreateTag(); + nbt.putString( NBT_SUB_PATH, subPath ); + + int slash = subPath.indexOf( '/' ); + if( slash >= 0 ) + { + String author = subPath.substring( 0, slash ); + String title = subPath.substring( slash + 1 ); + nbt.putString( NBT_TITLE, "\"" + title + "\" by " + author ); + } + else + { + nbt.putString( NBT_TITLE, "untitled" ); + } + nbt.putInt( NBT_COLOUR, Colour.values()[colourIndex].getHex() ); + + return result; + } + + private static IMount getTreasureMount() + { + return ComputerCraftAPI.createResourceMount( "computercraft", "lua/treasure" ); + } + + @Nonnull + private static String getTitle( @Nonnull ItemStack stack ) + { + CompoundTag nbt = stack.getTag(); + return nbt != null && nbt.contains( NBT_TITLE ) ? nbt.getString( NBT_TITLE ) : "'alongtimeago' by dan200"; + } + + @Nonnull + private static String getSubPath( @Nonnull ItemStack stack ) + { + CompoundTag nbt = stack.getTag(); + return nbt != null && nbt.contains( NBT_SUB_PATH ) ? nbt.getString( NBT_SUB_PATH ) : "dan200/alongtimeago"; + } + + public static int getColour( @Nonnull ItemStack stack ) + { + CompoundTag nbt = stack.getTag(); + return nbt != null && nbt.contains( NBT_COLOUR ) ? nbt.getInt( NBT_COLOUR ) : Colour.BLUE.getHex(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/media/items/RecordMedia.java b/src/main/java/dan200/computercraft/shared/media/items/RecordMedia.java new file mode 100644 index 000000000..f05d487d2 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/media/items/RecordMedia.java @@ -0,0 +1,63 @@ +/* + * 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.media.items; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.media.IMedia; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.MusicDiscItem; +import net.minecraft.sound.SoundEvent; +import net.minecraft.text.TranslatableText; +import net.minecraftforge.fml.common.ObfuscationReflectionHelper; +import net.minecraftforge.fml.common.ObfuscationReflectionHelper.UnableToAccessFieldException; +import net.minecraftforge.fml.common.ObfuscationReflectionHelper.UnableToFindFieldException; + +import javax.annotation.Nonnull; + +/** + * An implementation of IMedia for ItemRecords. + */ +public final class RecordMedia implements IMedia +{ + public static final RecordMedia INSTANCE = new RecordMedia(); + + private RecordMedia() + { + } + + @Override + public String getLabel( @Nonnull ItemStack stack ) + { + return getAudioTitle( stack ); + } + + @Override + public String getAudioTitle( @Nonnull ItemStack stack ) + { + Item item = stack.getItem(); + if( !(item instanceof MusicDiscItem) ) return null; + + return new TranslatableText( item.getTranslationKey() + ".desc" ).getString(); + } + + @Override + public SoundEvent getAudio( @Nonnull ItemStack stack ) + { + Item item = stack.getItem(); + if( !(item instanceof MusicDiscItem) ) return null; + + try + { + return ObfuscationReflectionHelper.getPrivateValue( MusicDiscItem.class, (MusicDiscItem) item, "field_185076_b" ); + } + catch( UnableToAccessFieldException | UnableToFindFieldException e ) + { + ComputerCraft.log.error( "Cannot get disk sound", e ); + return null; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java b/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java new file mode 100644 index 000000000..e8dcc3787 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java @@ -0,0 +1,114 @@ +/* + * 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.media.recipes; + +import dan200.computercraft.shared.media.items.ItemDisk; +import dan200.computercraft.shared.util.Colour; +import dan200.computercraft.shared.util.ColourTracker; +import dan200.computercraft.shared.util.ColourUtils; +import net.minecraft.inventory.CraftingInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.SpecialCraftingRecipe; +import net.minecraft.recipe.SpecialRecipeSerializer; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public class DiskRecipe extends SpecialCraftingRecipe +{ + private final Ingredient paper = Ingredient.ofItems( Items.PAPER ); + private final Ingredient redstone = Ingredient.ofItems( Items.REDSTONE ); + // TODO: Ingredient.fromTag( Tags.Items.DUSTS_REDSTONE ); + + public DiskRecipe( Identifier id ) + { + super( id ); + } + + @Override + public boolean matches( @Nonnull CraftingInventory inv, @Nonnull World world ) + { + boolean paperFound = false; + boolean redstoneFound = false; + + for( int i = 0; i < inv.size(); i++ ) + { + ItemStack stack = inv.getStack( i ); + + if( !stack.isEmpty() ) + { + if( paper.test( stack ) ) + { + if( paperFound ) return false; + paperFound = true; + } + else if( redstone.test( stack ) ) + { + if( redstoneFound ) return false; + redstoneFound = true; + } + else if( ColourUtils.getStackColour( stack ) != null ) + { + return false; + } + } + } + + return redstoneFound && paperFound; + } + + @Nonnull + @Override + public ItemStack getCraftingResult( @Nonnull CraftingInventory inv ) + { + ColourTracker tracker = new ColourTracker(); + + for( int i = 0; i < inv.size(); i++ ) + { + ItemStack stack = inv.getStack( i ); + + if( stack.isEmpty() ) continue; + + if( !paper.test( stack ) && !redstone.test( stack ) ) + { + DyeColor dye = ColourUtils.getStackColour( stack ); + if( dye == null ) continue; + + Colour colour = Colour.VALUES[dye.getId()]; + tracker.addColour( colour.getR(), colour.getG(), colour.getB() ); + } + } + + return ItemDisk.createFromIDAndColour( -1, null, tracker.hasColour() ? tracker.getColour() : Colour.BLUE.getHex() ); + } + + @Override + public boolean fits( int x, int y ) + { + return x >= 2 && y >= 2; + } + + @Nonnull + @Override + public ItemStack getOutput() + { + return ItemDisk.createFromIDAndColour( -1, null, Colour.BLUE.getHex() ); + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + public static final RecipeSerializer SERIALIZER = new SpecialRecipeSerializer<>( DiskRecipe::new ); +} diff --git a/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java b/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java new file mode 100644 index 000000000..3f567b9bc --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java @@ -0,0 +1,167 @@ +/* + * 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.media.recipes; + +import dan200.computercraft.shared.media.items.ItemPrintout; +import net.minecraft.inventory.CraftingInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.SpecialCraftingRecipe; +import net.minecraft.recipe.SpecialRecipeSerializer; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public final class PrintoutRecipe extends SpecialCraftingRecipe +{ + private final Ingredient paper = Ingredient.ofItems( net.minecraft.item.Items.PAPER ); + private final Ingredient leather = Ingredient.ofItems( net.minecraft.item.Items.LEATHER ); + private final Ingredient string = Ingredient.ofItems( Items.STRING ); + + private PrintoutRecipe( Identifier id ) + { + super( id ); + } + + @Override + public boolean fits( int x, int y ) + { + return x >= 3 && y >= 3; + } + + @Nonnull + @Override + public ItemStack getOutput() + { + return ItemPrintout.createMultipleFromTitleAndText( null, null, null ); + } + + @Override + public boolean matches( @Nonnull CraftingInventory inventory, @Nonnull World world ) + { + return !getCraftingResult( inventory ).isEmpty(); + } + + @Nonnull + @Override + public ItemStack getCraftingResult( @Nonnull CraftingInventory inventory ) + { + // See if we match the recipe, and extract the input disk ID and dye colour + int numPages = 0; + int numPrintouts = 0; + ItemStack[] printouts = null; + boolean stringFound = false; + boolean leatherFound = false; + boolean printoutFound = false; + for( int y = 0; y < inventory.getHeight(); y++ ) + { + for( int x = 0; x < inventory.getWidth(); x++ ) + { + ItemStack stack = inventory.getStack( x + y * inventory.getWidth() ); + if( !stack.isEmpty() ) + { + if( stack.getItem() instanceof ItemPrintout && ((ItemPrintout) stack.getItem()).getType() != ItemPrintout.Type.BOOK ) + { + if( printouts == null ) + { + printouts = new ItemStack[9]; + } + printouts[numPrintouts] = stack; + numPages += ItemPrintout.getPageCount( stack ); + numPrintouts++; + printoutFound = true; + } + else if( paper.test( stack ) ) + { + if( printouts == null ) + { + printouts = new ItemStack[9]; + } + printouts[numPrintouts] = stack; + numPages++; + numPrintouts++; + } + else if( string.test( stack ) && !stringFound ) + { + stringFound = true; + } + else if( leather.test( stack ) && !leatherFound ) + { + leatherFound = true; + } + else + { + return ItemStack.EMPTY; + } + } + } + } + + // Build some pages with what was passed in + if( numPages <= ItemPrintout.MAX_PAGES && stringFound && printoutFound && numPrintouts >= (leatherFound ? 1 : 2) ) + { + String[] text = new String[numPages * ItemPrintout.LINES_PER_PAGE]; + String[] colours = new String[numPages * ItemPrintout.LINES_PER_PAGE]; + int line = 0; + + for( int printout = 0; printout < numPrintouts; printout++ ) + { + ItemStack stack = printouts[printout]; + if( stack.getItem() instanceof ItemPrintout ) + { + // Add a printout + String[] pageText = ItemPrintout.getText( printouts[printout] ); + String[] pageColours = ItemPrintout.getColours( printouts[printout] ); + for( int pageLine = 0; pageLine < pageText.length; pageLine++ ) + { + text[line] = pageText[pageLine]; + colours[line] = pageColours[pageLine]; + line++; + } + } + else + { + // Add a blank page + for( int pageLine = 0; pageLine < ItemPrintout.LINES_PER_PAGE; pageLine++ ) + { + text[line] = ""; + colours[line] = ""; + line++; + } + } + } + + String title = null; + if( printouts[0].getItem() instanceof ItemPrintout ) + { + title = ItemPrintout.getTitle( printouts[0] ); + } + + if( leatherFound ) + { + return ItemPrintout.createBookFromTitleAndText( title, text, colours ); + } + else + { + return ItemPrintout.createMultipleFromTitleAndText( title, text, colours ); + } + } + + return ItemStack.EMPTY; + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + public static final RecipeSerializer SERIALIZER = new SpecialRecipeSerializer<>( PrintoutRecipe::new ); +} diff --git a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java new file mode 100644 index 000000000..f741bd517 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java @@ -0,0 +1,136 @@ +/* + * 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.network; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.shared.network.client.*; +import dan200.computercraft.shared.network.server.*; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraft.world.chunk.WorldChunk; +import net.minecraftforge.fml.network.NetworkDirection; +import net.minecraftforge.fml.network.NetworkEvent; +import net.minecraftforge.fml.network.NetworkRegistry; +import net.minecraftforge.fml.network.PacketDistributor; +import net.minecraftforge.fml.network.simple.SimpleChannel; +import net.minecraftforge.fml.server.ServerLifecycleHooks; + +import java.util.function.Function; +import java.util.function.Supplier; + +public final class NetworkHandler +{ + public static SimpleChannel network; + + private NetworkHandler() + { + } + + public static void setup() + { + String version = ComputerCraftAPI.getInstalledVersion(); + network = NetworkRegistry.ChannelBuilder.named( new Identifier( ComputerCraft.MOD_ID, "network" ) ) + .networkProtocolVersion( () -> version ) + .clientAcceptedVersions( version::equals ).serverAcceptedVersions( version::equals ) + .simpleChannel(); + + // Server messages + registerMainThread( 0, NetworkDirection.PLAY_TO_SERVER, ComputerActionServerMessage::new ); + registerMainThread( 1, NetworkDirection.PLAY_TO_SERVER, QueueEventServerMessage::new ); + registerMainThread( 2, NetworkDirection.PLAY_TO_SERVER, RequestComputerMessage::new ); + registerMainThread( 3, NetworkDirection.PLAY_TO_SERVER, KeyEventServerMessage::new ); + registerMainThread( 4, NetworkDirection.PLAY_TO_SERVER, MouseEventServerMessage::new ); + + // Client messages + registerMainThread( 10, NetworkDirection.PLAY_TO_CLIENT, ChatTableClientMessage::new ); + registerMainThread( 11, NetworkDirection.PLAY_TO_CLIENT, ComputerDataClientMessage::new ); + registerMainThread( 12, NetworkDirection.PLAY_TO_CLIENT, ComputerDeletedClientMessage::new ); + registerMainThread( 13, NetworkDirection.PLAY_TO_CLIENT, ComputerTerminalClientMessage::new ); + registerMainThread( 14, NetworkDirection.PLAY_TO_CLIENT, PlayRecordClientMessage.class, PlayRecordClientMessage::new ); + registerMainThread( 15, NetworkDirection.PLAY_TO_CLIENT, MonitorClientMessage.class, MonitorClientMessage::new ); + } + + public static void sendToPlayer( PlayerEntity player, NetworkMessage packet ) + { + network.sendTo( packet, ((ServerPlayerEntity) player).networkHandler.connection, NetworkDirection.PLAY_TO_CLIENT ); + } + + public static void sendToAllPlayers( NetworkMessage packet ) + { + for( ServerPlayerEntity player : ServerLifecycleHooks.getCurrentServer().getPlayerManager().getPlayerList() ) + { + sendToPlayer( player, packet ); + } + } + + public static void sendToServer( NetworkMessage packet ) + { + network.sendToServer( packet ); + } + + public static void sendToAllAround( NetworkMessage packet, World world, Vec3d pos, double range ) + { + PacketDistributor.TargetPoint target = new PacketDistributor.TargetPoint( pos.x, pos.y, pos.z, range, world.getRegistryKey() ); + network.send( PacketDistributor.NEAR.with( () -> target ), packet ); + } + + public static void sendToAllTracking( NetworkMessage packet, WorldChunk chunk ) + { + network.send( PacketDistributor.TRACKING_CHUNK.with( () -> chunk ), packet ); + } + + /** + * /** + * Register packet, and a thread-unsafe handler for it. + * + * @param The type of the packet to send. + * @param id The identifier for this packet type. + * @param direction A network direction which will be asserted before any processing of this message occurs. + * @param factory The factory for this type of packet. + */ + private static void registerMainThread( int id, NetworkDirection direction, Supplier factory ) + { + registerMainThread( id, direction, getType( factory ), buf -> { + T instance = factory.get(); + instance.fromBytes( buf ); + return instance; + } ); + } + + /** + * /** + * Register packet, and a thread-unsafe handler for it. + * + * @param The type of the packet to send. + * @param type The class of the type of packet to send. + * @param id The identifier for this packet type. + * @param direction A network direction which will be asserted before any processing of this message occurs + * @param decoder The factory for this type of packet. + */ + private static void registerMainThread( int id, NetworkDirection direction, Class type, Function decoder ) + { + network.messageBuilder( type, id, direction ) + .encoder( NetworkMessage::toBytes ) + .decoder( decoder ) + .consumer( ( packet, contextSup ) -> { + NetworkEvent.Context context = contextSup.get(); + context.enqueueWork( () -> packet.handle( context ) ); + context.setPacketHandled( true ); + } ) + .add(); + } + + @SuppressWarnings( "unchecked" ) + private static Class getType( Supplier supplier ) + { + return (Class) supplier.get().getClass(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/NetworkMessage.java b/src/main/java/dan200/computercraft/shared/network/NetworkMessage.java new file mode 100644 index 000000000..75b55b6a4 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/NetworkMessage.java @@ -0,0 +1,48 @@ +/* + * 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.network; + +import net.minecraft.network.PacketByteBuf; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; + +/** + * The base interface for any message which will be sent to the client or server. + * + * @see dan200.computercraft.shared.network.client + * @see dan200.computercraft.shared.network.server + */ +public interface NetworkMessage +{ + /** + * Write this packet to a buffer. + * + * This may be called on any thread, so this should be a pure operation. + * + * @param buf The buffer to write data to. + */ + void toBytes( @Nonnull PacketByteBuf buf ); + + /** + * Read this packet from a buffer. + * + * This may be called on any thread, so this should be a pure operation. + * + * @param buf The buffer to read data from. + */ + default void fromBytes( @Nonnull PacketByteBuf buf ) + { + throw new IllegalStateException( "Should have been registered using a \"from bytes\" method" ); + } + + /** + * Handle this {@link NetworkMessage}. + * + * @param context The context with which to handle this message + */ + void handle( NetworkEvent.Context context ); +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/ChatTableClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ChatTableClientMessage.java new file mode 100644 index 000000000..aa88660bc --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/ChatTableClientMessage.java @@ -0,0 +1,90 @@ +/* + * 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.network.client; + +import dan200.computercraft.client.ClientTableFormatter; +import dan200.computercraft.shared.command.text.TableBuilder; +import dan200.computercraft.shared.network.NetworkMessage; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.text.Text; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; + +public class ChatTableClientMessage implements NetworkMessage +{ + private TableBuilder table; + + public ChatTableClientMessage( TableBuilder table ) + { + if( table.getColumns() < 0 ) throw new IllegalStateException( "Cannot send an empty table" ); + this.table = table; + } + + public ChatTableClientMessage() + { + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + buf.writeVarInt( table.getId() ); + buf.writeVarInt( table.getColumns() ); + buf.writeBoolean( table.getHeaders() != null ); + if( table.getHeaders() != null ) + { + for( Text header : table.getHeaders() ) buf.writeText( header ); + } + + buf.writeVarInt( table.getRows().size() ); + for( Text[] row : table.getRows() ) + { + for( Text column : row ) buf.writeText( column ); + } + + buf.writeVarInt( table.getAdditional() ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + int id = buf.readVarInt(); + int columns = buf.readVarInt(); + TableBuilder table; + if( buf.readBoolean() ) + { + Text[] headers = new Text[columns]; + for( int i = 0; i < columns; i++ ) headers[i] = buf.readText(); + table = new TableBuilder( id, headers ); + } + else + { + table = new TableBuilder( id ); + } + + int rows = buf.readVarInt(); + for( int i = 0; i < rows; i++ ) + { + Text[] row = new Text[columns]; + for( int j = 0; j < columns; j++ ) row[j] = buf.readText(); + table.row( row ); + } + + table.setAdditional( buf.readVarInt() ); + this.table = table; + } + + @Override + @Environment(EnvType.CLIENT) + public void handle( NetworkEvent.Context context ) + { + ClientTableFormatter.INSTANCE.display( table ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/ComputerClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ComputerClientMessage.java new file mode 100644 index 000000000..c5df67d2e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/ComputerClientMessage.java @@ -0,0 +1,56 @@ +/* + * 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.network.client; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.ClientComputer; +import dan200.computercraft.shared.network.NetworkMessage; +import javax.annotation.Nonnull; +import net.minecraft.network.PacketByteBuf; + +/** + * A packet, which performs an action on a {@link ClientComputer}. + */ +public abstract class ComputerClientMessage implements NetworkMessage +{ + private int instanceId; + + public ComputerClientMessage( int instanceId ) + { + this.instanceId = instanceId; + } + + public ComputerClientMessage() + { + } + + public int getInstanceId() + { + return instanceId; + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + buf.writeVarInt( instanceId ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + instanceId = buf.readVarInt(); + } + + public ClientComputer getComputer() + { + ClientComputer computer = ComputerCraft.clientComputerRegistry.get( instanceId ); + if( computer == null ) + { + ComputerCraft.clientComputerRegistry.add( instanceId, computer = new ClientComputer( instanceId ) ); + } + return computer; + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/ComputerDataClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ComputerDataClientMessage.java new file mode 100644 index 000000000..1f7db362a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/ComputerDataClientMessage.java @@ -0,0 +1,56 @@ +/* + * 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.network.client; + +import dan200.computercraft.shared.computer.core.ComputerState; +import dan200.computercraft.shared.computer.core.ServerComputer; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.PacketByteBuf; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; + +/** + * Provides additional data about a client computer, such as its ID and current state. + */ +public class ComputerDataClientMessage extends ComputerClientMessage +{ + private ComputerState state; + private CompoundTag userData; + + public ComputerDataClientMessage( ServerComputer computer ) + { + super( computer.getInstanceID() ); + state = computer.getState(); + userData = computer.getUserData(); + } + + public ComputerDataClientMessage() + { + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + super.toBytes( buf ); + buf.writeEnumConstant( state ); + buf.writeCompoundTag( userData ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + super.fromBytes( buf ); + state = buf.readEnumConstant( ComputerState.class ); + userData = buf.readCompoundTag(); + } + + @Override + public void handle( NetworkEvent.Context context ) + { + getComputer().setState( state, userData ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/ComputerDeletedClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ComputerDeletedClientMessage.java new file mode 100644 index 000000000..5c42aa73e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/ComputerDeletedClientMessage.java @@ -0,0 +1,27 @@ +/* + * 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.network.client; + +import dan200.computercraft.ComputerCraft; +import net.minecraftforge.fml.network.NetworkEvent; + +public class ComputerDeletedClientMessage extends ComputerClientMessage +{ + public ComputerDeletedClientMessage( int instanceId ) + { + super( instanceId ); + } + + public ComputerDeletedClientMessage() + { + } + + @Override + public void handle( NetworkEvent.Context context ) + { + ComputerCraft.clientComputerRegistry.remove( getInstanceId() ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java new file mode 100644 index 000000000..e330e1f48 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java @@ -0,0 +1,46 @@ +/* + * 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.network.client; + +import net.minecraft.network.PacketByteBuf; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; + +public class ComputerTerminalClientMessage extends ComputerClientMessage +{ + private TerminalState state; + + public ComputerTerminalClientMessage( int instanceId, TerminalState state ) + { + super( instanceId ); + this.state = state; + } + + public ComputerTerminalClientMessage() + { + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + super.toBytes( buf ); + state.write( buf ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + super.fromBytes( buf ); + state = new TerminalState( buf ); + } + + @Override + public void handle( NetworkEvent.Context context ) + { + getComputer().read( state ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/MonitorClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/MonitorClientMessage.java new file mode 100644 index 000000000..b3849c1b2 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/MonitorClientMessage.java @@ -0,0 +1,55 @@ +/* + * 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.network.client; + +import dan200.computercraft.shared.network.NetworkMessage; +import dan200.computercraft.shared.peripheral.monitor.TileMonitor; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.math.BlockPos; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; + +public class MonitorClientMessage implements NetworkMessage +{ + private final BlockPos pos; + private final TerminalState state; + + public MonitorClientMessage( BlockPos pos, TerminalState state ) + { + this.pos = pos; + this.state = state; + } + + public MonitorClientMessage( @Nonnull PacketByteBuf buf ) + { + pos = buf.readBlockPos(); + state = new TerminalState( buf ); + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + buf.writeBlockPos( pos ); + state.write( buf ); + } + + @Override + public void handle( NetworkEvent.Context context ) + { + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if( player == null || player.world == null ) return; + + BlockEntity te = player.world.getBlockEntity( pos ); + if( !(te instanceof TileMonitor) ) return; + + ((TileMonitor) te).read( state ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/PlayRecordClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/PlayRecordClientMessage.java new file mode 100644 index 000000000..932ca9c76 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/PlayRecordClientMessage.java @@ -0,0 +1,88 @@ +/* + * 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.network.client; + +import dan200.computercraft.shared.network.NetworkMessage; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.sound.SoundEvent; +import net.minecraft.text.LiteralText; +import net.minecraft.util.math.BlockPos; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; + +/** + * Starts or stops a record on the client, depending on if {@link #soundEvent} is {@code null}. + * + * Used by disk drives to play record items. + * + * @see dan200.computercraft.shared.peripheral.diskdrive.TileDiskDrive + */ +public class PlayRecordClientMessage implements NetworkMessage +{ + private final BlockPos pos; + private final String name; + private final SoundEvent soundEvent; + + public PlayRecordClientMessage( BlockPos pos, SoundEvent event, String name ) + { + this.pos = pos; + this.name = name; + soundEvent = event; + } + + public PlayRecordClientMessage( BlockPos pos ) + { + this.pos = pos; + name = null; + soundEvent = null; + } + + public PlayRecordClientMessage( PacketByteBuf buf ) + { + pos = buf.readBlockPos(); + if( buf.readBoolean() ) + { + name = buf.readString( Short.MAX_VALUE ); + soundEvent = buf.readRegistryIdSafe( SoundEvent.class ); + } + else + { + name = null; + soundEvent = null; + } + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + buf.writeBlockPos( pos ); + if( soundEvent == null ) + { + buf.writeBoolean( false ); + } + else + { + buf.writeBoolean( true ); + buf.writeString( name ); + buf.writeRegistryId( soundEvent ); + } + } + + @Override + @Environment(EnvType.CLIENT) + public void handle( NetworkEvent.Context context ) + { + MinecraftClient mc = MinecraftClient.getInstance(); + mc.worldRenderer.playRecord( soundEvent, pos, null ); + if( name != null ) mc.inGameHud.setRecordPlayingOverlay( new LiteralText( name ) ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/TerminalState.java b/src/main/java/dan200/computercraft/shared/network/client/TerminalState.java new file mode 100644 index 000000000..fe32133c6 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/TerminalState.java @@ -0,0 +1,182 @@ +/* + * 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.network.client; + +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.util.IoUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.Unpooled; +import javax.annotation.Nullable; +import net.minecraft.network.PacketByteBuf; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * A snapshot of a terminal's state. + * + * This is somewhat memory inefficient (we build a buffer, only to write it elsewhere), however it means we get a + * complete and accurate description of a terminal, which avoids a lot of complexities with resizing terminals, dirty + * states, etc... + */ +public class TerminalState +{ + public final boolean colour; + + public final int width; + public final int height; + + private final boolean compress; + + @Nullable + private final ByteBuf buffer; + + private ByteBuf compressed; + + public TerminalState( boolean colour, @Nullable Terminal terminal ) + { + this( colour, terminal, true ); + } + + public TerminalState( boolean colour, @Nullable Terminal terminal, boolean compress ) + { + this.colour = colour; + this.compress = compress; + + if( terminal == null ) + { + this.width = this.height = 0; + this.buffer = null; + } + else + { + this.width = terminal.getWidth(); + this.height = terminal.getHeight(); + + ByteBuf buf = this.buffer = Unpooled.buffer(); + terminal.write( new PacketByteBuf( buf ) ); + } + } + + public TerminalState( PacketByteBuf buf ) + { + this.colour = buf.readBoolean(); + this.compress = buf.readBoolean(); + + if( buf.readBoolean() ) + { + this.width = buf.readVarInt(); + this.height = buf.readVarInt(); + + int length = buf.readVarInt(); + this.buffer = readCompressed( buf, length, compress ); + } + else + { + this.width = this.height = 0; + this.buffer = null; + } + } + + public void write( PacketByteBuf buf ) + { + buf.writeBoolean( colour ); + buf.writeBoolean( compress ); + + buf.writeBoolean( buffer != null ); + if( buffer != null ) + { + buf.writeVarInt( width ); + buf.writeVarInt( height ); + + ByteBuf sendBuffer = getCompressed(); + buf.writeVarInt( sendBuffer.readableBytes() ); + buf.writeBytes( sendBuffer, sendBuffer.readerIndex(), sendBuffer.readableBytes() ); + } + } + + public boolean hasTerminal() + { + return buffer != null; + } + + public int size() + { + return buffer == null ? 0 : buffer.readableBytes(); + } + + public void apply( Terminal terminal ) + { + if( buffer == null ) throw new NullPointerException( "buffer" ); + terminal.read( new PacketByteBuf( buffer ) ); + } + + private ByteBuf getCompressed() + { + if( buffer == null ) throw new NullPointerException( "buffer" ); + if( !compress ) return buffer; + if( compressed != null ) return compressed; + + ByteBuf compressed = Unpooled.directBuffer(); + OutputStream stream = null; + try + { + stream = new GZIPOutputStream( new ByteBufOutputStream( compressed ) ); + stream.write( buffer.array(), buffer.arrayOffset(), buffer.readableBytes() ); + } + catch( IOException e ) + { + throw new UncheckedIOException( e ); + } + finally + { + IoUtil.closeQuietly( stream ); + } + + return this.compressed = compressed; + } + + private static ByteBuf readCompressed( ByteBuf buf, int length, boolean compress ) + { + if( compress ) + { + ByteBuf buffer = Unpooled.buffer(); + InputStream stream = null; + try + { + stream = new GZIPInputStream( new ByteBufInputStream( buf, length ) ); + byte[] swap = new byte[8192]; + while( true ) + { + int bytes = stream.read( swap ); + if( bytes == -1 ) break; + buffer.writeBytes( swap, 0, bytes ); + } + } + catch( IOException e ) + { + throw new UncheckedIOException( e ); + } + finally + { + IoUtil.closeQuietly( stream ); + } + return buffer; + } + else + { + ByteBuf buffer = Unpooled.buffer( length ); + buf.readBytes( buffer, length ); + return buffer; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java b/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java new file mode 100644 index 000000000..0863a8ab0 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java @@ -0,0 +1,45 @@ +/* + * 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.network.container; + +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ServerComputer; +import net.minecraft.network.PacketByteBuf; + +public class ComputerContainerData implements ContainerData +{ + private final int id; + private final ComputerFamily family; + + public ComputerContainerData( ServerComputer computer ) + { + this.id = computer.getInstanceID(); + this.family = computer.getFamily(); + } + + public ComputerContainerData( PacketByteBuf buf ) + { + this.id = buf.readInt(); + this.family = buf.readEnumConstant( ComputerFamily.class ); + } + + @Override + public void toBytes( PacketByteBuf buf ) + { + buf.writeInt( id ); + buf.writeEnumConstant( family ); + } + + public int getInstanceId() + { + return id; + } + + public ComputerFamily getFamily() + { + return family; + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/container/ContainerData.java b/src/main/java/dan200/computercraft/shared/network/container/ContainerData.java new file mode 100644 index 000000000..003d9eadb --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/container/ContainerData.java @@ -0,0 +1,44 @@ +/* + * 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.network.container; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraftforge.common.extensions.IForgeContainerType; +import net.minecraftforge.fml.network.NetworkHooks; + +import javax.annotation.Nonnull; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * An extension over the basic {@link IForgeContainerType}/{@link NetworkHooks#openGui(ServerPlayerEntity, INamedContainerProvider, Consumer)} + * hooks, with a more convenient way of reading and writing data. + */ +public interface ContainerData +{ + void toBytes( PacketByteBuf buf ); + + default void open( PlayerEntity player, NamedScreenHandlerFactory owner ) + { + NetworkHooks.openGui( (ServerPlayerEntity) player, owner, this::toBytes ); + } + + static ScreenHandlerType toType( Function reader, Factory factory ) + { + return IForgeContainerType.create( ( id, player, data ) -> factory.create( id, player, reader.apply( data ) ) ); + } + + interface Factory + { + C create( int id, @Nonnull PlayerInventory inventory, T data ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/container/HeldItemContainerData.java b/src/main/java/dan200/computercraft/shared/network/container/HeldItemContainerData.java new file mode 100644 index 000000000..d7351b7e5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/container/HeldItemContainerData.java @@ -0,0 +1,45 @@ +/* + * 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.network.container; + +import dan200.computercraft.shared.common.ContainerHeldItem; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.Hand; + +import javax.annotation.Nonnull; + +/** + * Opens a printout GUI based on the currently held item. + * + * @see ContainerHeldItem + * @see dan200.computercraft.shared.media.items.ItemPrintout + */ +public class HeldItemContainerData implements ContainerData +{ + private final Hand hand; + + public HeldItemContainerData( Hand hand ) + { + this.hand = hand; + } + + public HeldItemContainerData( PacketByteBuf buffer ) + { + hand = buffer.readEnumConstant( Hand.class ); + } + + @Override + public void toBytes( PacketByteBuf buf ) + { + buf.writeEnumConstant( hand ); + } + + @Nonnull + public Hand getHand() + { + return hand; + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/container/ViewComputerContainerData.java b/src/main/java/dan200/computercraft/shared/network/container/ViewComputerContainerData.java new file mode 100644 index 000000000..35c54ae14 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/container/ViewComputerContainerData.java @@ -0,0 +1,62 @@ +/* + * 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.network.container; + +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.computer.core.ServerComputer; +import javax.annotation.Nonnull; +import net.minecraft.network.PacketByteBuf; + +/** + * View an arbitrary computer on the client. + * + * @see dan200.computercraft.shared.command.CommandComputerCraft + */ +public class ViewComputerContainerData extends ComputerContainerData +{ + private final int width; + private final int height; + + public ViewComputerContainerData( ServerComputer computer ) + { + super( computer ); + Terminal terminal = computer.getTerminal(); + if( terminal != null ) + { + width = terminal.getWidth(); + height = terminal.getHeight(); + } + else + { + width = height = 0; + } + } + + public ViewComputerContainerData( PacketByteBuf buffer ) + { + super( buffer ); + width = buffer.readVarInt(); + height = buffer.readVarInt(); + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + super.toBytes( buf ); + buf.writeVarInt( width ); + buf.writeVarInt( height ); + } + + public int getWidth() + { + return width; + } + + public int getHeight() + { + return height; + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java new file mode 100644 index 000000000..1af49d0f0 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java @@ -0,0 +1,64 @@ +/* + * 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.network.server; + +import dan200.computercraft.shared.computer.core.IContainerComputer; +import dan200.computercraft.shared.computer.core.ServerComputer; +import javax.annotation.Nonnull; +import net.minecraft.network.PacketByteBuf; + +public class ComputerActionServerMessage extends ComputerServerMessage +{ + private Action action; + + public ComputerActionServerMessage( int instanceId, Action action ) + { + super( instanceId ); + this.action = action; + } + + public ComputerActionServerMessage() + { + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + super.toBytes( buf ); + buf.writeEnumConstant( action ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + super.fromBytes( buf ); + action = buf.readEnumConstant( Action.class ); + } + + @Override + protected void handle( @Nonnull ServerComputer computer, @Nonnull IContainerComputer container ) + { + switch( action ) + { + case TURN_ON: + computer.turnOn(); + break; + case REBOOT: + computer.reboot(); + break; + case SHUTDOWN: + computer.shutdown(); + break; + } + } + + public enum Action + { + TURN_ON, + SHUTDOWN, + REBOOT + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java new file mode 100644 index 000000000..1c017193a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java @@ -0,0 +1,61 @@ +/* + * 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.network.server; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.IContainerComputer; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.network.NetworkMessage; +import net.minecraft.network.PacketByteBuf; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; + +/** + * A packet, which performs an action on a {@link ServerComputer}. + * + * This requires that the sending player is interacting with that computer via a + * {@link IContainerComputer}. + */ +public abstract class ComputerServerMessage implements NetworkMessage +{ + private int instanceId; + + public ComputerServerMessage( int instanceId ) + { + this.instanceId = instanceId; + } + + public ComputerServerMessage() + { + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + buf.writeVarInt( instanceId ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + instanceId = buf.readVarInt(); + } + + @Override + public void handle( NetworkEvent.Context context ) + { + ServerComputer computer = ComputerCraft.serverComputerRegistry.get( instanceId ); + if( computer == null ) return; + + IContainerComputer container = computer.getContainer( context.getSender() ); + if( container == null ) return; + + handle( computer, container ); + } + + protected abstract void handle( @Nonnull ServerComputer computer, @Nonnull IContainerComputer container ); +} diff --git a/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java new file mode 100644 index 000000000..b3793a210 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java @@ -0,0 +1,63 @@ +/* + * 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.network.server; + +import dan200.computercraft.shared.computer.core.IContainerComputer; +import dan200.computercraft.shared.computer.core.InputState; +import dan200.computercraft.shared.computer.core.ServerComputer; +import javax.annotation.Nonnull; +import net.minecraft.network.PacketByteBuf; + +public class KeyEventServerMessage extends ComputerServerMessage +{ + public static final int TYPE_DOWN = 0; + public static final int TYPE_REPEAT = 1; + public static final int TYPE_UP = 2; + + private int type; + private int key; + + public KeyEventServerMessage( int instanceId, int type, int key ) + { + super( instanceId ); + this.type = type; + this.key = key; + } + + public KeyEventServerMessage() + { + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + super.toBytes( buf ); + buf.writeByte( type ); + buf.writeVarInt( key ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + super.fromBytes( buf ); + type = buf.readByte(); + key = buf.readVarInt(); + } + + @Override + protected void handle( @Nonnull ServerComputer computer, @Nonnull IContainerComputer container ) + { + InputState input = container.getInput(); + if( type == TYPE_UP ) + { + input.keyUp( key ); + } + else + { + input.keyDown( key, type == TYPE_REPEAT ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java new file mode 100644 index 000000000..88851955d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java @@ -0,0 +1,79 @@ +/* + * 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.network.server; + +import dan200.computercraft.shared.computer.core.IContainerComputer; +import dan200.computercraft.shared.computer.core.InputState; +import dan200.computercraft.shared.computer.core.ServerComputer; +import javax.annotation.Nonnull; +import net.minecraft.network.PacketByteBuf; + +public class MouseEventServerMessage extends ComputerServerMessage +{ + public static final int TYPE_CLICK = 0; + public static final int TYPE_DRAG = 1; + public static final int TYPE_UP = 2; + public static final int TYPE_SCROLL = 3; + + private int type; + private int x; + private int y; + private int arg; + + public MouseEventServerMessage( int instanceId, int type, int arg, int x, int y ) + { + super( instanceId ); + this.type = type; + this.arg = arg; + this.x = x; + this.y = y; + } + + public MouseEventServerMessage() + { + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + super.toBytes( buf ); + buf.writeByte( type ); + buf.writeVarInt( arg ); + buf.writeVarInt( x ); + buf.writeVarInt( y ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + super.fromBytes( buf ); + type = buf.readByte(); + arg = buf.readVarInt(); + x = buf.readVarInt(); + y = buf.readVarInt(); + } + + @Override + protected void handle( @Nonnull ServerComputer computer, @Nonnull IContainerComputer container ) + { + InputState input = container.getInput(); + switch( type ) + { + case TYPE_CLICK: + input.mouseClick( arg, x, y ); + break; + case TYPE_DRAG: + input.mouseDrag( arg, x, y ); + break; + case TYPE_UP: + input.mouseUp( arg, x, y ); + break; + case TYPE_SCROLL: + input.mouseScroll( arg, x, y ); + break; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/server/QueueEventServerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/QueueEventServerMessage.java new file mode 100644 index 000000000..8eadbb617 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/server/QueueEventServerMessage.java @@ -0,0 +1,61 @@ +/* + * 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.network.server; + +import dan200.computercraft.shared.computer.core.IContainerComputer; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.util.NBTUtil; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.PacketByteBuf; + +/** + * Queue an event on a {@link ServerComputer}. + * + * @see dan200.computercraft.shared.computer.core.ClientComputer#queueEvent(String) + * @see ServerComputer#queueEvent(String) + */ +public class QueueEventServerMessage extends ComputerServerMessage +{ + private String event; + private Object[] args; + + public QueueEventServerMessage( int instanceId, @Nonnull String event, @Nullable Object[] args ) + { + super( instanceId ); + this.event = event; + this.args = args; + } + + public QueueEventServerMessage() + { + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + super.toBytes( buf ); + buf.writeString( event ); + buf.writeCompoundTag( args == null ? null : NBTUtil.encodeObjects( args ) ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + super.fromBytes( buf ); + event = buf.readString( Short.MAX_VALUE ); + + CompoundTag args = buf.readCompoundTag(); + this.args = args == null ? null : NBTUtil.decodeObjects( args ); + } + + @Override + protected void handle( @Nonnull ServerComputer computer, @Nonnull IContainerComputer container ) + { + computer.queueEvent( event, args ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/server/RequestComputerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/RequestComputerMessage.java new file mode 100644 index 000000000..e83c4bcf2 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/server/RequestComputerMessage.java @@ -0,0 +1,47 @@ +/* + * 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.network.server; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.network.NetworkMessage; +import net.minecraft.network.PacketByteBuf; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; + +public class RequestComputerMessage implements NetworkMessage +{ + private int instance; + + public RequestComputerMessage( int instance ) + { + this.instance = instance; + } + + public RequestComputerMessage() + { + } + + @Override + public void toBytes( @Nonnull PacketByteBuf buf ) + { + buf.writeVarInt( instance ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + instance = buf.readVarInt(); + } + + @Override + public void handle( NetworkEvent.Context context ) + { + ServerComputer computer = ComputerCraft.serverComputerRegistry.get( instance ); + if( computer != null ) computer.sendComputerState( context.getSender() ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java new file mode 100644 index 000000000..b4accd625 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java @@ -0,0 +1,138 @@ +/* + * 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.commandblock; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.computer.apis.CommandAPI; +import dan200.computercraft.shared.util.CapabilityUtil; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.CommandBlockBlockEntity; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Direction; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; + +/** + * This peripheral allows you to interact with command blocks. + * + * Command blocks are only wrapped as peripherals if the {@literal enable_command_block} option is true within the + * config. + * + * This API is not the same as the {@link CommandAPI} API, which is exposed on command computers. + * + * @cc.module command + */ +@Mod.EventBusSubscriber +public class CommandBlockPeripheral implements IPeripheral, ICapabilityProvider +{ + private static final Identifier CAP_ID = new Identifier( ComputerCraft.MOD_ID, "command_block" ); + + private final CommandBlockBlockEntity commandBlock; + private LazyOptional self; + + public CommandBlockPeripheral( CommandBlockBlockEntity commandBlock ) + { + this.commandBlock = commandBlock; + } + + @Nonnull + @Override + public String getType() + { + return "command"; + } + + /** + * Get the command this command block will run. + * + * @return The current command. + */ + @LuaFunction( mainThread = true ) + public final String getCommand() + { + return commandBlock.getCommandExecutor().getCommand(); + } + + /** + * Set the command block's command. + * + * @param command The new command. + */ + @LuaFunction( mainThread = true ) + public final void setCommand( String command ) + { + commandBlock.getCommandExecutor().setCommand( command ); + commandBlock.getCommandExecutor().markDirty(); + } + + /** + * Execute the command block once. + * + * @return The result of executing. + * @cc.treturn boolean If the command completed successfully. + * @cc.treturn string|nil A failure message. + */ + @LuaFunction( mainThread = true ) + public final Object[] runCommand() + { + commandBlock.getCommandExecutor().execute( commandBlock.getWorld() ); + int result = commandBlock.getCommandExecutor().getSuccessCount(); + return result > 0 ? new Object[] { true } : new Object[] { false, "Command failed" }; + } + + @Override + public boolean equals( IPeripheral other ) + { + return other != null && other.getClass() == getClass(); + } + + @Nonnull + @Override + public Object getTarget() + { + return commandBlock; + } + + @Nonnull + @Override + public LazyOptional getCapability( @Nonnull Capability cap, @Nullable Direction side ) + { + if( cap == CAPABILITY_PERIPHERAL ) + { + if( self == null ) self = LazyOptional.of( () -> this ); + return self.cast(); + } + return LazyOptional.empty(); + } + + private void invalidate() + { + self = CapabilityUtil.invalidate( self ); + } + + @SubscribeEvent + public static void onCapability( AttachCapabilitiesEvent event ) + { + BlockEntity tile = event.getObject(); + if( tile instanceof CommandBlockBlockEntity ) + { + CommandBlockPeripheral peripheral = new CommandBlockPeripheral( (CommandBlockBlockEntity) tile ); + event.addCapability( CAP_ID, peripheral ); + event.addListener( peripheral::invalidate ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/BlockDiskDrive.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/BlockDiskDrive.java new file mode 100644 index 000000000..53d1aaf84 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/BlockDiskDrive.java @@ -0,0 +1,84 @@ +/* + * 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.diskdrive; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.common.BlockGeneric; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.stat.Stats; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.DirectionProperty; +import net.minecraft.state.property.EnumProperty; +import net.minecraft.state.property.Properties; +import net.minecraft.util.Nameable; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class BlockDiskDrive extends BlockGeneric +{ + static final DirectionProperty FACING = Properties.HORIZONTAL_FACING; + static final EnumProperty STATE = EnumProperty.of( "state", DiskDriveState.class ); + + public BlockDiskDrive( Settings settings ) + { + super( settings, Registry.ModTiles.DISK_DRIVE ); + setDefaultState( getStateManager().getDefaultState() + .with( FACING, Direction.NORTH ) + .with( STATE, DiskDriveState.EMPTY ) ); + } + + + @Override + protected void appendProperties( StateManager.Builder properties ) + { + properties.add( FACING, STATE ); + } + + @Nullable + @Override + public BlockState getPlacementState( ItemPlacementContext placement ) + { + return getDefaultState().with( FACING, placement.getPlayerFacing().getOpposite() ); + } + + @Override + public void afterBreak( @Nonnull World world, @Nonnull PlayerEntity player, @Nonnull BlockPos pos, @Nonnull BlockState state, @Nullable BlockEntity te, @Nonnull ItemStack stack ) + { + if( te instanceof Nameable && ((Nameable) te).hasCustomName() ) + { + player.incrementStat( Stats.MINED.getOrCreateStat( this ) ); + player.addExhaustion( 0.005F ); + + ItemStack result = new ItemStack( this ); + result.setCustomName( ((Nameable) te).getCustomName() ); + dropStack( world, pos, result ); + } + else + { + super.afterBreak( world, player, pos, state, te, stack ); + } + } + + @Override + public void onPlaced( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState state, LivingEntity placer, ItemStack stack ) + { + if( stack.hasCustomName() ) + { + BlockEntity tileentity = world.getBlockEntity( pos ); + if( tileentity instanceof TileDiskDrive ) ((TileDiskDrive) tileentity).customName = stack.getName(); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/ContainerDiskDrive.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/ContainerDiskDrive.java new file mode 100644 index 000000000..ba0df2f84 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/ContainerDiskDrive.java @@ -0,0 +1,89 @@ +/* + * 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.diskdrive; + +import dan200.computercraft.shared.Registry; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SimpleInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.Slot; +import javax.annotation.Nonnull; + +public class ContainerDiskDrive extends ScreenHandler +{ + private final Inventory inventory; + + public ContainerDiskDrive( int id, PlayerInventory player, Inventory inventory ) + { + super( Registry.ModContainers.DISK_DRIVE.get(), id ); + + this.inventory = inventory; + + addSlot( new Slot( this.inventory, 0, 8 + 4 * 18, 35 ) ); + + for( int y = 0; y < 3; y++ ) + { + for( int x = 0; x < 9; x++ ) + { + addSlot( new Slot( player, x + y * 9 + 9, 8 + x * 18, 84 + y * 18 ) ); + } + } + + for( int x = 0; x < 9; x++ ) + { + addSlot( new Slot( player, x, 8 + x * 18, 142 ) ); + } + } + + public ContainerDiskDrive( int id, PlayerInventory player ) + { + this( id, player, new SimpleInventory( 1 ) ); + } + + @Override + public boolean canUse( @Nonnull PlayerEntity player ) + { + return inventory.canPlayerUse( player ); + } + + @Nonnull + @Override + public ItemStack transferSlot( @Nonnull PlayerEntity player, int slotIndex ) + { + Slot slot = slots.get( slotIndex ); + if( slot == null || !slot.hasStack() ) return ItemStack.EMPTY; + + ItemStack existing = slot.getStack().copy(); + ItemStack result = existing.copy(); + if( slotIndex == 0 ) + { + // Insert into player inventory + if( !insertItem( existing, 1, 37, true ) ) return ItemStack.EMPTY; + } + else + { + // Insert into drive inventory + if( !insertItem( existing, 0, 1, false ) ) return ItemStack.EMPTY; + } + + if( existing.isEmpty() ) + { + slot.setStack( ItemStack.EMPTY ); + } + else + { + slot.markDirty(); + } + + if( existing.getCount() == result.getCount() ) return ItemStack.EMPTY; + + slot.onTakeItem( player, existing ); + return result; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java new file mode 100644 index 000000000..dd15192db --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java @@ -0,0 +1,220 @@ +/* + * 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.diskdrive; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.MediaProviders; +import dan200.computercraft.shared.media.items.ItemDisk; +import dan200.computercraft.shared.util.StringUtil; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Optional; + +/** + * Disk drives are a peripheral which allow you to read and write to floppy disks and other "mountable media" (such as + * computers or turtles). They also allow you to {@link #playAudio play records}. + * + * When a disk drive attaches some mount (such as a floppy disk or computer), it attaches a folder called {@code disk}, + * {@code disk2}, etc... to the root directory of the computer. This folder can be used to interact with the files on + * that disk. + * + * When a disk is inserted, a {@code disk} event is fired, with the side peripheral is on. Likewise, when the disk is + * detached, a {@code disk_eject} event is fired. + * + * @cc.module drive + */ +public class DiskDrivePeripheral implements IPeripheral +{ + private final TileDiskDrive diskDrive; + + DiskDrivePeripheral( TileDiskDrive diskDrive ) + { + this.diskDrive = diskDrive; + } + + @Nonnull + @Override + public String getType() + { + return "drive"; + } + + /** + * Returns whether a disk is currently inserted in the drive. + * + * @return Whether a disk is currently inserted in the drive. + */ + @LuaFunction + public final boolean isDiskPresent() + { + return !diskDrive.getDiskStack().isEmpty(); + } + + /** + * Returns the label of the disk in the drive if available. + * + * @return The label of the disk, or {@code nil} if either no disk is inserted or the disk doesn't have a label. + * @cc.treturn string The label of the disk, or {@code nil} if either no disk is inserted or the disk doesn't have a label. + */ + @LuaFunction + public final Object[] getDiskLabel() + { + ItemStack stack = diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + return media == null ? null : new Object[] { media.getLabel( stack ) }; + } + + /** + * Sets or clears the label for a disk. + * + * If no label or {@code nil} is passed, the label will be cleared. + * + * If the inserted disk's label can't be changed (for example, a record), + * an error will be thrown. + * + * @param labelA The new label of the disk, or {@code nil} to clear. + * @throws LuaException If the disk's label can't be changed. + */ + @LuaFunction( mainThread = true ) + public final void setDiskLabel( Optional labelA ) throws LuaException + { + String label = labelA.orElse( null ); + ItemStack stack = diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + if( media == null ) return; + + if( !media.setLabel( stack, StringUtil.normaliseLabel( label ) ) ) + { + throw new LuaException( "Disk label cannot be changed" ); + } + diskDrive.setDiskStack( stack ); + } + + /** + * Returns whether a disk with data is inserted. + * + * @param computer The computer object + * @return Whether a disk with data is inserted. + */ + @LuaFunction + public final boolean hasData( IComputerAccess computer ) + { + return diskDrive.getDiskMountPath( computer ) != null; + } + + /** + * Returns the mount path for the inserted disk. + * + * @param computer The computer object + * @return The mount path for the disk, or {@code nil} if no data disk is inserted. + */ + @LuaFunction + @Nullable + public final String getMountPath( IComputerAccess computer ) + { + return diskDrive.getDiskMountPath( computer ); + } + + /** + * Returns whether a disk with audio is inserted. + * + * @return Whether a disk with audio is inserted. + */ + @LuaFunction + public final boolean hasAudio() + { + ItemStack stack = diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + return media != null && media.getAudio( stack ) != null; + } + + /** + * Returns the title of the inserted audio disk. + * + * @return The title of the audio, or {@code nil} if no audio disk is inserted. + */ + @LuaFunction + @Nullable + public final Object getAudioTitle() + { + ItemStack stack = diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + return media != null ? media.getAudioTitle( stack ) : false; + } + + /** + * Plays the audio in the inserted disk, if available. + */ + @LuaFunction + public final void playAudio() + { + diskDrive.playDiskAudio(); + } + + /** + * Stops any audio that may be playing. + * + * @see #playAudio + */ + @LuaFunction + public final void stopAudio() + { + diskDrive.stopDiskAudio(); + } + + /** + * Ejects any disk that may be in the drive. + */ + @LuaFunction + public final void ejectDisk() + { + diskDrive.ejectDisk(); + } + + /** + * Returns the ID of the disk inserted in the drive. + * + * @return The ID of the disk in the drive, or {@code nil} if no disk with an ID is inserted. + * @cc.treturn number The The ID of the disk in the drive, or {@code nil} if no disk with an ID is inserted. + */ + @LuaFunction + public final Object[] getDiskID() + { + ItemStack disk = diskDrive.getDiskStack(); + return disk.getItem() instanceof ItemDisk ? new Object[] { ItemDisk.getDiskID( disk ) } : null; + } + + @Override + public void attach( @Nonnull IComputerAccess computer ) + { + diskDrive.mount( computer ); + } + + @Override + public void detach( @Nonnull IComputerAccess computer ) + { + diskDrive.unmount( computer ); + } + + @Override + public boolean equals( IPeripheral other ) + { + return this == other || other instanceof DiskDrivePeripheral && ((DiskDrivePeripheral) other).diskDrive == diskDrive; + } + + @Nonnull + @Override + public Object getTarget() + { + return diskDrive; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveState.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveState.java new file mode 100644 index 000000000..6444a41e3 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveState.java @@ -0,0 +1,30 @@ +/* + * 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.diskdrive; + +import javax.annotation.Nonnull; +import net.minecraft.util.StringIdentifiable; + +public enum DiskDriveState implements StringIdentifiable +{ + EMPTY( "empty" ), + FULL( "full" ), + INVALID( "invalid" ); + + private final String name; + + DiskDriveState( String name ) + { + this.name = name; + } + + @Override + @Nonnull + public String asString() + { + return name; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java new file mode 100644 index 000000000..89b6035b8 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java @@ -0,0 +1,569 @@ +/* + * 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.diskdrive; + +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.MediaProviders; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.util.CapabilityUtil; +import dan200.computercraft.shared.util.DefaultInventory; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.RecordUtil; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.ItemEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.sound.SoundEvent; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.*; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.fml.network.NetworkHooks; +import net.minecraftforge.items.IItemHandlerModifiable; +import net.minecraftforge.items.wrapper.InvWrapper; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; +import static net.minecraftforge.items.CapabilityItemHandler.ITEM_HANDLER_CAPABILITY; + +public final class TileDiskDrive extends TileGeneric implements DefaultInventory, Tickable, Nameable, NamedScreenHandlerFactory +{ + private static final String NBT_NAME = "CustomName"; + private static final String NBT_ITEM = "Item"; + + private static class MountInfo + { + String mountPath; + } + + Text customName; + + private final Map m_computers = new HashMap<>(); + + @Nonnull + private ItemStack m_diskStack = ItemStack.EMPTY; + private LazyOptional itemHandlerCap; + private LazyOptional peripheralCap; + private IMount m_diskMount = null; + + private boolean m_recordQueued = false; + private boolean m_recordPlaying = false; + private boolean m_restartRecord = false; + private boolean m_ejectQueued; + + public TileDiskDrive( BlockEntityType type ) + { + super( type ); + } + + @Override + public void destroy() + { + ejectContents( true ); + if( m_recordPlaying ) stopRecord(); + } + + @Override + protected void invalidateCaps() + { + super.invalidateCaps(); + itemHandlerCap = CapabilityUtil.invalidate( itemHandlerCap ); + peripheralCap = CapabilityUtil.invalidate( peripheralCap ); + } + + @Nonnull + @Override + public ActionResult onActivate( PlayerEntity player, Hand hand, BlockHitResult hit ) + { + if( player.isInSneakingPose() ) + { + // Try to put a disk into the drive + ItemStack disk = player.getStackInHand( hand ); + if( disk.isEmpty() ) return ActionResult.PASS; + if( !getWorld().isClient && getStack( 0 ).isEmpty() && MediaProviders.get( disk ) != null ) + { + setDiskStack( disk ); + player.setStackInHand( hand, ItemStack.EMPTY ); + } + return ActionResult.SUCCESS; + } + else + { + // Open the GUI + if( !getWorld().isClient ) NetworkHooks.openGui( (ServerPlayerEntity) player, this ); + return ActionResult.SUCCESS; + } + } + + public Direction getDirection() + { + return getCachedState().get( BlockDiskDrive.FACING ); + } + + @Override + public void fromTag( @Nonnull BlockState state, @Nonnull CompoundTag nbt ) + { + super.fromTag( state, nbt ); + customName = nbt.contains( NBT_NAME ) ? Text.Serializer.fromJson( nbt.getString( NBT_NAME ) ) : null; + if( nbt.contains( NBT_ITEM ) ) + { + CompoundTag item = nbt.getCompound( NBT_ITEM ); + m_diskStack = ItemStack.fromTag( item ); + m_diskMount = null; + } + } + + @Nonnull + @Override + public CompoundTag toTag( @Nonnull CompoundTag nbt ) + { + if( customName != null ) nbt.putString( NBT_NAME, Text.Serializer.toJson( customName ) ); + + if( !m_diskStack.isEmpty() ) + { + CompoundTag item = new CompoundTag(); + m_diskStack.toTag( item ); + nbt.put( NBT_ITEM, item ); + } + return super.toTag( nbt ); + } + + @Override + public void tick() + { + // Ejection + if( m_ejectQueued ) + { + ejectContents( false ); + m_ejectQueued = false; + } + + // Music + synchronized( this ) + { + if( !world.isClient && m_recordPlaying != m_recordQueued || m_restartRecord ) + { + m_restartRecord = false; + if( m_recordQueued ) + { + IMedia contents = getDiskMedia(); + SoundEvent record = contents != null ? contents.getAudio( m_diskStack ) : null; + if( record != null ) + { + m_recordPlaying = true; + playRecord(); + } + else + { + m_recordQueued = false; + } + } + else + { + stopRecord(); + m_recordPlaying = false; + } + } + } + } + + // IInventory implementation + + @Override + public int size() + { + return 1; + } + + @Override + public boolean isEmpty() + { + return m_diskStack.isEmpty(); + } + + @Nonnull + @Override + public ItemStack getStack( int slot ) + { + return m_diskStack; + } + + @Nonnull + @Override + public ItemStack removeStack( int slot ) + { + ItemStack result = m_diskStack; + m_diskStack = ItemStack.EMPTY; + m_diskMount = null; + + return result; + } + + @Nonnull + @Override + public ItemStack removeStack( int slot, int count ) + { + if( m_diskStack.isEmpty() ) return ItemStack.EMPTY; + + if( m_diskStack.getCount() <= count ) + { + ItemStack disk = m_diskStack; + setStack( slot, ItemStack.EMPTY ); + return disk; + } + + ItemStack part = m_diskStack.split( count ); + setStack( slot, m_diskStack.isEmpty() ? ItemStack.EMPTY : m_diskStack ); + return part; + } + + @Override + public void setStack( int slot, @Nonnull ItemStack stack ) + { + if( getWorld().isClient ) + { + m_diskStack = stack; + m_diskMount = null; + markDirty(); + return; + } + + synchronized( this ) + { + if( InventoryUtil.areItemsStackable( stack, m_diskStack ) ) + { + m_diskStack = stack; + return; + } + + // Unmount old disk + if( !m_diskStack.isEmpty() ) + { + // TODO: Is this iteration thread safe? + Set computers = m_computers.keySet(); + for( IComputerAccess computer : computers ) unmountDisk( computer ); + } + + // Stop music + if( m_recordPlaying ) + { + stopRecord(); + m_recordPlaying = false; + m_recordQueued = false; + } + + // Swap disk over + m_diskStack = stack; + m_diskMount = null; + markDirty(); + + // Mount new disk + if( !m_diskStack.isEmpty() ) + { + Set computers = m_computers.keySet(); + for( IComputerAccess computer : computers ) mountDisk( computer ); + } + } + } + + @Override + public void markDirty() + { + if( !world.isClient ) updateBlockState(); + super.markDirty(); + } + + @Override + public boolean canPlayerUse( @Nonnull PlayerEntity player ) + { + return isUsable( player, false ); + } + + @Override + public void clear() + { + setStack( 0, ItemStack.EMPTY ); + } + + @Nonnull + ItemStack getDiskStack() + { + return getStack( 0 ); + } + + void setDiskStack( @Nonnull ItemStack stack ) + { + setStack( 0, stack ); + } + + private IMedia getDiskMedia() + { + return MediaProviders.get( getDiskStack() ); + } + + String getDiskMountPath( IComputerAccess computer ) + { + synchronized( this ) + { + MountInfo info = m_computers.get( computer ); + return info != null ? info.mountPath : null; + } + } + + void mount( IComputerAccess computer ) + { + synchronized( this ) + { + m_computers.put( computer, new MountInfo() ); + mountDisk( computer ); + } + } + + void unmount( IComputerAccess computer ) + { + synchronized( this ) + { + unmountDisk( computer ); + m_computers.remove( computer ); + } + } + + void playDiskAudio() + { + synchronized( this ) + { + IMedia media = getDiskMedia(); + if( media != null && media.getAudioTitle( m_diskStack ) != null ) + { + m_recordQueued = true; + m_restartRecord = m_recordPlaying; + } + } + } + + void stopDiskAudio() + { + synchronized( this ) + { + m_recordQueued = false; + m_restartRecord = false; + } + } + + void ejectDisk() + { + synchronized( this ) + { + m_ejectQueued = true; + } + } + + // private methods + + private synchronized void mountDisk( IComputerAccess computer ) + { + if( !m_diskStack.isEmpty() ) + { + MountInfo info = m_computers.get( computer ); + IMedia contents = getDiskMedia(); + if( contents != null ) + { + if( m_diskMount == null ) + { + m_diskMount = contents.createDataMount( m_diskStack, getWorld() ); + } + if( m_diskMount != null ) + { + if( m_diskMount instanceof IWritableMount ) + { + // Try mounting at the lowest numbered "disk" name we can + int n = 1; + while( info.mountPath == null ) + { + info.mountPath = computer.mountWritable( n == 1 ? "disk" : "disk" + n, (IWritableMount) m_diskMount ); + n++; + } + } + else + { + // Try mounting at the lowest numbered "disk" name we can + int n = 1; + while( info.mountPath == null ) + { + info.mountPath = computer.mount( n == 1 ? "disk" : "disk" + n, m_diskMount ); + n++; + } + } + } + else + { + info.mountPath = null; + } + } + computer.queueEvent( "disk", computer.getAttachmentName() ); + } + } + + private synchronized void unmountDisk( IComputerAccess computer ) + { + if( !m_diskStack.isEmpty() ) + { + MountInfo info = m_computers.get( computer ); + assert info != null; + if( info.mountPath != null ) + { + computer.unmount( info.mountPath ); + info.mountPath = null; + } + computer.queueEvent( "disk_eject", computer.getAttachmentName() ); + } + } + + private void updateBlockState() + { + if( removed ) return; + + if( !m_diskStack.isEmpty() ) + { + IMedia contents = getDiskMedia(); + updateBlockState( contents != null ? DiskDriveState.FULL : DiskDriveState.INVALID ); + } + else + { + updateBlockState( DiskDriveState.EMPTY ); + } + } + + private void updateBlockState( DiskDriveState state ) + { + BlockState blockState = getCachedState(); + if( blockState.get( BlockDiskDrive.STATE ) == state ) return; + + getWorld().setBlockState( getPos(), blockState.with( BlockDiskDrive.STATE, state ) ); + } + + private synchronized void ejectContents( boolean destroyed ) + { + if( getWorld().isClient || m_diskStack.isEmpty() ) return; + + // Remove the disks from the inventory + ItemStack disks = m_diskStack; + setDiskStack( ItemStack.EMPTY ); + + // Spawn the item in the world + int xOff = 0; + int zOff = 0; + if( !destroyed ) + { + Direction dir = getDirection(); + xOff = dir.getOffsetX(); + zOff = dir.getOffsetZ(); + } + + BlockPos pos = getPos(); + double x = pos.getX() + 0.5 + xOff * 0.5; + double y = pos.getY() + 0.75; + double z = pos.getZ() + 0.5 + zOff * 0.5; + ItemEntity entityitem = new ItemEntity( getWorld(), x, y, z, disks ); + entityitem.setVelocity( xOff * 0.15, 0, zOff * 0.15 ); + + getWorld().spawnEntity( entityitem ); + if( !destroyed ) getWorld().syncGlobalEvent( 1000, getPos(), 0 ); + } + + // Private methods + + private void playRecord() + { + IMedia contents = getDiskMedia(); + SoundEvent record = contents != null ? contents.getAudio( m_diskStack ) : null; + if( record != null ) + { + RecordUtil.playRecord( record, contents.getAudioTitle( m_diskStack ), getWorld(), getPos() ); + } + else + { + RecordUtil.playRecord( null, null, getWorld(), getPos() ); + } + } + + private void stopRecord() + { + RecordUtil.playRecord( null, null, getWorld(), getPos() ); + } + + @Nonnull + @Override + public LazyOptional getCapability( @Nonnull Capability cap, @Nullable final Direction side ) + { + if( cap == ITEM_HANDLER_CAPABILITY ) + { + if( itemHandlerCap == null ) itemHandlerCap = LazyOptional.of( () -> new InvWrapper( this ) ); + return itemHandlerCap.cast(); + } + + if( cap == CAPABILITY_PERIPHERAL ) + { + if( peripheralCap == null ) peripheralCap = LazyOptional.of( () -> new DiskDrivePeripheral( this ) ); + return peripheralCap.cast(); + } + + return super.getCapability( cap, side ); + } + + @Override + public boolean hasCustomName() + { + return customName != null; + } + + @Nullable + @Override + public Text getCustomName() + { + return customName; + } + + @Nonnull + @Override + public Text getName() + { + return customName != null ? customName : new TranslatableText( getCachedState().getBlock().getTranslationKey() ); + } + + @Nonnull + @Override + public Text getDisplayName() + { + return Nameable.super.getDisplayName(); + } + + @Nonnull + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory inventory, @Nonnull PlayerEntity player ) + { + return new ContainerDiskDrive( id, inventory, this ); + } +} 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..02a9726e6 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java @@ -0,0 +1,75 @@ +/* + * 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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.util.Identifier; +import java.util.List; + +class GenericPeripheral implements IDynamicPeripheral +{ + private final String type; + private final BlockEntity tile; + private final List methods; + + GenericPeripheral( BlockEntity tile, List methods ) + { + Identifier 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..0cedb9764 --- /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.block.entity.BlockEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +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(); + + BlockEntity tile = world.getBlockEntity( 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/data/BlockData.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/BlockData.java new file mode 100644 index 000000000..69b93db05 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/BlockData.java @@ -0,0 +1,41 @@ +/* + * 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.data; + +import com.google.common.collect.ImmutableMap; +import net.minecraft.block.BlockState; +import net.minecraft.state.property.Property; +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; + +public class BlockData +{ + @Nonnull + public static > T fill( @Nonnull T data, @Nonnull BlockState state ) + { + data.put( "name", DataHelpers.getId( state.getBlock() ) ); + + Map stateTable = new HashMap<>(); + for( ImmutableMap.Entry, ? extends Comparable> entry : state.getEntries().entrySet() ) + { + Property property = entry.getKey(); + stateTable.put( property.getName(), getPropertyValue( property, entry.getValue() ) ); + } + data.put( "state", stateTable ); + data.put( "tags", DataHelpers.getTags( state.getBlock().getTags() ) ); + + return data; + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + private static Object getPropertyValue( Property property, Comparable value ) + { + if( value instanceof String || value instanceof Number || value instanceof Boolean ) return value; + return property.name( value ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/data/DataHelpers.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/DataHelpers.java new file mode 100644 index 000000000..0f6ced166 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/DataHelpers.java @@ -0,0 +1,37 @@ +/* + * 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.data; + +import net.minecraft.util.Identifier; +import net.minecraftforge.registries.IForgeRegistryEntry; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public final class DataHelpers +{ + private DataHelpers() + { } + + @Nonnull + public static Map getTags( @Nonnull Collection tags ) + { + Map result = new HashMap<>( tags.size() ); + for( Identifier location : tags ) result.put( location.toString(), true ); + return result; + } + + @Nullable + public static String getId( @Nonnull IForgeRegistryEntry entry ) + { + Identifier id = entry.getRegistryName(); + return id == null ? null : id.toString(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/data/FluidData.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/FluidData.java new file mode 100644 index 000000000..e5b19af7d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/FluidData.java @@ -0,0 +1,31 @@ +/* + * 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.data; + +import net.minecraftforge.fluids.FluidStack; + +import javax.annotation.Nonnull; +import java.util.Map; + +public class FluidData +{ + @Nonnull + public static > T fillBasic( @Nonnull T data, @Nonnull FluidStack stack ) + { + data.put( "name", DataHelpers.getId( stack.getFluid() ) ); + data.put( "amount", stack.getAmount() ); + return data; + } + + @Nonnull + public static > T fill( @Nonnull T data, @Nonnull FluidStack stack ) + { + fillBasic( data, stack ); + data.put( "tags", DataHelpers.getTags( stack.getFluid().getTags() ) ); + return data; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/data/ItemData.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/ItemData.java new file mode 100644 index 000000000..0d5250157 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/data/ItemData.java @@ -0,0 +1,178 @@ +/* + * 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.data; + +import com.google.gson.JsonParseException; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.util.NBTUtil; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.enchantment.EnchantmentHelper; +import net.minecraft.item.EnchantedBookItem; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.text.Text; +import net.minecraftforge.common.util.Constants; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Data providers for items. + * + * We guard using {@link ComputerCraft#genericPeripheral} in several places, as advanced functionality should not be + * exposed for {@code turtle.getItemDetail} when generic peripehrals are disabled. + */ +public class ItemData +{ + @Nonnull + public static > T fillBasicSafe( @Nonnull T data, @Nonnull ItemStack stack ) + { + data.put( "name", DataHelpers.getId( stack.getItem() ) ); + data.put( "count", stack.getCount() ); + + return data; + } + + @Nonnull + public static > T fillBasic( @Nonnull T data, @Nonnull ItemStack stack ) + { + fillBasicSafe( data, stack ); + String hash = NBTUtil.getNBTHash( stack.getTag() ); + if( hash != null ) data.put( "nbt", hash ); + return data; + } + + @Nonnull + public static > T fill( @Nonnull T data, @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return data; + + fillBasic( data, stack ); + + data.put( "displayName", stack.getName().getString() ); + data.put( "maxCount", stack.getMaxCount() ); + + 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 ) ); + } + + data.put( "tags", DataHelpers.getTags( stack.getItem().getTags() ) ); + + if( !ComputerCraft.genericPeripheral ) return data; + + CompoundTag tag = stack.getTag(); + if( tag != null && tag.contains( "display", Constants.NBT.TAG_COMPOUND ) ) + { + CompoundTag displayTag = tag.getCompound( "display" ); + if( displayTag.contains( "Lore", Constants.NBT.TAG_LIST ) ) + { + ListTag loreTag = displayTag.getList( "Lore", Constants.NBT.TAG_STRING ); + data.put( "lore", loreTag.stream() + .map( ItemData::parseTextComponent ) + .filter( Objects::nonNull ) + .map( Text::getString ) + .collect( Collectors.toList() ) ); + } + } + + /* + * Used to hide some data from ItemStack tooltip. + * @see https://minecraft.gamepedia.com/Tutorials/Command_NBT_tags + * @see ItemStack#getTooltip + */ + int hideFlags = tag != null ? tag.getInt( "HideFlags" ) : 0; + + List> enchants = getAllEnchants( stack, hideFlags ); + if( !enchants.isEmpty() ) data.put( "enchantments", enchants ); + + if( tag != null && tag.getBoolean( "Unbreakable" ) && (hideFlags & 4) == 0 ) + { + data.put( "unbreakable", true ); + } + + return data; + } + + @Nullable + private static Text parseTextComponent( @Nonnull Tag x ) + { + try + { + return Text.Serializer.fromJson( x.asString() ); + } + catch( JsonParseException e ) + { + return null; + } + } + + /** + * Retrieve all visible enchantments from given stack. Try to follow all tooltip rules : order and visibility. + * + * @param stack Stack to analyse + * @param hideFlags An int used as bit field to provide visibility rules. + * @return A filled list that contain all visible enchantments. + */ + @Nonnull + private static List> getAllEnchants( @Nonnull ItemStack stack, int hideFlags ) + { + ArrayList> enchants = new ArrayList<>( 0 ); + + if( stack.getItem() instanceof EnchantedBookItem && (hideFlags & 32) == 0 ) + { + addEnchantments( EnchantedBookItem.getEnchantmentTag( stack ), enchants ); + } + + if( stack.hasEnchantments() && (hideFlags & 1) == 0 ) + { + /* + * Mimic the EnchantmentHelper.getEnchantments(ItemStack stack) behavior without special case for Enchanted book. + * I'll do that to have the same data than ones displayed in tooltip. + * @see EnchantmentHelper.getEnchantments(ItemStack stack) + */ + addEnchantments( stack.getEnchantments(), enchants ); + } + + return enchants; + } + + /** + * Converts a Mojang enchant map to a Lua list. + * + * @param rawEnchants The raw NBT list of enchantments + * @param enchants The enchantment map to add it to. + * @see net.minecraft.enchantment.EnchantmentHelper + */ + private static void addEnchantments( @Nonnull ListTag rawEnchants, @Nonnull ArrayList> enchants ) + { + if( rawEnchants.isEmpty() ) return; + + enchants.ensureCapacity( enchants.size() + rawEnchants.size() ); + + for( Map.Entry entry : EnchantmentHelper.fromTag( rawEnchants ).entrySet() ) + { + Enchantment enchantment = entry.getKey(); + Integer level = entry.getValue(); + HashMap enchant = new HashMap<>( 3 ); + enchant.put( "name", DataHelpers.getId( enchantment ) ); + enchant.put( "level", level ); + enchant.put( "displayName", enchantment.getName( level ).getString() ); + enchants.add( enchant ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/ArgumentHelpers.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/ArgumentHelpers.java new file mode 100644 index 000000000..7c95de7a9 --- /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.Identifier; +import net.minecraft.util.InvalidIdentifierException; +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 + { + Identifier id; + try + { + id = new Identifier( name ); + } + catch( InvalidIdentifierException 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..c2da4dd21 --- /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.Identifier; +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 Identifier id() + { + return new Identifier( 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..45d8dd696 --- /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.data.FluidData; +import net.minecraft.fluid.Fluid; +import net.minecraft.util.Identifier; +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 Identifier id() + { + return new Identifier( 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, FluidData.fillBasic( 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..9c7257d6a --- /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.data.ItemData; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +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 Identifier id() + { + return new Identifier( 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, ItemData.fillBasic( new HashMap<>( 4 ), stack ) ); + } + + return result; + } + + @LuaFunction( mainThread = true ) + public static Map getItemDetail( 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 : ItemData.fill( 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 Inventory ) return new InvWrapper( (Inventory) 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; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemPeripheral.java new file mode 100644 index 000000000..0805406a9 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemPeripheral.java @@ -0,0 +1,246 @@ +/* + * 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.modem; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.network.IPacketNetwork; +import dan200.computercraft.api.network.IPacketReceiver; +import dan200.computercraft.api.network.IPacketSender; +import dan200.computercraft.api.network.Packet; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import java.util.HashSet; +import java.util.Set; + +/** + * The modem peripheral allows you to send messages between computers. + * + * @cc.module modem + */ +public abstract class ModemPeripheral implements IPeripheral, IPacketSender, IPacketReceiver +{ + private IPacketNetwork m_network; + private final Set m_computers = new HashSet<>( 1 ); + private final ModemState m_state; + + protected ModemPeripheral( ModemState state ) + { + m_state = state; + } + + public ModemState getModemState() + { + return m_state; + } + + private synchronized void setNetwork( IPacketNetwork network ) + { + if( m_network == network ) return; + + // Leave old network + if( m_network != null ) m_network.removeReceiver( this ); + + // Set new network + m_network = network; + + // Join new network + if( m_network != null ) m_network.addReceiver( this ); + } + + public void destroy() + { + setNetwork( null ); + } + + @Override + public void receiveSameDimension( @Nonnull Packet packet, double distance ) + { + if( packet.getSender() == this || !m_state.isOpen( packet.getChannel() ) ) return; + + synchronized( m_computers ) + { + for( IComputerAccess computer : m_computers ) + { + computer.queueEvent( "modem_message", + computer.getAttachmentName(), packet.getChannel(), packet.getReplyChannel(), packet.getPayload(), distance ); + } + } + } + + @Override + public void receiveDifferentDimension( @Nonnull Packet packet ) + { + if( packet.getSender() == this || !m_state.isOpen( packet.getChannel() ) ) return; + + synchronized( m_computers ) + { + for( IComputerAccess computer : m_computers ) + { + computer.queueEvent( "modem_message", + computer.getAttachmentName(), packet.getChannel(), packet.getReplyChannel(), packet.getPayload() ); + } + } + } + + protected abstract IPacketNetwork getNetwork(); + + @Nonnull + @Override + public String getType() + { + return "modem"; + } + + private static int parseChannel( int channel ) throws LuaException + { + if( channel < 0 || channel > 65535 ) throw new LuaException( "Expected number in range 0-65535" ); + return channel; + } + + /** + * Open a channel on a modem. A channel must be open in order to receive messages. Modems can have up to 128 + * channels open at one time. + * + * @param channel The channel to open. This must be a number between 0 and 65535. + * @throws LuaException If the channel is out of range. + * @throws LuaException If there are too many open channels. + */ + @LuaFunction + public final void open( int channel ) throws LuaException + { + m_state.open( parseChannel( channel ) ); + } + + /** + * Check if a channel is open. + * + * @param channel The channel to check. + * @return Whether the channel is open. + * @throws LuaException If the channel is out of range. + */ + @LuaFunction + public final boolean isOpen( int channel ) throws LuaException + { + return m_state.isOpen( parseChannel( channel ) ); + } + + /** + * Close an open channel, meaning it will no longer receive messages. + * + * @param channel The channel to close. + * @throws LuaException If the channel is out of range. + */ + @LuaFunction + public final void close( int channel ) throws LuaException + { + m_state.close( parseChannel( channel ) ); + } + + /** + * Close all open channels. + */ + @LuaFunction + public final void closeAll() + { + m_state.closeAll(); + } + + /** + * Sends a modem message on a certain channel. Modems listening on the channel will queue a {@code modem_message} + * event on adjacent computers. + * + *
Note: The channel does not need be open to send a message.
+ * + * @param channel The channel to send messages on. + * @param replyChannel The channel that responses to this message should be sent on. + * @param payload The object to send. This can be a string, number, or table. + * @throws LuaException If the channel is out of range. + */ + @LuaFunction + public final void transmit( int channel, int replyChannel, Object payload ) throws LuaException + { + parseChannel( channel ); + parseChannel( replyChannel ); + + World world = getWorld(); + Vec3d position = getPosition(); + IPacketNetwork network = m_network; + + if( world == null || position == null || network == null ) return; + + Packet packet = new Packet( channel, replyChannel, payload, this ); + if( isInterdimensional() ) + { + network.transmitInterdimensional( packet ); + } + else + { + network.transmitSameDimension( packet, getRange() ); + } + } + + /** + * Determine if this is a wired or wireless modem. + * + * Some methods (namely those dealing with wired networks and remote peripherals) are only available on wired + * modems. + * + * @return {@code true} if this is a wireless modem. + */ + @LuaFunction + public final boolean isWireless() + { + IPacketNetwork network = m_network; + return network != null && network.isWireless(); + } + + @Override + public synchronized void attach( @Nonnull IComputerAccess computer ) + { + synchronized( m_computers ) + { + m_computers.add( computer ); + } + + setNetwork( getNetwork() ); + } + + @Override + public synchronized void detach( @Nonnull IComputerAccess computer ) + { + boolean empty; + synchronized( m_computers ) + { + m_computers.remove( computer ); + empty = m_computers.isEmpty(); + } + + if( empty ) setNetwork( null ); + } + + @Nonnull + @Override + public String getSenderID() + { + synchronized( m_computers ) + { + if( m_computers.size() != 1 ) + { + return "unknown"; + } + else + { + IComputerAccess computer = m_computers.iterator().next(); + return computer.getID() + "_" + computer.getAttachmentName(); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java new file mode 100644 index 000000000..c2875be0c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java @@ -0,0 +1,30 @@ +/* + * 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.modem; + +import javax.annotation.Nonnull; +import net.minecraft.util.math.Direction; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; + +public final class ModemShapes +{ + private static final VoxelShape[] BOXES = new VoxelShape[] { + VoxelShapes.cuboid( 0.125, 0.0, 0.125, 0.875, 0.1875, 0.875 ), // Down + VoxelShapes.cuboid( 0.125, 0.8125, 0.125, 0.875, 1.0, 0.875 ), // Up + VoxelShapes.cuboid( 0.125, 0.125, 0.0, 0.875, 0.875, 0.1875 ), // North + VoxelShapes.cuboid( 0.125, 0.125, 0.8125, 0.875, 0.875, 1.0 ), // South + VoxelShapes.cuboid( 0.0, 0.125, 0.125, 0.1875, 0.875, 0.875 ), // West + VoxelShapes.cuboid( 0.8125, 0.125, 0.125, 1.0, 0.875, 0.875 ), // East + }; + + @Nonnull + public static VoxelShape getBounds( Direction facing ) + { + int direction = facing.ordinal(); + return direction < BOXES.length ? BOXES[direction] : VoxelShapes.fullCube(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemState.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemState.java new file mode 100644 index 000000000..d206c1d4b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemState.java @@ -0,0 +1,87 @@ +/* + * 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.modem; + +import dan200.computercraft.api.lua.LuaException; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class ModemState +{ + private final Runnable onChanged; + private final AtomicBoolean changed = new AtomicBoolean( true ); + + private boolean open = false; + private final IntSet channels = new IntOpenHashSet(); + + public ModemState() + { + onChanged = null; + } + + public ModemState( Runnable onChanged ) + { + this.onChanged = onChanged; + } + + private void setOpen( boolean open ) + { + if( this.open == open ) return; + this.open = open; + if( !changed.getAndSet( true ) && onChanged != null ) onChanged.run(); + } + + public boolean pollChanged() + { + return changed.getAndSet( false ); + } + + public boolean isOpen() + { + return open; + } + + public boolean isOpen( int channel ) + { + synchronized( channels ) + { + return channels.contains( channel ); + } + } + + public void open( int channel ) throws LuaException + { + synchronized( channels ) + { + if( !channels.contains( channel ) ) + { + if( channels.size() >= 128 ) throw new LuaException( "Too many open channels" ); + channels.add( channel ); + setOpen( true ); + } + } + } + + public void close( int channel ) + { + synchronized( channels ) + { + channels.remove( channel ); + if( channels.isEmpty() ) setOpen( false ); + } + } + + public void closeAll() + { + synchronized( channels ) + { + channels.clear(); + setOpen( false ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java new file mode 100644 index 000000000..f9a2a10c1 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java @@ -0,0 +1,250 @@ +/* + * 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.modem.wired; + +import com.google.common.collect.ImmutableMap; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.common.BlockGeneric; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.ShapeContext; +import net.minecraft.block.Waterloggable; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.fluid.FluidState; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.BooleanProperty; +import net.minecraft.state.property.EnumProperty; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.BlockView; +import net.minecraft.world.RaycastContext; +import net.minecraft.world.World; +import net.minecraft.world.WorldAccess; +import net.minecraft.world.WorldView; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.EnumMap; + +import static dan200.computercraft.shared.util.WaterloggableHelpers.*; + +public class BlockCable extends BlockGeneric implements Waterloggable +{ + public static final EnumProperty MODEM = EnumProperty.of( "modem", CableModemVariant.class ); + public static final BooleanProperty CABLE = BooleanProperty.of( "cable" ); + + private static final BooleanProperty NORTH = BooleanProperty.of( "north" ); + private static final BooleanProperty SOUTH = BooleanProperty.of( "south" ); + private static final BooleanProperty EAST = BooleanProperty.of( "east" ); + private static final BooleanProperty WEST = BooleanProperty.of( "west" ); + private static final BooleanProperty UP = BooleanProperty.of( "up" ); + private static final BooleanProperty DOWN = BooleanProperty.of( "down" ); + + static final EnumMap CONNECTIONS = + new EnumMap<>( new ImmutableMap.Builder() + .put( Direction.DOWN, DOWN ).put( Direction.UP, UP ) + .put( Direction.NORTH, NORTH ).put( Direction.SOUTH, SOUTH ) + .put( Direction.WEST, WEST ).put( Direction.EAST, EAST ) + .build() ); + + public BlockCable( Settings settings ) + { + super( settings, Registry.ModTiles.CABLE ); + + setDefaultState( getStateManager().getDefaultState() + .with( MODEM, CableModemVariant.None ) + .with( CABLE, false ) + .with( NORTH, false ).with( SOUTH, false ) + .with( EAST, false ).with( WEST, false ) + .with( UP, false ).with( DOWN, false ) + .with( WATERLOGGED, false ) + ); + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( MODEM, CABLE, NORTH, SOUTH, EAST, WEST, UP, DOWN, WATERLOGGED ); + } + + public static boolean canConnectIn( BlockState state, Direction direction ) + { + return state.get( BlockCable.CABLE ) && state.get( BlockCable.MODEM ).getFacing() != direction; + } + + public static boolean doesConnectVisually( BlockState state, BlockView world, BlockPos pos, Direction direction ) + { + if( !state.get( CABLE ) ) return false; + if( state.get( MODEM ).getFacing() == direction ) return true; + return ComputerCraftAPI.getWiredElementAt( world, pos.offset( direction ), direction.getOpposite() ).isPresent(); + } + + @Nonnull + @Override + @Deprecated + public VoxelShape getOutlineShape( @Nonnull BlockState state, @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull ShapeContext context ) + { + return CableShapes.getShape( state ); + } + + @Override + public boolean removedByPlayer( BlockState state, World world, BlockPos pos, PlayerEntity player, boolean willHarvest, FluidState fluid ) + { + if( state.get( CABLE ) && state.get( MODEM ).getFacing() != null ) + { + BlockHitResult hit = world.raycast( new RaycastContext( + WorldUtil.getRayStart( player ), WorldUtil.getRayEnd( player ), + RaycastContext.ShapeType.COLLIDER, RaycastContext.FluidHandling.NONE, player + ) ); + if( hit.getType() == HitResult.Type.BLOCK ) + { + BlockEntity tile = world.getBlockEntity( pos ); + if( tile instanceof TileCable && tile.hasWorld() ) + { + TileCable cable = (TileCable) tile; + + ItemStack item; + BlockState newState; + + if( WorldUtil.isVecInside( CableShapes.getModemShape( state ), hit.getPos().subtract( pos.getX(), pos.getY(), pos.getZ() ) ) ) + { + newState = state.with( MODEM, CableModemVariant.None ); + item = new ItemStack( Registry.ModItems.WIRED_MODEM.get() ); + } + else + { + newState = state.with( CABLE, false ); + item = new ItemStack( Registry.ModItems.CABLE.get() ); + } + + world.setBlockState( pos, correctConnections( world, pos, newState ), 3 ); + + cable.modemChanged(); + cable.connectionsChanged(); + if( !world.isClient && !player.abilities.creativeMode ) + { + Block.dropStack( world, pos, item ); + } + + return false; + } + } + } + + return super.removedByPlayer( state, world, pos, player, willHarvest, fluid ); + } + + @Nonnull + @Override + public ItemStack getPickBlock( BlockState state, HitResult hit, BlockView world, BlockPos pos, PlayerEntity player ) + { + Direction modem = state.get( MODEM ).getFacing(); + boolean cable = state.get( CABLE ); + + // If we've only got one, just use that. + if( !cable ) return new ItemStack( Registry.ModItems.WIRED_MODEM.get() ); + if( modem == null ) return new ItemStack( Registry.ModItems.CABLE.get() ); + + // We've a modem and cable, so try to work out which one we're interacting with + return hit != null && WorldUtil.isVecInside( CableShapes.getModemShape( state ), hit.getPos().subtract( pos.getX(), pos.getY(), pos.getZ() ) ) + ? new ItemStack( Registry.ModItems.WIRED_MODEM.get() ) + : new ItemStack( Registry.ModItems.CABLE.get() ); + + } + + @Override + public void onPlaced( World world, @Nonnull BlockPos pos, @Nonnull BlockState state, LivingEntity placer, @Nonnull ItemStack stack ) + { + BlockEntity tile = world.getBlockEntity( pos ); + if( tile instanceof TileCable ) + { + TileCable cable = (TileCable) tile; + if( cable.hasCable() ) cable.connectionsChanged(); + } + + super.onPlaced( world, pos, state, placer, stack ); + } + + @Nonnull + @Override + @Deprecated + public FluidState getFluidState( @Nonnull BlockState state ) + { + return getWaterloggedFluidState( state ); + } + + @Nonnull + @Override + @Deprecated + public BlockState getStateForNeighborUpdate( @Nonnull BlockState state, @Nonnull Direction side, @Nonnull BlockState otherState, @Nonnull WorldAccess world, @Nonnull BlockPos pos, @Nonnull BlockPos otherPos ) + { + updateWaterloggedPostPlacement( state, world, pos ); + // Should never happen, but handle the case where we've no modem or cable. + if( !state.get( CABLE ) && state.get( MODEM ) == CableModemVariant.None ) + { + return getFluidState( state ).getBlockState(); + } + + return state.with( CONNECTIONS.get( side ), doesConnectVisually( state, world, pos, side ) ); + } + + @Override + @Deprecated + public boolean canPlaceAt( BlockState state, @Nonnull WorldView world, @Nonnull BlockPos pos ) + { + Direction facing = state.get( MODEM ).getFacing(); + if( facing == null ) return true; + + return sideCoversSmallSquare( world, pos.offset( facing ), facing.getOpposite() ); + } + + @Nullable + @Override + public BlockState getPlacementState( @Nonnull ItemPlacementContext context ) + { + BlockState state = getDefaultState() + .with( WATERLOGGED, getWaterloggedStateForPlacement( context ) ); + + if( context.getStack().getItem() instanceof ItemBlockCable.Cable ) + { + World world = context.getWorld(); + BlockPos pos = context.getBlockPos(); + return correctConnections( world, pos, state.with( CABLE, true ) ); + } + else + { + return state.with( MODEM, CableModemVariant.from( context.getSide().getOpposite() ) ); + } + } + + public static BlockState correctConnections( World world, BlockPos pos, BlockState state ) + { + if( state.get( CABLE ) ) + { + return state + .with( NORTH, doesConnectVisually( state, world, pos, Direction.NORTH ) ) + .with( SOUTH, doesConnectVisually( state, world, pos, Direction.SOUTH ) ) + .with( EAST, doesConnectVisually( state, world, pos, Direction.EAST ) ) + .with( WEST, doesConnectVisually( state, world, pos, Direction.WEST ) ) + .with( UP, doesConnectVisually( state, world, pos, Direction.UP ) ) + .with( DOWN, doesConnectVisually( state, world, pos, Direction.DOWN ) ); + } + else + { + return state + .with( NORTH, false ).with( SOUTH, false ).with( EAST, false ) + .with( WEST, false ).with( UP, false ).with( DOWN, false ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockWiredModemFull.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockWiredModemFull.java new file mode 100644 index 000000000..14448a42f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockWiredModemFull.java @@ -0,0 +1,34 @@ +/* + * 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.modem.wired; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.common.BlockGeneric; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.BooleanProperty; + +public class BlockWiredModemFull extends BlockGeneric +{ + public static final BooleanProperty MODEM_ON = BooleanProperty.of( "modem" ); + public static final BooleanProperty PERIPHERAL_ON = BooleanProperty.of( "peripheral" ); + + public BlockWiredModemFull( Settings settings ) + { + super( settings, Registry.ModTiles.WIRED_MODEM_FULL ); + setDefaultState( getStateManager().getDefaultState() + .with( MODEM_ON, false ) + .with( PERIPHERAL_ON, false ) + ); + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( MODEM_ON, PERIPHERAL_ON ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java new file mode 100644 index 000000000..da3ca5673 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java @@ -0,0 +1,83 @@ +/* + * 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.modem.wired; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import net.minecraft.util.StringIdentifiable; +import net.minecraft.util.math.Direction; + +public enum CableModemVariant implements StringIdentifiable +{ + None( "none", null ), + DownOff( "down_off", Direction.DOWN ), + UpOff( "up_off", Direction.UP ), + NorthOff( "north_off", Direction.NORTH ), + SouthOff( "south_off", Direction.SOUTH ), + WestOff( "west_off", Direction.WEST ), + EastOff( "east_off", Direction.EAST ), + DownOn( "down_on", Direction.DOWN ), + UpOn( "up_on", Direction.UP ), + NorthOn( "north_on", Direction.NORTH ), + SouthOn( "south_on", Direction.SOUTH ), + WestOn( "west_on", Direction.WEST ), + EastOn( "east_on", Direction.EAST ), + DownOffPeripheral( "down_off_peripheral", Direction.DOWN ), + UpOffPeripheral( "up_off_peripheral", Direction.UP ), + NorthOffPeripheral( "north_off_peripheral", Direction.NORTH ), + SouthOffPeripheral( "south_off_peripheral", Direction.SOUTH ), + WestOffPeripheral( "west_off_peripheral", Direction.WEST ), + EastOffPeripheral( "east_off_peripheral", Direction.EAST ), + DownOnPeripheral( "down_on_peripheral", Direction.DOWN ), + UpOnPeripheral( "up_on_peripheral", Direction.UP ), + NorthOnPeripheral( "north_on_peripheral", Direction.NORTH ), + SouthOnPeripheral( "south_on_peripheral", Direction.SOUTH ), + WestOnPeripheral( "west_on_peripheral", Direction.WEST ), + EastOnPeripheral( "east_on_peripheral", Direction.EAST ); + + private static final CableModemVariant[] VALUES = values(); + + private final String name; + private final Direction facing; + + CableModemVariant( String name, Direction facing ) + { + this.name = name; + this.facing = facing; + } + + @Nonnull + public static CableModemVariant from( Direction facing ) + { + return facing == null ? None : VALUES[1 + facing.getId()]; + } + + @Nonnull + public static CableModemVariant from( Direction facing, boolean modem, boolean peripheral ) + { + int state = (modem ? 2 : 0) + (peripheral ? 1 : 0); + return facing == null ? None : VALUES[1 + 6 * state + facing.getId()]; + } + + @Nonnull + @Override + public String asString() + { + return name; + } + + @Nullable + public Direction getFacing() + { + return facing; + } + + @Override + public String toString() + { + return name; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableShapes.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableShapes.java new file mode 100644 index 000000000..b01c428ac --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableShapes.java @@ -0,0 +1,98 @@ +/* + * 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.modem.wired; + +import com.google.common.collect.ImmutableMap; +import dan200.computercraft.shared.peripheral.modem.ModemShapes; +import dan200.computercraft.shared.util.DirectionUtil; +import net.minecraft.block.BlockState; +import net.minecraft.util.math.Direction; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; +import java.util.EnumMap; + +import static dan200.computercraft.shared.peripheral.modem.wired.BlockCable.*; + +public final class CableShapes +{ + private static final double MIN = 0.375; + private static final double MAX = 1 - MIN; + + private static final VoxelShape SHAPE_CABLE_CORE = VoxelShapes.cuboid( MIN, MIN, MIN, MAX, MAX, MAX ); + private static final EnumMap SHAPE_CABLE_ARM = + new EnumMap<>( new ImmutableMap.Builder() + .put( Direction.DOWN, VoxelShapes.cuboid( MIN, 0, MIN, MAX, MIN, MAX ) ) + .put( Direction.UP, VoxelShapes.cuboid( MIN, MAX, MIN, MAX, 1, MAX ) ) + .put( Direction.NORTH, VoxelShapes.cuboid( MIN, MIN, 0, MAX, MAX, MIN ) ) + .put( Direction.SOUTH, VoxelShapes.cuboid( MIN, MIN, MAX, MAX, MAX, 1 ) ) + .put( Direction.WEST, VoxelShapes.cuboid( 0, MIN, MIN, MIN, MAX, MAX ) ) + .put( Direction.EAST, VoxelShapes.cuboid( MAX, MIN, MIN, 1, MAX, MAX ) ) + .build() + ); + + private static final VoxelShape[] SHAPES = new VoxelShape[(1 << 6) * 7]; + private static final VoxelShape[] CABLE_SHAPES = new VoxelShape[1 << 6]; + + private CableShapes() + { + } + + private static int getCableIndex( BlockState state ) + { + int index = 0; + for( Direction facing : DirectionUtil.FACINGS ) + { + if( state.get( CONNECTIONS.get( facing ) ) ) index |= 1 << facing.ordinal(); + } + + return index; + } + + private static VoxelShape getCableShape( int index ) + { + VoxelShape shape = CABLE_SHAPES[index]; + if( shape != null ) return shape; + + shape = SHAPE_CABLE_CORE; + for( Direction facing : DirectionUtil.FACINGS ) + { + if( (index & (1 << facing.ordinal())) != 0 ) + { + shape = VoxelShapes.union( shape, SHAPE_CABLE_ARM.get( facing ) ); + } + } + + return CABLE_SHAPES[index] = shape; + } + + public static VoxelShape getCableShape( BlockState state ) + { + if( !state.get( CABLE ) ) return VoxelShapes.empty(); + return getCableShape( getCableIndex( state ) ); + } + + public static VoxelShape getModemShape( BlockState state ) + { + Direction facing = state.get( MODEM ).getFacing(); + return facing == null ? VoxelShapes.empty() : ModemShapes.getBounds( facing ); + } + + public static VoxelShape getShape( BlockState state ) + { + Direction facing = state.get( MODEM ).getFacing(); + if( !state.get( CABLE ) ) return getModemShape( state ); + + int cableIndex = getCableIndex( state ); + int index = cableIndex + ((facing == null ? 0 : facing.ordinal() + 1) << 6); + + VoxelShape shape = SHAPES[index]; + if( shape != null ) return shape; + + shape = getCableShape( cableIndex ); + if( facing != null ) shape = VoxelShapes.union( shape, ModemShapes.getBounds( facing ) ); + return SHAPES[index] = shape; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/ItemBlockCable.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/ItemBlockCable.java new file mode 100644 index 000000000..c488178c0 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/ItemBlockCable.java @@ -0,0 +1,155 @@ +/* + * 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.modem.wired; + +import dan200.computercraft.shared.Registry; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.BlockItem; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.sound.BlockSoundGroup; +import net.minecraft.sound.SoundCategory; +import net.minecraft.util.*; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.registries.ForgeRegistries; + +import javax.annotation.Nonnull; + +import static dan200.computercraft.shared.peripheral.modem.wired.BlockCable.*; + +public abstract class ItemBlockCable extends BlockItem +{ + private String translationKey; + + public ItemBlockCable( BlockCable block, Settings settings ) + { + super( block, settings ); + } + + boolean placeAt( World world, BlockPos pos, BlockState state, PlayerEntity player ) + { + // TODO: Check entity collision. + if( !state.canPlaceAt( world, pos ) ) return false; + + world.setBlockState( pos, state, 3 ); + BlockSoundGroup soundType = state.getBlock().getSoundType( state, world, pos, player ); + world.playSound( null, pos, soundType.getPlaceSound(), SoundCategory.BLOCKS, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F ); + + BlockEntity tile = world.getBlockEntity( pos ); + if( tile instanceof TileCable ) + { + TileCable cable = (TileCable) tile; + cable.modemChanged(); + cable.connectionsChanged(); + } + + return true; + } + + boolean placeAtCorrected( World world, BlockPos pos, BlockState state ) + { + return placeAt( world, pos, correctConnections( world, pos, state ), null ); + } + + @Override + public void appendStacks( @Nonnull ItemGroup group, @Nonnull DefaultedList list ) + { + if( isIn( group ) ) list.add( new ItemStack( this ) ); + } + + @Nonnull + @Override + public String getTranslationKey() + { + if( translationKey == null ) + { + translationKey = Util.createTranslationKey( "block", ForgeRegistries.ITEMS.getKey( this ) ); + } + return translationKey; + } + + public static class WiredModem extends ItemBlockCable + { + public WiredModem( BlockCable block, Settings settings ) + { + super( block, settings ); + } + + @Nonnull + @Override + public ActionResult place( ItemPlacementContext context ) + { + ItemStack stack = context.getStack(); + if( stack.isEmpty() ) return ActionResult.FAIL; + + World world = context.getWorld(); + BlockPos pos = context.getBlockPos(); + BlockState existingState = world.getBlockState( pos ); + + // Try to add a modem to a cable + if( existingState.getBlock() == Registry.ModBlocks.CABLE.get() && existingState.get( MODEM ) == CableModemVariant.None ) + { + Direction side = context.getSide().getOpposite(); + BlockState newState = existingState + .with( MODEM, CableModemVariant.from( side ) ) + .with( CONNECTIONS.get( side ), existingState.get( CABLE ) ); + if( placeAt( world, pos, newState, context.getPlayer() ) ) + { + stack.decrement( 1 ); + return ActionResult.SUCCESS; + } + } + + return super.place( context ); + } + } + + public static class Cable extends ItemBlockCable + { + public Cable( BlockCable block, Settings settings ) + { + super( block, settings ); + } + + @Nonnull + @Override + public ActionResult place( ItemPlacementContext context ) + { + ItemStack stack = context.getStack(); + if( stack.isEmpty() ) return ActionResult.FAIL; + + World world = context.getWorld(); + BlockPos pos = context.getBlockPos(); + + // Try to add a cable to a modem inside the block we're clicking on. + BlockPos insidePos = pos.offset( context.getSide().getOpposite() ); + BlockState insideState = world.getBlockState( insidePos ); + if( insideState.getBlock() == Registry.ModBlocks.CABLE.get() && !insideState.get( BlockCable.CABLE ) + && placeAtCorrected( world, insidePos, insideState.with( BlockCable.CABLE, true ) ) ) + { + stack.decrement( 1 ); + return ActionResult.SUCCESS; + } + + // Try to add a cable to a modem adjacent to this block + BlockState existingState = world.getBlockState( pos ); + if( existingState.getBlock() == Registry.ModBlocks.CABLE.get() && !existingState.get( BlockCable.CABLE ) + && placeAtCorrected( world, pos, existingState.with( BlockCable.CABLE, true ) ) ) + { + stack.decrement( 1 ); + return ActionResult.SUCCESS; + } + + return super.place( context ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java new file mode 100644 index 000000000..d1c46fafa --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java @@ -0,0 +1,461 @@ +/* + * 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.modem.wired; + +import com.google.common.base.Objects; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.command.CommandCopy; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.peripheral.modem.ModemState; +import dan200.computercraft.shared.util.CapabilityUtil; +import dan200.computercraft.shared.util.DirectionUtil; +import dan200.computercraft.shared.util.TickScheduler; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.common.util.NonNullConsumer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.Map; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; +import static dan200.computercraft.shared.Capabilities.CAPABILITY_WIRED_ELEMENT; + +public class TileCable extends TileGeneric +{ + private static final String NBT_PERIPHERAL_ENABLED = "PeirpheralAccess"; + + private class CableElement extends WiredModemElement + { + @Nonnull + @Override + public World getWorld() + { + return TileCable.this.getWorld(); + } + + @Nonnull + @Override + public Vec3d getPosition() + { + BlockPos pos = getPos(); + return new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ); + } + + @Override + protected void attachPeripheral( String name, IPeripheral peripheral ) + { + m_modem.attachPeripheral( name, peripheral ); + } + + @Override + protected void detachPeripheral( String name ) + { + m_modem.detachPeripheral( name ); + } + } + + private boolean m_peripheralAccessAllowed; + private final WiredModemLocalPeripheral m_peripheral = new WiredModemLocalPeripheral( this::refreshPeripheral ); + + private boolean m_destroyed = false; + + private Direction modemDirection = Direction.NORTH; + private boolean hasModemDirection = false; + private boolean m_connectionsFormed = false; + + private final WiredModemElement m_cable = new CableElement(); + private LazyOptional elementCap; + private final IWiredNode m_node = m_cable.getNode(); + private final WiredModemPeripheral m_modem = new WiredModemPeripheral( + new ModemState( () -> TickScheduler.schedule( this ) ), + m_cable + ) + { + @Nonnull + @Override + protected WiredModemLocalPeripheral getLocalPeripheral() + { + return m_peripheral; + } + + @Nonnull + @Override + public Vec3d getPosition() + { + BlockPos pos = getPos().offset( modemDirection ); + return new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ); + } + + @Nonnull + @Override + public Object getTarget() + { + return TileCable.this; + } + }; + private LazyOptional modemCap; + + private final NonNullConsumer> connectedNodeChanged = x -> connectionsChanged(); + + public TileCable( BlockEntityType type ) + { + super( type ); + } + + private void onRemove() + { + if( world == null || !world.isClient ) + { + m_node.remove(); + m_connectionsFormed = false; + } + } + + @Override + public void destroy() + { + if( !m_destroyed ) + { + m_destroyed = true; + m_modem.destroy(); + onRemove(); + } + } + + @Override + public void onChunkUnloaded() + { + super.onChunkUnloaded(); + onRemove(); + } + + @Override + public void markRemoved() + { + super.markRemoved(); + onRemove(); + } + + @Override + protected void invalidateCaps() + { + super.invalidateCaps(); + elementCap = CapabilityUtil.invalidate( elementCap ); + modemCap = CapabilityUtil.invalidate( modemCap ); + } + + @Override + public void onLoad() + { + super.onLoad(); + TickScheduler.schedule( this ); + } + + @Override + public void resetBlock() + { + super.resetBlock(); + hasModemDirection = false; + if( !world.isClient ) world.getBlockTickScheduler().schedule( pos, getCachedState().getBlock(), 0 ); + } + + private void refreshDirection() + { + if( hasModemDirection ) return; + + hasModemDirection = true; + modemDirection = getCachedState().get( BlockCable.MODEM ).getFacing(); + } + + @Nullable + private Direction getMaybeDirection() + { + refreshDirection(); + return modemDirection; + } + + @Nonnull + private Direction getDirection() + { + refreshDirection(); + return modemDirection == null ? Direction.NORTH : modemDirection; + } + + @Override + public void onNeighbourChange( @Nonnull BlockPos neighbour ) + { + Direction dir = getDirection(); + if( neighbour.equals( getPos().offset( dir ) ) && hasModem() && !getCachedState().canPlaceAt( getWorld(), getPos() ) ) + { + if( hasCable() ) + { + // Drop the modem and convert to cable + Block.dropStack( getWorld(), getPos(), new ItemStack( Registry.ModItems.WIRED_MODEM.get() ) ); + getWorld().setBlockState( getPos(), getCachedState().with( BlockCable.MODEM, CableModemVariant.None ) ); + modemChanged(); + connectionsChanged(); + } + else + { + // Drop everything and remove block + Block.dropStack( getWorld(), getPos(), new ItemStack( Registry.ModItems.WIRED_MODEM.get() ) ); + getWorld().removeBlock( getPos(), false ); + // This'll call #destroy(), so we don't need to reset the network here. + } + + return; + } + + onNeighbourTileEntityChange( neighbour ); + } + + @Override + public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour ) + { + super.onNeighbourTileEntityChange( neighbour ); + if( !world.isClient && m_peripheralAccessAllowed ) + { + Direction facing = getDirection(); + if( getPos().offset( facing ).equals( neighbour ) ) refreshPeripheral(); + } + } + + private void refreshPeripheral() + { + if( world != null && !isRemoved() && m_peripheral.attach( world, getPos(), getDirection() ) ) + { + updateConnectedPeripherals(); + } + } + + @Nonnull + @Override + public ActionResult onActivate( PlayerEntity player, Hand hand, BlockHitResult hit ) + { + if( player.isInSneakingPose() ) return ActionResult.PASS; + if( !canAttachPeripheral() ) return ActionResult.FAIL; + + if( getWorld().isClient ) return ActionResult.SUCCESS; + + String oldName = m_peripheral.getConnectedName(); + togglePeripheralAccess(); + String newName = m_peripheral.getConnectedName(); + if( !Objects.equal( newName, oldName ) ) + { + if( oldName != null ) + { + player.sendMessage( new TranslatableText( "chat.computercraft.wired_modem.peripheral_disconnected", + CommandCopy.createCopyText( oldName ) ), false ); + } + if( newName != null ) + { + player.sendMessage( new TranslatableText( "chat.computercraft.wired_modem.peripheral_connected", + CommandCopy.createCopyText( newName ) ), false ); + } + } + + return ActionResult.SUCCESS; + } + + @Override + public void fromTag( @Nonnull BlockState state, @Nonnull CompoundTag nbt ) + { + super.fromTag( state, nbt ); + m_peripheralAccessAllowed = nbt.getBoolean( NBT_PERIPHERAL_ENABLED ); + m_peripheral.read( nbt, "" ); + } + + @Nonnull + @Override + public CompoundTag toTag( CompoundTag nbt ) + { + nbt.putBoolean( NBT_PERIPHERAL_ENABLED, m_peripheralAccessAllowed ); + m_peripheral.write( nbt, "" ); + return super.toTag( nbt ); + } + + private void updateBlockState() + { + BlockState state = getCachedState(); + CableModemVariant oldVariant = state.get( BlockCable.MODEM ); + CableModemVariant newVariant = CableModemVariant + .from( oldVariant.getFacing(), m_modem.getModemState().isOpen(), m_peripheralAccessAllowed ); + + if( oldVariant != newVariant ) + { + world.setBlockState( getPos(), state.with( BlockCable.MODEM, newVariant ) ); + } + } + + @Override + public void blockTick() + { + if( getWorld().isClient ) return; + + Direction oldDirection = modemDirection; + refreshDirection(); + if( modemDirection != oldDirection ) + { + // We invalidate both the modem and element if the modem's direction is different. + modemCap = CapabilityUtil.invalidate( modemCap ); + elementCap = CapabilityUtil.invalidate( elementCap ); + } + + if( m_modem.getModemState().pollChanged() ) updateBlockState(); + + if( !m_connectionsFormed ) + { + m_connectionsFormed = true; + + connectionsChanged(); + if( m_peripheralAccessAllowed ) + { + m_peripheral.attach( world, pos, modemDirection ); + updateConnectedPeripherals(); + } + } + } + + void connectionsChanged() + { + if( getWorld().isClient ) return; + + BlockState state = getCachedState(); + World world = getWorld(); + BlockPos current = getPos(); + for( Direction facing : DirectionUtil.FACINGS ) + { + BlockPos offset = current.offset( facing ); + if( !world.isAreaLoaded( offset, 0 ) ) continue; + + LazyOptional element = ComputerCraftAPI.getWiredElementAt( world, offset, facing.getOpposite() ); + if( !element.isPresent() ) continue; + + element.addListener( connectedNodeChanged ); + IWiredNode node = element.orElseThrow( NullPointerException::new ).getNode(); + if( BlockCable.canConnectIn( state, facing ) ) + { + // If we can connect to it then do so + m_node.connectTo( node ); + } + else if( m_node.getNetwork() == node.getNetwork() ) + { + // Otherwise if we're on the same network then attempt to void it. + m_node.disconnectFrom( node ); + } + } + } + + void modemChanged() + { + // Tell anyone who cares that the connection state has changed + elementCap = CapabilityUtil.invalidate( elementCap ); + + if( getWorld().isClient ) return; + + // If we can no longer attach peripherals, then detach any + // which may have existed + if( !canAttachPeripheral() && m_peripheralAccessAllowed ) + { + m_peripheralAccessAllowed = false; + m_peripheral.detach(); + m_node.updatePeripherals( Collections.emptyMap() ); + markDirty(); + updateBlockState(); + } + } + + private void togglePeripheralAccess() + { + if( !m_peripheralAccessAllowed ) + { + m_peripheral.attach( world, getPos(), getDirection() ); + if( !m_peripheral.hasPeripheral() ) return; + + m_peripheralAccessAllowed = true; + m_node.updatePeripherals( m_peripheral.toMap() ); + } + else + { + m_peripheral.detach(); + + m_peripheralAccessAllowed = false; + m_node.updatePeripherals( Collections.emptyMap() ); + } + + updateBlockState(); + } + + private void updateConnectedPeripherals() + { + Map peripherals = m_peripheral.toMap(); + if( peripherals.isEmpty() ) + { + // If there are no peripherals then disable access and update the display state. + m_peripheralAccessAllowed = false; + updateBlockState(); + } + + m_node.updatePeripherals( peripherals ); + } + + @Nonnull + @Override + public LazyOptional getCapability( @Nonnull Capability capability, @Nullable Direction side ) + { + if( capability == CAPABILITY_WIRED_ELEMENT ) + { + if( m_destroyed || !BlockCable.canConnectIn( getCachedState(), side ) ) return LazyOptional.empty(); + if( elementCap == null ) elementCap = LazyOptional.of( () -> m_cable ); + return elementCap.cast(); + } + + if( capability == CAPABILITY_PERIPHERAL ) + { + refreshDirection(); + if( side != null && getMaybeDirection() != side ) return LazyOptional.empty(); + if( modemCap == null ) modemCap = LazyOptional.of( () -> m_modem ); + return modemCap.cast(); + } + + return super.getCapability( capability, side ); + } + + boolean hasCable() + { + return getCachedState().get( BlockCable.CABLE ); + } + + public boolean hasModem() + { + return getCachedState().get( BlockCable.MODEM ) != CableModemVariant.None; + } + + private boolean canAttachPeripheral() + { + return hasCable() && hasModem(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java new file mode 100644 index 000000000..8fbef71f4 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java @@ -0,0 +1,416 @@ +/* + * 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.modem.wired; + +import com.google.common.base.Objects; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.command.CommandCopy; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.peripheral.modem.ModemState; +import dan200.computercraft.shared.util.CapabilityUtil; +import dan200.computercraft.shared.util.DirectionUtil; +import dan200.computercraft.shared.util.SidedCaps; +import dan200.computercraft.shared.util.TickScheduler; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.text.LiteralText; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.common.util.NonNullConsumer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; +import static dan200.computercraft.shared.Capabilities.CAPABILITY_WIRED_ELEMENT; +import static dan200.computercraft.shared.peripheral.modem.wired.BlockWiredModemFull.MODEM_ON; +import static dan200.computercraft.shared.peripheral.modem.wired.BlockWiredModemFull.PERIPHERAL_ON; + +public class TileWiredModemFull extends TileGeneric +{ + private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess"; + + private static final class FullElement extends WiredModemElement + { + private final TileWiredModemFull m_entity; + + private FullElement( TileWiredModemFull entity ) + { + m_entity = entity; + } + + @Override + protected void attachPeripheral( String name, IPeripheral peripheral ) + { + for( int i = 0; i < 6; i++ ) + { + WiredModemPeripheral modem = m_entity.modems[i]; + if( modem != null ) modem.attachPeripheral( name, peripheral ); + } + } + + @Override + protected void detachPeripheral( String name ) + { + for( int i = 0; i < 6; i++ ) + { + WiredModemPeripheral modem = m_entity.modems[i]; + if( modem != null ) modem.detachPeripheral( name ); + } + } + + @Nonnull + @Override + public World getWorld() + { + return m_entity.getWorld(); + } + + @Nonnull + @Override + public Vec3d getPosition() + { + BlockPos pos = m_entity.getPos(); + return new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ); + } + } + + private final WiredModemPeripheral[] modems = new WiredModemPeripheral[6]; + private final SidedCaps modemCaps = SidedCaps.ofNonNull( this::getPeripheral ); + + private boolean m_peripheralAccessAllowed = false; + private final WiredModemLocalPeripheral[] m_peripherals = new WiredModemLocalPeripheral[6]; + + private boolean m_destroyed = false; + private boolean m_connectionsFormed = false; + + private final ModemState m_modemState = new ModemState( () -> TickScheduler.schedule( this ) ); + private final WiredModemElement m_element = new FullElement( this ); + private LazyOptional elementCap; + private final IWiredNode m_node = m_element.getNode(); + + private final NonNullConsumer> connectedNodeChanged = x -> connectionsChanged(); + + public TileWiredModemFull( BlockEntityType type ) + { + super( type ); + for( int i = 0; i < m_peripherals.length; i++ ) + { + Direction facing = Direction.byId( i ); + m_peripherals[i] = new WiredModemLocalPeripheral( () -> refreshPeripheral( facing ) ); + } + } + + private void doRemove() + { + if( world == null || !world.isClient ) + { + m_node.remove(); + m_connectionsFormed = false; + } + } + + @Override + public void destroy() + { + if( !m_destroyed ) + { + m_destroyed = true; + doRemove(); + } + super.destroy(); + } + + @Override + public void onChunkUnloaded() + { + super.onChunkUnloaded(); + doRemove(); + } + + @Override + protected void invalidateCaps() + { + super.invalidateCaps(); + elementCap = CapabilityUtil.invalidate( elementCap ); + modemCaps.invalidate(); + } + + @Override + public void markRemoved() + { + super.markRemoved(); + doRemove(); + } + + @Override + public void onNeighbourChange( @Nonnull BlockPos neighbour ) + { + onNeighbourTileEntityChange( neighbour ); + } + + @Override + public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour ) + { + if( !world.isClient && m_peripheralAccessAllowed ) + { + for( Direction facing : DirectionUtil.FACINGS ) + { + if( getPos().offset( facing ).equals( neighbour ) ) refreshPeripheral( facing ); + } + } + } + + private void refreshPeripheral( @Nonnull Direction facing ) + { + WiredModemLocalPeripheral peripheral = m_peripherals[facing.ordinal()]; + if( world != null && !isRemoved() && peripheral.attach( world, getPos(), facing ) ) + { + updateConnectedPeripherals(); + } + } + + @Nonnull + @Override + public ActionResult onActivate( PlayerEntity player, Hand hand, BlockHitResult hit ) + { + if( getWorld().isClient ) return ActionResult.SUCCESS; + + // On server, we interacted if a peripheral was found + Set oldPeriphNames = getConnectedPeripheralNames(); + togglePeripheralAccess(); + Set periphNames = getConnectedPeripheralNames(); + + if( !Objects.equal( periphNames, oldPeriphNames ) ) + { + sendPeripheralChanges( player, "chat.computercraft.wired_modem.peripheral_disconnected", oldPeriphNames ); + sendPeripheralChanges( player, "chat.computercraft.wired_modem.peripheral_connected", periphNames ); + } + + return ActionResult.SUCCESS; + } + + private static void sendPeripheralChanges( PlayerEntity player, String kind, Collection peripherals ) + { + if( peripherals.isEmpty() ) return; + + List names = new ArrayList<>( peripherals ); + names.sort( Comparator.naturalOrder() ); + + LiteralText base = new LiteralText( "" ); + for( int i = 0; i < names.size(); i++ ) + { + if( i > 0 ) base.append( ", " ); + base.append( CommandCopy.createCopyText( names.get( i ) ) ); + } + + player.sendMessage( new TranslatableText( kind, base ), false ); + } + + @Override + public void fromTag( @Nonnull BlockState state, @Nonnull CompoundTag nbt ) + { + super.fromTag( state, nbt ); + m_peripheralAccessAllowed = nbt.getBoolean( NBT_PERIPHERAL_ENABLED ); + for( int i = 0; i < m_peripherals.length; i++ ) m_peripherals[i].read( nbt, Integer.toString( i ) ); + } + + @Nonnull + @Override + public CompoundTag toTag( CompoundTag nbt ) + { + nbt.putBoolean( NBT_PERIPHERAL_ENABLED, m_peripheralAccessAllowed ); + for( int i = 0; i < m_peripherals.length; i++ ) m_peripherals[i].write( nbt, Integer.toString( i ) ); + return super.toTag( nbt ); + } + + private void updateBlockState() + { + BlockState state = getCachedState(); + boolean modemOn = m_modemState.isOpen(), peripheralOn = m_peripheralAccessAllowed; + if( state.get( MODEM_ON ) == modemOn && state.get( PERIPHERAL_ON ) == peripheralOn ) return; + + getWorld().setBlockState( getPos(), state.with( MODEM_ON, modemOn ).with( PERIPHERAL_ON, peripheralOn ) ); + } + + @Override + public void onLoad() + { + super.onLoad(); + TickScheduler.schedule( this ); + } + + @Override + public void blockTick() + { + if( getWorld().isClient ) return; + + if( m_modemState.pollChanged() ) updateBlockState(); + + if( !m_connectionsFormed ) + { + m_connectionsFormed = true; + + connectionsChanged(); + if( m_peripheralAccessAllowed ) + { + for( Direction facing : DirectionUtil.FACINGS ) + { + m_peripherals[facing.ordinal()].attach( world, getPos(), facing ); + } + updateConnectedPeripherals(); + } + } + } + + private void connectionsChanged() + { + if( getWorld().isClient ) return; + + World world = getWorld(); + BlockPos current = getPos(); + for( Direction facing : DirectionUtil.FACINGS ) + { + BlockPos offset = current.offset( facing ); + if( !world.isAreaLoaded( offset, 0 ) ) continue; + + LazyOptional element = ComputerCraftAPI.getWiredElementAt( world, offset, facing.getOpposite() ); + if( !element.isPresent() ) continue; + + element.addListener( connectedNodeChanged ); + m_node.connectTo( element.orElseThrow( NullPointerException::new ).getNode() ); + } + } + + private void togglePeripheralAccess() + { + if( !m_peripheralAccessAllowed ) + { + boolean hasAny = false; + for( Direction facing : DirectionUtil.FACINGS ) + { + WiredModemLocalPeripheral peripheral = m_peripherals[facing.ordinal()]; + peripheral.attach( world, getPos(), facing ); + hasAny |= peripheral.hasPeripheral(); + } + + if( !hasAny ) return; + + m_peripheralAccessAllowed = true; + m_node.updatePeripherals( getConnectedPeripherals() ); + } + else + { + m_peripheralAccessAllowed = false; + + for( WiredModemLocalPeripheral peripheral : m_peripherals ) peripheral.detach(); + m_node.updatePeripherals( Collections.emptyMap() ); + } + + updateBlockState(); + } + + private Set getConnectedPeripheralNames() + { + if( !m_peripheralAccessAllowed ) return Collections.emptySet(); + + Set peripherals = new HashSet<>( 6 ); + for( WiredModemLocalPeripheral peripheral : m_peripherals ) + { + String name = peripheral.getConnectedName(); + if( name != null ) peripherals.add( name ); + } + return peripherals; + } + + private Map getConnectedPeripherals() + { + if( !m_peripheralAccessAllowed ) return Collections.emptyMap(); + + Map peripherals = new HashMap<>( 6 ); + for( WiredModemLocalPeripheral peripheral : m_peripherals ) peripheral.extendMap( peripherals ); + return peripherals; + } + + private void updateConnectedPeripherals() + { + Map peripherals = getConnectedPeripherals(); + if( peripherals.isEmpty() ) + { + // If there are no peripherals then disable access and update the display state. + m_peripheralAccessAllowed = false; + updateBlockState(); + } + + m_node.updatePeripherals( peripherals ); + } + + @Nonnull + @Override + public LazyOptional getCapability( @Nonnull Capability capability, @Nullable Direction side ) + { + if( capability == CAPABILITY_WIRED_ELEMENT ) + { + if( elementCap == null ) elementCap = LazyOptional.of( () -> m_element ); + return elementCap.cast(); + } + + if( capability == CAPABILITY_PERIPHERAL ) return modemCaps.get( side ).cast(); + + return super.getCapability( capability, side ); + } + + public IWiredElement getElement() + { + return m_element; + } + + private WiredModemPeripheral getPeripheral( @Nonnull Direction side ) + { + WiredModemPeripheral peripheral = modems[side.ordinal()]; + if( peripheral != null ) return peripheral; + + WiredModemLocalPeripheral localPeripheral = m_peripherals[side.ordinal()]; + return modems[side.ordinal()] = new WiredModemPeripheral( m_modemState, m_element ) + { + @Nonnull + @Override + protected WiredModemLocalPeripheral getLocalPeripheral() + { + return localPeripheral; + } + + @Nonnull + @Override + public Vec3d getPosition() + { + BlockPos pos = getPos().offset( side ); + return new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ); + } + + @Nonnull + @Override + public Object getTarget() + { + return TileWiredModemFull.this; + } + }; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemElement.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemElement.java new file mode 100644 index 000000000..510e5e342 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemElement.java @@ -0,0 +1,64 @@ +/* + * 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.modem.wired; + +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNetworkChange; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.wired.WiredNode; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; + +public abstract class WiredModemElement implements IWiredElement +{ + private final IWiredNode node = new WiredNode( this ); + private final Map remotePeripherals = new HashMap<>(); + + @Nonnull + @Override + public IWiredNode getNode() + { + return node; + } + + @Nonnull + @Override + public String getSenderID() + { + return "modem"; + } + + @Override + public void networkChanged( @Nonnull IWiredNetworkChange change ) + { + synchronized( remotePeripherals ) + { + remotePeripherals.keySet().removeAll( change.peripheralsRemoved().keySet() ); + for( String name : change.peripheralsRemoved().keySet() ) + { + detachPeripheral( name ); + } + + for( Map.Entry peripheral : change.peripheralsAdded().entrySet() ) + { + attachPeripheral( peripheral.getKey(), peripheral.getValue() ); + } + remotePeripherals.putAll( change.peripheralsAdded() ); + } + } + + public Map getRemotePeripherals() + { + return remotePeripherals; + } + + protected abstract void attachPeripheral( String name, IPeripheral peripheral ); + + protected abstract void detachPeripheral( String name ); +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java new file mode 100644 index 000000000..8cdc08cd7 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java @@ -0,0 +1,152 @@ +/* + * 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.modem.wired; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.Peripherals; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.util.IDAssigner; +import net.minecraft.block.Block; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.common.util.Constants; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.common.util.NonNullConsumer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.Map; + +/** + * Represents a local peripheral exposed on the wired network. + * + * This is responsible for getting the peripheral in world, tracking id and type and determining whether + * it has changed. + */ +public final class WiredModemLocalPeripheral +{ + private static final String NBT_PERIPHERAL_TYPE = "PeripheralType"; + private static final String NBT_PERIPHERAL_ID = "PeripheralId"; + + private int id; + private String type; + + private IPeripheral peripheral; + private final NonNullConsumer> invalidate; + + public WiredModemLocalPeripheral( @Nonnull Runnable invalidate ) + { + this.invalidate = x -> invalidate.run(); + } + + /** + * Attach a new peripheral from the world. + * + * @param world The world to search in + * @param origin The position to search from + * @param direction The direction so search in + * @return Whether the peripheral changed. + */ + public boolean attach( @Nonnull World world, @Nonnull BlockPos origin, @Nonnull Direction direction ) + { + IPeripheral oldPeripheral = peripheral; + IPeripheral peripheral = this.peripheral = getPeripheralFrom( world, origin, direction ); + + if( peripheral == null ) + { + return oldPeripheral != null; + } + else + { + String type = peripheral.getType(); + int id = this.id; + + if( id > 0 && this.type == null ) + { + // If we had an ID but no type, then just set the type. + this.type = type; + } + else if( id < 0 || !type.equals( this.type ) ) + { + this.type = type; + this.id = IDAssigner.getNextId( "peripheral." + type ); + } + + return oldPeripheral == null || !oldPeripheral.equals( peripheral ); + } + } + + /** + * Detach the current peripheral. + * + * @return Whether the peripheral changed + */ + public boolean detach() + { + if( peripheral == null ) return false; + peripheral = null; + return true; + } + + @Nullable + public String getConnectedName() + { + return peripheral != null ? type + "_" + id : null; + } + + @Nullable + public IPeripheral getPeripheral() + { + return peripheral; + } + + public boolean hasPeripheral() + { + return peripheral != null; + } + + public void extendMap( @Nonnull Map peripherals ) + { + if( peripheral != null ) peripherals.put( type + "_" + id, peripheral ); + } + + public Map toMap() + { + return peripheral == null + ? Collections.emptyMap() + : Collections.singletonMap( type + "_" + id, peripheral ); + } + + public void write( @Nonnull CompoundTag tag, @Nonnull String suffix ) + { + if( id >= 0 ) tag.putInt( NBT_PERIPHERAL_ID + suffix, id ); + if( type != null ) tag.putString( NBT_PERIPHERAL_TYPE + suffix, type ); + } + + public void read( @Nonnull CompoundTag tag, @Nonnull String suffix ) + { + id = tag.contains( NBT_PERIPHERAL_ID + suffix, Constants.NBT.TAG_ANY_NUMERIC ) + ? tag.getInt( NBT_PERIPHERAL_ID + suffix ) : -1; + + type = tag.contains( NBT_PERIPHERAL_TYPE + suffix, Constants.NBT.TAG_STRING ) + ? tag.getString( NBT_PERIPHERAL_TYPE + suffix ) : null; + } + + @Nullable + private IPeripheral getPeripheralFrom( World world, BlockPos pos, Direction direction ) + { + BlockPos offset = pos.offset( direction ); + + Block block = world.getBlockState( offset ).getBlock(); + if( block == Registry.ModBlocks.WIRED_MODEM_FULL.get() || block == Registry.ModBlocks.CABLE.get() ) return null; + + IPeripheral peripheral = Peripherals.getPeripheral( world, offset, direction.getOpposite(), invalidate ); + return peripheral instanceof WiredModemPeripheral ? null : peripheral; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java new file mode 100644 index 000000000..bdd1187ca --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java @@ -0,0 +1,431 @@ +/* + * 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.modem.wired; + +import com.google.common.collect.ImmutableMap; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.*; +import dan200.computercraft.api.network.IPacketNetwork; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.network.wired.IWiredSender; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IWorkMonitor; +import dan200.computercraft.core.apis.PeripheralAPI; +import dan200.computercraft.core.asm.PeripheralMethod; +import dan200.computercraft.shared.peripheral.modem.ModemPeripheral; +import dan200.computercraft.shared.peripheral.modem.ModemState; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public abstract class WiredModemPeripheral extends ModemPeripheral implements IWiredSender +{ + private final WiredModemElement modem; + + private final Map> peripheralWrappers = new HashMap<>( 1 ); + + public WiredModemPeripheral( ModemState state, WiredModemElement modem ) + { + super( state ); + this.modem = modem; + } + + //region IPacketSender implementation + @Override + public boolean isInterdimensional() + { + return false; + } + + @Override + public double getRange() + { + return 256.0; + } + + @Override + protected IPacketNetwork getNetwork() + { + return modem.getNode(); + } + + @Nonnull + @Override + public World getWorld() + { + return modem.getWorld(); + } + + @Nonnull + protected abstract WiredModemLocalPeripheral getLocalPeripheral(); + //endregion + + //region Peripheral methods + + /** + * List all remote peripherals on the wired network. + * + * If this computer is attached to the network, it _will not_ be included in + * this list. + * + *
Important: This function only appears on wired modems. Check {@link #isWireless} + * returns false before calling it.
+ * + * @param computer The calling computer. + * @return Remote peripheral names on the network. + */ + @LuaFunction + public final Collection getNamesRemote( IComputerAccess computer ) + { + return getWrappers( computer ).keySet(); + } + + /** + * Determine if a peripheral is available on this wired network. + * + *
Important: This function only appears on wired modems. Check {@link #isWireless} + * returns false before calling it.
+ * + * @param computer The calling computer. + * @param name The peripheral's name. + * @return boolean If a peripheral is present with the given name. + * @see PeripheralAPI#isPresent + */ + @LuaFunction + public final boolean isPresentRemote( IComputerAccess computer, String name ) + { + return getWrapper( computer, name ) != null; + } + + /** + * Get the type of a peripheral is available on this wired network. + * + *
Important: This function only appears on wired modems. Check {@link #isWireless} + * returns false before calling it.
+ * + * @param computer The calling computer. + * @param name The peripheral's name. + * @return The peripheral's name. + * @cc.treturn string|nil The peripheral's type, or {@code nil} if it is not present. + * @see PeripheralAPI#getType + */ + @LuaFunction + public final Object[] getTypeRemote( IComputerAccess computer, String name ) + { + RemotePeripheralWrapper wrapper = getWrapper( computer, name ); + return wrapper != null ? new Object[] { wrapper.getType() } : null; + } + + /** + * Get all available methods for the remote peripheral with the given name. + * + *
Important: This function only appears on wired modems. Check {@link #isWireless} + * returns false before calling it.
+ * + * @param computer The calling computer. + * @param name The peripheral's name. + * @return A list of methods provided by this peripheral, or {@code nil} if it is not present. + * @cc.treturn { string... }|nil A list of methods provided by this peripheral, or {@code nil} if it is not present. + * @see PeripheralAPI#getMethods + */ + @LuaFunction + public final Object[] getMethodsRemote( IComputerAccess computer, String name ) + { + RemotePeripheralWrapper wrapper = getWrapper( computer, name ); + if( wrapper == null ) return null; + + return new Object[] { wrapper.getMethodNames() }; + } + + /** + * Call a method on a peripheral on this wired network. + * + *
Important: This function only appears on wired modems. Check {@link #isWireless} + * returns false before calling it.
+ * + * @param computer The calling computer. + * @param context The Lua context we're executing in. + * @param arguments Arguments to this computer. + * @return The peripheral's result. + * @throws LuaException (hidden) If the method throws an error. + * @cc.tparam string remoteName The name of the peripheral to invoke the method on. + * @cc.tparam string method The name of the method + * @cc.param ... Additional arguments to pass to the method + * @cc.treturn string The return values of the peripheral method. + * @see PeripheralAPI#call + */ + @LuaFunction + public final MethodResult callRemote( IComputerAccess computer, ILuaContext context, IArguments arguments ) throws LuaException + { + String remoteName = arguments.getString( 0 ); + String methodName = arguments.getString( 1 ); + RemotePeripheralWrapper wrapper = getWrapper( computer, remoteName ); + if( wrapper == null ) throw new LuaException( "No peripheral: " + remoteName ); + + return wrapper.callMethod( context, methodName, arguments.drop( 2 ) ); + } + + /** + * Returns the network name of the current computer, if the modem is on. This + * may be used by other computers on the network to wrap this computer as a + * peripheral. + * + *
Important: This function only appears on wired modems. Check {@link #isWireless} + * returns false before calling it.
+ * + * @return The current computer's name. + * @cc.treturn string|nil The current computer's name on the wired network. + */ + @LuaFunction + public final Object[] getNameLocal() + { + String local = getLocalPeripheral().getConnectedName(); + return local == null ? null : new Object[] { local }; + } + + @Override + public void attach( @Nonnull IComputerAccess computer ) + { + super.attach( computer ); + + ConcurrentMap wrappers; + synchronized( peripheralWrappers ) + { + wrappers = peripheralWrappers.get( computer ); + if( wrappers == null ) peripheralWrappers.put( computer, wrappers = new ConcurrentHashMap<>() ); + } + + synchronized( modem.getRemotePeripherals() ) + { + for( Map.Entry entry : modem.getRemotePeripherals().entrySet() ) + { + attachPeripheralImpl( computer, wrappers, entry.getKey(), entry.getValue() ); + } + } + } + + @Override + public void detach( @Nonnull IComputerAccess computer ) + { + Map wrappers; + synchronized( peripheralWrappers ) + { + wrappers = peripheralWrappers.remove( computer ); + } + if( wrappers != null ) + { + for( RemotePeripheralWrapper wrapper : wrappers.values() ) wrapper.detach(); + wrappers.clear(); + } + + super.detach( computer ); + } + + @Override + public boolean equals( IPeripheral other ) + { + if( other instanceof WiredModemPeripheral ) + { + WiredModemPeripheral otherModem = (WiredModemPeripheral) other; + return otherModem.modem == modem; + } + return false; + } + //endregion + + @Nonnull + @Override + public IWiredNode getNode() + { + return modem.getNode(); + } + + public void attachPeripheral( String name, IPeripheral peripheral ) + { + synchronized( peripheralWrappers ) + { + for( Map.Entry> entry : peripheralWrappers.entrySet() ) + { + attachPeripheralImpl( entry.getKey(), entry.getValue(), name, peripheral ); + } + } + } + + public void detachPeripheral( String name ) + { + synchronized( peripheralWrappers ) + { + for( ConcurrentMap wrappers : peripheralWrappers.values() ) + { + RemotePeripheralWrapper wrapper = wrappers.remove( name ); + if( wrapper != null ) wrapper.detach(); + } + + } + } + + private void attachPeripheralImpl( IComputerAccess computer, ConcurrentMap peripherals, String periphName, IPeripheral peripheral ) + { + if( !peripherals.containsKey( periphName ) && !periphName.equals( getLocalPeripheral().getConnectedName() ) ) + { + RemotePeripheralWrapper wrapper = new RemotePeripheralWrapper( modem, peripheral, computer, periphName ); + peripherals.put( periphName, wrapper ); + wrapper.attach(); + } + } + + private ConcurrentMap getWrappers( IComputerAccess computer ) + { + synchronized( peripheralWrappers ) + { + return peripheralWrappers.get( computer ); + } + } + + private RemotePeripheralWrapper getWrapper( IComputerAccess computer, String remoteName ) + { + ConcurrentMap wrappers = getWrappers( computer ); + return wrappers == null ? null : wrappers.get( remoteName ); + } + + private static class RemotePeripheralWrapper implements IComputerAccess + { + private final WiredModemElement element; + private final IPeripheral peripheral; + private final IComputerAccess computer; + private final String name; + + private final String type; + private final Map methodMap; + + RemotePeripheralWrapper( WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name ) + { + this.element = element; + this.peripheral = peripheral; + this.computer = computer; + this.name = name; + + type = Objects.requireNonNull( peripheral.getType(), "Peripheral type cannot be null" ); + methodMap = PeripheralAPI.getMethods( peripheral ); + } + + public void attach() + { + peripheral.attach( this ); + computer.queueEvent( "peripheral", getAttachmentName() ); + } + + public void detach() + { + peripheral.detach( this ); + computer.queueEvent( "peripheral_detach", getAttachmentName() ); + } + + public String getType() + { + return type; + } + + public Collection getMethodNames() + { + return methodMap.keySet(); + } + + public MethodResult callMethod( ILuaContext context, String methodName, IArguments arguments ) throws LuaException + { + PeripheralMethod method = methodMap.get( methodName ); + if( method == null ) throw new LuaException( "No such method " + methodName ); + return method.apply( peripheral, context, this, arguments ); + } + + // IComputerAccess implementation + + @Override + public String mount( @Nonnull String desiredLocation, @Nonnull IMount mount ) + { + return computer.mount( desiredLocation, mount, name ); + } + + @Override + public String mount( @Nonnull String desiredLocation, @Nonnull IMount mount, @Nonnull String driveName ) + { + return computer.mount( desiredLocation, mount, driveName ); + } + + @Override + public String mountWritable( @Nonnull String desiredLocation, @Nonnull IWritableMount mount ) + { + return computer.mountWritable( desiredLocation, mount, name ); + } + + @Override + public String mountWritable( @Nonnull String desiredLocation, @Nonnull IWritableMount mount, @Nonnull String driveName ) + { + return computer.mountWritable( desiredLocation, mount, driveName ); + } + + @Override + public void unmount( String location ) + { + computer.unmount( location ); + } + + @Override + public int getID() + { + return computer.getID(); + } + + @Override + public void queueEvent( @Nonnull String event, Object... arguments ) + { + computer.queueEvent( event, arguments ); + } + + @Nonnull + @Override + public IWorkMonitor getMainThreadMonitor() + { + return computer.getMainThreadMonitor(); + } + + @Nonnull + @Override + public String getAttachmentName() + { + return name; + } + + @Nonnull + @Override + public Map getAvailablePeripherals() + { + synchronized( element.getRemotePeripherals() ) + { + return ImmutableMap.copyOf( element.getRemotePeripherals() ); + } + } + + @Nullable + @Override + public IPeripheral getAvailablePeripheral( @Nonnull String name ) + { + synchronized( element.getRemotePeripherals() ) + { + return element.getRemotePeripherals().get( name ); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/BlockWirelessModem.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/BlockWirelessModem.java new file mode 100644 index 000000000..8151d4664 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/BlockWirelessModem.java @@ -0,0 +1,97 @@ +/* + * 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.modem.wireless; + +import dan200.computercraft.shared.common.BlockGeneric; +import dan200.computercraft.shared.peripheral.modem.ModemShapes; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.ShapeContext; +import net.minecraft.block.Waterloggable; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.fluid.FluidState; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.BooleanProperty; +import net.minecraft.state.property.DirectionProperty; +import net.minecraft.state.property.Properties; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.BlockView; +import net.minecraft.world.WorldAccess; +import net.minecraft.world.WorldView; +import net.minecraftforge.fml.RegistryObject; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static dan200.computercraft.shared.util.WaterloggableHelpers.*; + +public class BlockWirelessModem extends BlockGeneric implements Waterloggable +{ + public static final DirectionProperty FACING = Properties.FACING; + public static final BooleanProperty ON = BooleanProperty.of( "on" ); + + public BlockWirelessModem( Settings settings, RegistryObject> type ) + { + super( settings, type ); + setDefaultState( getStateManager().getDefaultState() + .with( FACING, Direction.NORTH ) + .with( ON, false ) + .with( WATERLOGGED, false ) ); + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( FACING, ON, WATERLOGGED ); + } + + @Nonnull + @Override + @Deprecated + public VoxelShape getOutlineShape( BlockState blockState, @Nonnull BlockView blockView, @Nonnull BlockPos blockPos, @Nonnull ShapeContext context ) + { + return ModemShapes.getBounds( blockState.get( FACING ) ); + } + + @Nonnull + @Override + @Deprecated + public FluidState getFluidState( @Nonnull BlockState state ) + { + return getWaterloggedFluidState( state ); + } + + @Nonnull + @Override + @Deprecated + public BlockState getStateForNeighborUpdate( @Nonnull BlockState state, @Nonnull Direction side, @Nonnull BlockState otherState, @Nonnull WorldAccess world, @Nonnull BlockPos pos, @Nonnull BlockPos otherPos ) + { + updateWaterloggedPostPlacement( state, world, pos ); + return side == state.get( FACING ) && !state.canPlaceAt( world, pos ) + ? state.getFluidState().getBlockState() + : state; + } + + @Override + @Deprecated + public boolean canPlaceAt( BlockState state, @Nonnull WorldView world, BlockPos pos ) + { + Direction facing = state.get( FACING ); + return sideCoversSmallSquare( world, pos.offset( facing ), facing.getOpposite() ); + } + + @Nullable + @Override + public BlockState getPlacementState( ItemPlacementContext placement ) + { + return getDefaultState() + .with( FACING, placement.getSide().getOpposite() ) + .with( WATERLOGGED, getWaterloggedStateForPlacement( placement ) ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java new file mode 100644 index 000000000..aecfbfa73 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java @@ -0,0 +1,154 @@ +/* + * 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.modem.wireless; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.peripheral.modem.ModemPeripheral; +import dan200.computercraft.shared.peripheral.modem.ModemState; +import dan200.computercraft.shared.util.CapabilityUtil; +import dan200.computercraft.shared.util.TickScheduler; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; + +public class TileWirelessModem extends TileGeneric +{ + private static class Peripheral extends WirelessModemPeripheral + { + private final TileWirelessModem entity; + + Peripheral( TileWirelessModem entity ) + { + super( new ModemState( () -> TickScheduler.schedule( entity ) ), entity.advanced ); + this.entity = entity; + } + + @Nonnull + @Override + public World getWorld() + { + return entity.getWorld(); + } + + @Nonnull + @Override + public Vec3d getPosition() + { + BlockPos pos = entity.getPos().offset( entity.modemDirection ); + return new Vec3d( pos.getX(), pos.getY(), pos.getZ() ); + } + + @Override + public boolean equals( IPeripheral other ) + { + return this == other || (other instanceof Peripheral && entity == ((Peripheral) other).entity); + } + + @Nonnull + @Override + public Object getTarget() + { + return entity; + } + } + + private final boolean advanced; + + private boolean hasModemDirection = false; + private Direction modemDirection = Direction.DOWN; + private final ModemPeripheral modem; + private boolean destroyed = false; + private LazyOptional modemCap; + + public TileWirelessModem( BlockEntityType type, boolean advanced ) + { + super( type ); + this.advanced = advanced; + modem = new Peripheral( this ); + } + + @Override + public void onLoad() + { + super.onLoad(); + TickScheduler.schedule( this ); + } + + @Override + public void destroy() + { + if( !destroyed ) + { + modem.destroy(); + destroyed = true; + } + } + + @Override + public void resetBlock() + { + super.resetBlock(); + hasModemDirection = false; + world.getBlockTickScheduler().schedule( getPos(), getCachedState().getBlock(), 0 ); + } + + @Override + public void blockTick() + { + Direction currentDirection = modemDirection; + refreshDirection(); + // Invalidate the capability if the direction has changed. I'm not 100% happy with this implementation + // - ideally we'd do it within refreshDirection or updateContainingBlockInfo, but this seems the _safest_ + // place. + if( currentDirection != modemDirection ) modemCap = CapabilityUtil.invalidate( modemCap ); + + if( modem.getModemState().pollChanged() ) updateBlockState(); + } + + private void refreshDirection() + { + if( hasModemDirection ) return; + + hasModemDirection = true; + modemDirection = getCachedState().get( BlockWirelessModem.FACING ); + } + + private void updateBlockState() + { + boolean on = modem.getModemState().isOpen(); + BlockState state = getCachedState(); + if( state.get( BlockWirelessModem.ON ) != on ) + { + getWorld().setBlockState( getPos(), state.with( BlockWirelessModem.ON, on ) ); + } + } + + @Nonnull + @Override + public LazyOptional getCapability( @Nonnull Capability cap, @Nullable Direction side ) + { + if( cap == CAPABILITY_PERIPHERAL ) + { + refreshDirection(); + if( side != null && modemDirection != side ) return LazyOptional.empty(); + if( modemCap == null ) modemCap = LazyOptional.of( () -> modem ); + return modemCap.cast(); + } + + return super.getCapability( cap, side ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemPeripheral.java new file mode 100644 index 000000000..68d77d366 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemPeripheral.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.modem.wireless; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.network.IPacketNetwork; +import dan200.computercraft.shared.peripheral.modem.ModemPeripheral; +import dan200.computercraft.shared.peripheral.modem.ModemState; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +public abstract class WirelessModemPeripheral extends ModemPeripheral +{ + private final boolean m_advanced; + + public WirelessModemPeripheral( ModemState state, boolean advanced ) + { + super( state ); + m_advanced = advanced; + } + + @Override + public boolean isInterdimensional() + { + return m_advanced; + } + + @Override + public double getRange() + { + if( m_advanced ) + { + return Integer.MAX_VALUE; + } + else + { + World world = getWorld(); + if( world != null ) + { + Vec3d position = getPosition(); + double minRange = ComputerCraft.modemRange; + double maxRange = ComputerCraft.modemHighAltitudeRange; + if( world.isRaining() && world.isThundering() ) + { + minRange = ComputerCraft.modemRangeDuringStorm; + maxRange = ComputerCraft.modemHighAltitudeRangeDuringStorm; + } + if( position.y > 96.0 && maxRange > minRange ) + { + return minRange + (position.y - 96.0) * ((maxRange - minRange) / ((world.getHeight() - 1) - 96.0)); + } + return minRange; + } + return 0.0; + } + } + + @Override + protected IPacketNetwork getNetwork() + { + return WirelessNetwork.getUniversal(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessNetwork.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessNetwork.java new file mode 100644 index 000000000..b796fd4fc --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessNetwork.java @@ -0,0 +1,93 @@ +/* + * 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.modem.wireless; + +import dan200.computercraft.api.network.IPacketNetwork; +import dan200.computercraft.api.network.IPacketReceiver; +import dan200.computercraft.api.network.IPacketSender; +import dan200.computercraft.api.network.Packet; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class WirelessNetwork implements IPacketNetwork +{ + private static WirelessNetwork s_universalNetwork = null; + + public static WirelessNetwork getUniversal() + { + if( s_universalNetwork == null ) + { + s_universalNetwork = new WirelessNetwork(); + } + return s_universalNetwork; + } + + public static void resetNetworks() + { + s_universalNetwork = null; + } + + private final Set m_receivers = Collections.newSetFromMap( new ConcurrentHashMap<>() ); + + @Override + public void addReceiver( @Nonnull IPacketReceiver receiver ) + { + Objects.requireNonNull( receiver, "device cannot be null" ); + m_receivers.add( receiver ); + } + + @Override + public void removeReceiver( @Nonnull IPacketReceiver receiver ) + { + Objects.requireNonNull( receiver, "device cannot be null" ); + m_receivers.remove( receiver ); + } + + @Override + public void transmitSameDimension( @Nonnull Packet packet, double range ) + { + Objects.requireNonNull( packet, "packet cannot be null" ); + for( IPacketReceiver device : m_receivers ) tryTransmit( device, packet, range, false ); + } + + @Override + public void transmitInterdimensional( @Nonnull Packet packet ) + { + Objects.requireNonNull( packet, "packet cannot be null" ); + for( IPacketReceiver device : m_receivers ) tryTransmit( device, packet, 0, true ); + } + + private static void tryTransmit( IPacketReceiver receiver, Packet packet, double range, boolean interdimensional ) + { + IPacketSender sender = packet.getSender(); + if( receiver.getWorld() == sender.getWorld() ) + { + double receiveRange = Math.max( range, receiver.getRange() ); // Ensure range is symmetrical + double distanceSq = receiver.getPosition().squaredDistanceTo( sender.getPosition() ); + if( interdimensional || receiver.isInterdimensional() || distanceSq <= receiveRange * receiveRange ) + { + receiver.receiveSameDimension( packet, Math.sqrt( distanceSq ) ); + } + } + else + { + if( interdimensional || receiver.isInterdimensional() ) + { + receiver.receiveDifferentDimension( packet ); + } + } + } + + @Override + public boolean isWireless() + { + return true; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java new file mode 100644 index 000000000..f80a02327 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java @@ -0,0 +1,93 @@ +/* + * 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.monitor; + +import dan200.computercraft.shared.common.BlockGeneric; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.DirectionProperty; +import net.minecraft.state.property.EnumProperty; +import net.minecraft.state.property.Properties; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.fml.RegistryObject; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class BlockMonitor extends BlockGeneric +{ + public static final DirectionProperty ORIENTATION = DirectionProperty.of( "orientation", + Direction.UP, Direction.DOWN, Direction.NORTH ); + + public static final DirectionProperty FACING = Properties.HORIZONTAL_FACING; + + static final EnumProperty STATE = EnumProperty.of( "state", MonitorEdgeState.class ); + + public BlockMonitor( Settings settings, RegistryObject> type ) + { + super( settings, type ); + // TODO: Test underwater - do we need isSolid at all? + setDefaultState( getStateManager().getDefaultState() + .with( ORIENTATION, Direction.NORTH ) + .with( FACING, Direction.NORTH ) + .with( STATE, MonitorEdgeState.NONE ) ); + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( ORIENTATION, FACING, STATE ); + } + + @Override + @Nullable + public BlockState getPlacementState( ItemPlacementContext context ) + { + float pitch = context.getPlayer() == null ? 0 : context.getPlayer().pitch; + Direction orientation; + if( pitch > 66.5f ) + { + // If the player is looking down, place it facing upwards + orientation = Direction.UP; + } + else if( pitch < -66.5f ) + { + // If they're looking up, place it down. + orientation = Direction.DOWN; + } + else + { + orientation = Direction.NORTH; + } + + return getDefaultState() + .with( FACING, context.getPlayerFacing().getOpposite() ) + .with( ORIENTATION, orientation ); + } + + @Override + public void onPlaced( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState blockState, @Nullable LivingEntity livingEntity, @Nonnull ItemStack itemStack ) + { + super.onPlaced( world, pos, blockState, livingEntity, itemStack ); + + BlockEntity entity = world.getBlockEntity( pos ); + if( entity instanceof TileMonitor && !world.isClient ) + { + TileMonitor monitor = (TileMonitor) entity; + monitor.contractNeighbours(); + monitor.contract(); + monitor.expand(); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java new file mode 100644 index 000000000..7d89cf426 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java @@ -0,0 +1,154 @@ +/* + * 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.monitor; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.client.gui.FixedWidthFontRenderer; +import dan200.computercraft.shared.common.ClientTerminal; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gl.VertexBuffer; +import net.minecraft.util.math.BlockPos; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL31; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +public final class ClientMonitor extends ClientTerminal +{ + private static final Set allMonitors = new HashSet<>(); + + private final TileMonitor origin; + + public long lastRenderFrame = -1; + public BlockPos lastRenderPos = null; + + public int tboBuffer; + public int tboTexture; + public VertexBuffer buffer; + + public ClientMonitor( boolean colour, TileMonitor origin ) + { + super( colour ); + this.origin = origin; + } + + public TileMonitor getOrigin() + { + return origin; + } + + /** + * Create the appropriate buffer if needed. + * + * @param renderer The renderer to use. This can be fetched from {@link MonitorRenderer#current()}. + * @return If a buffer was created. This will return {@code false} if we already have an appropriate buffer, + * or this mode does not require one. + */ + @Environment(EnvType.CLIENT) + public boolean createBuffer( MonitorRenderer renderer ) + { + switch( renderer ) + { + case TBO: + { + if( tboBuffer != 0 ) return false; + + deleteBuffers(); + + tboBuffer = GlStateManager.genBuffers(); + GlStateManager.bindBuffers( GL31.GL_TEXTURE_BUFFER, tboBuffer ); + GL15.glBufferData( GL31.GL_TEXTURE_BUFFER, 0, GL15.GL_STATIC_DRAW ); + tboTexture = GlStateManager.genTextures(); + GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, tboTexture ); + GL31.glTexBuffer( GL31.GL_TEXTURE_BUFFER, GL30.GL_R8, tboBuffer ); + GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, 0 ); + + GlStateManager.bindBuffers( GL31.GL_TEXTURE_BUFFER, 0 ); + + addMonitor(); + return true; + } + + case VBO: + if( buffer != null ) return false; + + deleteBuffers(); + buffer = new VertexBuffer( FixedWidthFontRenderer.TYPE.getVertexFormat() ); + addMonitor(); + return true; + + default: + return false; + } + } + + private void addMonitor() + { + synchronized( allMonitors ) + { + allMonitors.add( this ); + } + } + + private void deleteBuffers() + { + + if( tboBuffer != 0 ) + { + RenderSystem.glDeleteBuffers( tboBuffer ); + tboBuffer = 0; + } + + if( tboTexture != 0 ) + { + GlStateManager.deleteTexture( tboTexture ); + tboTexture = 0; + } + + if( buffer != null ) + { + buffer.close(); + buffer = null; + } + } + + @Environment(EnvType.CLIENT) + public void destroy() + { + if( tboBuffer != 0 || buffer != null ) + { + synchronized( allMonitors ) + { + allMonitors.remove( this ); + } + + deleteBuffers(); + } + } + + @Environment(EnvType.CLIENT) + public static void destroyAll() + { + synchronized( allMonitors ) + { + for( Iterator iterator = allMonitors.iterator(); iterator.hasNext(); ) + { + ClientMonitor monitor = iterator.next(); + monitor.deleteBuffers(); + + iterator.remove(); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorEdgeState.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorEdgeState.java new file mode 100644 index 000000000..08bcaca7e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorEdgeState.java @@ -0,0 +1,73 @@ +/* + * 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.monitor; + +import javax.annotation.Nonnull; +import net.minecraft.util.StringIdentifiable; + +import static dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState.Flags.*; + +public enum MonitorEdgeState implements StringIdentifiable +{ + NONE( "none", 0 ), + + L( "l", LEFT ), + R( "r", RIGHT ), + LR( "lr", LEFT | RIGHT ), + U( "u", UP ), + D( "d", DOWN ), + + UD( "ud", UP | DOWN ), + RD( "rd", RIGHT | DOWN ), + LD( "ld", LEFT | DOWN ), + RU( "ru", RIGHT | UP ), + LU( "lu", LEFT | UP ), + + LRD( "lrd", LEFT | RIGHT | DOWN ), + RUD( "rud", RIGHT | UP | DOWN ), + LUD( "lud", LEFT | UP | DOWN ), + LRU( "lru", LEFT | RIGHT | UP ), + LRUD( "lrud", LEFT | RIGHT | UP | DOWN ); + + private final String name; + private final int flags; + + MonitorEdgeState( String name, int flags ) + { + this.name = name; + this.flags = flags; + } + + private static final MonitorEdgeState[] BY_FLAG = new MonitorEdgeState[16]; + + static + { + for( MonitorEdgeState state : values() ) + { + BY_FLAG[state.flags] = state; + } + } + + public static MonitorEdgeState fromConnections( boolean up, boolean down, boolean left, boolean right ) + { + return BY_FLAG[(up ? UP : 0) | (down ? DOWN : 0) | (left ? LEFT : 0) | (right ? RIGHT : 0)]; + } + + @Nonnull + @Override + public String asString() + { + return name; + } + + static final class Flags + { + static final int UP = 1 << 0; + static final int DOWN = 1 << 1; + static final int LEFT = 1 << 2; + static final int RIGHT = 1 << 3; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java new file mode 100644 index 000000000..c2e8cb050 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java @@ -0,0 +1,128 @@ +/* + * 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.monitor; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.LuaValues; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.apis.TermMethods; +import dan200.computercraft.core.terminal.Terminal; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Monitors are a block which act as a terminal, displaying information on one side. This allows them to be read and + * interacted with in-world without opening a GUI. + * + * Monitors act as @{term.Redirect|terminal redirects} and so expose the same methods, as well as several additional + * ones, which are documented below. + * + * Like computers, monitors come in both normal (no colour) and advanced (colour) varieties. + * + * @cc.module monitor + * @cc.usage Write "Hello, world!" to an adjacent monitor: + * + *
+ * local monitor = peripheral.find("monitor")
+ * monitor.setCursorPos(1, 1)
+ * monitor.write("Hello, world!")
+ * 
+ */ +public class MonitorPeripheral extends TermMethods implements IPeripheral +{ + private final TileMonitor monitor; + + public MonitorPeripheral( TileMonitor monitor ) + { + this.monitor = monitor; + } + + @Nonnull + @Override + public String getType() + { + return "monitor"; + } + + /** + * Set the scale of this monitor. A larger scale will result in the monitor having a lower resolution, but display + * text much larger. + * + * @param scaleArg The monitor's scale. This must be a multiple of 0.5 between 0.5 and 5. + * @throws LuaException If the scale is out of range. + * @see #getTextScale() + */ + @LuaFunction + public final void setTextScale( double scaleArg ) throws LuaException + { + int scale = (int) (LuaValues.checkFinite( 0, scaleArg ) * 2.0); + if( scale < 1 || scale > 10 ) throw new LuaException( "Expected number in range 0.5-5" ); + getMonitor().setTextScale( scale ); + } + + /** + * Get the monitor's current text scale. + * + * @return The monitor's current scale. + * @throws LuaException If the monitor cannot be found. + */ + @LuaFunction + public final double getTextScale() throws LuaException + { + return getMonitor().getTextScale() / 2.0; + } + + @Override + public void attach( @Nonnull IComputerAccess computer ) + { + monitor.addComputer( computer ); + } + + @Override + public void detach( @Nonnull IComputerAccess computer ) + { + monitor.removeComputer( computer ); + } + + @Override + public boolean equals( IPeripheral other ) + { + return other instanceof MonitorPeripheral && monitor == ((MonitorPeripheral) other).monitor; + } + + @Nonnull + private ServerMonitor getMonitor() throws LuaException + { + ServerMonitor monitor = this.monitor.getCachedServerMonitor(); + if( monitor == null ) throw new LuaException( "Monitor has been detached" ); + return monitor; + } + + @Nonnull + @Override + public Terminal getTerminal() throws LuaException + { + Terminal terminal = getMonitor().getTerminal(); + if( terminal == null ) throw new LuaException( "Monitor has been detached" ); + return terminal; + } + + @Override + public boolean isColour() throws LuaException + { + return getMonitor().isColour(); + } + + @Nullable + @Override + public Object getTarget() + { + return monitor; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorRenderer.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorRenderer.java new file mode 100644 index 000000000..0aa86610c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorRenderer.java @@ -0,0 +1,86 @@ +/* + * 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.monitor; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.render.TileEntityMonitorRenderer; +import org.lwjgl.opengl.GL; + +import javax.annotation.Nonnull; + +/** + * The render type to use for monitors. + * + * @see TileEntityMonitorRenderer + * @see ClientMonitor + */ +public enum MonitorRenderer +{ + /** + * Determine the best monitor backend. + */ + BEST, + + /** + * Render using texture buffer objects. + * + * @see org.lwjgl.opengl.GL31#glTexBuffer(int, int, int) + */ + TBO, + + /** + * Render using VBOs. + * + * @see net.minecraft.client.renderer.vertex.VertexBuffer + */ + VBO; + + /** + * Get the current renderer to use. + * + * @return The current renderer. Will not return {@link MonitorRenderer#BEST}. + */ + @Nonnull + public static MonitorRenderer current() + { + MonitorRenderer current = ComputerCraft.monitorRenderer; + switch( current ) + { + case BEST: + return best(); + case TBO: + checkCapabilities(); + if( !textureBuffer ) + { + ComputerCraft.log.warn( "Texture buffers are not supported on your graphics card. Falling back to default." ); + ComputerCraft.monitorRenderer = BEST; + return best(); + } + + return TBO; + default: + return current; + } + } + + private static MonitorRenderer best() + { + checkCapabilities(); + return textureBuffer ? TBO : VBO; + } + + private static boolean initialised = false; + private static boolean textureBuffer = false; + + private static void checkCapabilities() + { + if( initialised ) return; + + textureBuffer = GL.getCapabilities().OpenGL31; + initialised = true; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorWatcher.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorWatcher.java new file mode 100644 index 000000000..a986a8f74 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorWatcher.java @@ -0,0 +1,105 @@ +/* + * 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.monitor; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.client.MonitorClientMessage; +import dan200.computercraft.shared.network.client.TerminalState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.chunk.ChunkStatus; +import net.minecraft.world.chunk.WorldChunk; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.world.ChunkWatchEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.ArrayDeque; +import java.util.Queue; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID ) +public final class MonitorWatcher +{ + private static final Queue watching = new ArrayDeque<>(); + + private MonitorWatcher() + { + } + + static void enqueue( TileMonitor monitor ) + { + if( monitor.enqueued ) return; + + monitor.enqueued = true; + monitor.cached = null; + watching.add( monitor ); + } + + @SubscribeEvent + public static void onWatch( ChunkWatchEvent.Watch event ) + { + ChunkPos chunkPos = event.getPos(); + WorldChunk chunk = (WorldChunk) event.getWorld().getChunk( chunkPos.x, chunkPos.z, ChunkStatus.FULL, false ); + if( chunk == null ) return; + + for( BlockEntity te : chunk.getBlockEntities().values() ) + { + // Find all origin monitors who are not already on the queue. + if( !(te instanceof TileMonitor) ) continue; + + TileMonitor monitor = (TileMonitor) te; + ServerMonitor serverMonitor = getMonitor( monitor ); + if( serverMonitor == null || monitor.enqueued ) continue; + + // We use the cached terminal state if available - this is guaranteed to + TerminalState state = monitor.cached; + if( state == null ) state = monitor.cached = serverMonitor.write(); + NetworkHandler.sendToPlayer( event.getPlayer(), new MonitorClientMessage( monitor.getPos(), state ) ); + } + } + + @SubscribeEvent + public static void onTick( TickEvent.ServerTickEvent event ) + { + if( event.phase != TickEvent.Phase.END ) return; + + long limit = ComputerCraft.monitorBandwidth; + boolean obeyLimit = limit > 0; + + TileMonitor tile; + while( (!obeyLimit || limit > 0) && (tile = watching.poll()) != null ) + { + tile.enqueued = false; + ServerMonitor monitor = getMonitor( tile ); + if( monitor == null ) continue; + + BlockPos pos = tile.getPos(); + World world = tile.getWorld(); + if( !(world instanceof ServerWorld) ) continue; + + WorldChunk chunk = world.getWorldChunk( pos ); + if( !((ServerWorld) world).getChunkManager().threadedAnvilChunkStorage.getPlayersWatchingChunk( chunk.getPos(), false ).findAny().isPresent() ) + { + continue; + } + + TerminalState state = tile.cached = monitor.write(); + NetworkHandler.sendToAllTracking( new MonitorClientMessage( pos, state ), chunk ); + + limit -= state.size(); + } + } + + private static ServerMonitor getMonitor( TileMonitor monitor ) + { + return !monitor.isRemoved() && monitor.getXIndex() == 0 && monitor.getYIndex() == 0 ? monitor.getCachedServerMonitor() : null; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java new file mode 100644 index 000000000..11e404dcf --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java @@ -0,0 +1,91 @@ +/* + * 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.monitor; + +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.common.ServerTerminal; +import dan200.computercraft.shared.util.TickScheduler; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class ServerMonitor extends ServerTerminal +{ + private final TileMonitor origin; + private int textScale = 2; + private final AtomicBoolean resized = new AtomicBoolean( false ); + private final AtomicBoolean changed = new AtomicBoolean( false ); + + public ServerMonitor( boolean colour, TileMonitor origin ) + { + super( colour ); + this.origin = origin; + } + + public synchronized void rebuild() + { + Terminal oldTerm = getTerminal(); + int oldWidth = oldTerm == null ? -1 : oldTerm.getWidth(); + int oldHeight = oldTerm == null ? -1 : oldTerm.getHeight(); + + double textScale = this.textScale * 0.5; + int termWidth = (int) Math.max( + Math.round( (origin.getWidth() - 2.0 * (TileMonitor.RENDER_BORDER + TileMonitor.RENDER_MARGIN)) / (textScale * 6.0 * TileMonitor.RENDER_PIXEL_SCALE) ), + 1.0 + ); + int termHeight = (int) Math.max( + Math.round( (origin.getHeight() - 2.0 * (TileMonitor.RENDER_BORDER + TileMonitor.RENDER_MARGIN)) / (textScale * 9.0 * TileMonitor.RENDER_PIXEL_SCALE) ), + 1.0 + ); + + resize( termWidth, termHeight ); + if( oldWidth != termWidth || oldHeight != termHeight ) + { + getTerminal().clear(); + resized.set( true ); + markChanged(); + } + } + + @Override + protected void markTerminalChanged() + { + super.markTerminalChanged(); + markChanged(); + } + + private void markChanged() + { + if( !changed.getAndSet( true ) ) TickScheduler.schedule( origin ); + } + + protected void clearChanged() + { + changed.set( false ); + } + + public int getTextScale() + { + return textScale; + } + + public synchronized void setTextScale( int textScale ) + { + if( this.textScale == textScale ) return; + this.textScale = textScale; + rebuild(); + } + + public boolean pollResized() + { + return resized.getAndSet( false ); + } + + public boolean pollTerminalChanged() + { + update(); + return hasTerminalChanged(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java new file mode 100644 index 000000000..8aa20e8a8 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java @@ -0,0 +1,699 @@ +/* + * 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.monitor; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.common.ServerTerminal; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.network.client.TerminalState; +import dan200.computercraft.shared.util.CapabilityUtil; +import dan200.computercraft.shared.util.TickScheduler; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashSet; +import java.util.Set; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; + +public class TileMonitor extends TileGeneric +{ + public static final double RENDER_BORDER = 2.0 / 16.0; + public static final double RENDER_MARGIN = 0.5 / 16.0; + public static final double RENDER_PIXEL_SCALE = 1.0 / 64.0; + + private static final String NBT_X = "XIndex"; + private static final String NBT_Y = "YIndex"; + private static final String NBT_WIDTH = "Width"; + private static final String NBT_HEIGHT = "Height"; + + private final boolean advanced; + + private ServerMonitor m_serverMonitor; + private ClientMonitor m_clientMonitor; + private MonitorPeripheral peripheral; + private LazyOptional peripheralCap; + private final Set m_computers = new HashSet<>(); + + private boolean m_destroyed = false; + private boolean visiting = false; + + // MonitorWatcher state. + boolean enqueued; + TerminalState cached; + + private int m_width = 1; + private int m_height = 1; + private int m_xIndex = 0; + private int m_yIndex = 0; + + public TileMonitor( BlockEntityType type, boolean advanced ) + { + super( type ); + this.advanced = advanced; + } + + @Override + public void onLoad() + { + super.onLoad(); + TickScheduler.schedule( this ); + } + + @Override + public void destroy() + { + // TODO: Call this before using the block + if( m_destroyed ) return; + m_destroyed = true; + if( !getWorld().isClient ) contractNeighbours(); + } + + @Override + public void markRemoved() + { + super.markRemoved(); + if( m_clientMonitor != null && m_xIndex == 0 && m_yIndex == 0 ) m_clientMonitor.destroy(); + } + + @Override + public void onChunkUnloaded() + { + super.onChunkUnloaded(); + if( m_clientMonitor != null && m_xIndex == 0 && m_yIndex == 0 ) m_clientMonitor.destroy(); + } + + @Nonnull + @Override + public ActionResult onActivate( PlayerEntity player, Hand hand, BlockHitResult hit ) + { + if( !player.isInSneakingPose() && getFront() == hit.getSide() ) + { + if( !getWorld().isClient ) + { + monitorTouched( + (float) (hit.getPos().x - hit.getBlockPos().getX()), + (float) (hit.getPos().y - hit.getBlockPos().getY()), + (float) (hit.getPos().z - hit.getBlockPos().getZ()) + ); + } + return ActionResult.SUCCESS; + } + + return ActionResult.PASS; + } + + @Nonnull + @Override + public CompoundTag toTag( CompoundTag tag ) + { + tag.putInt( NBT_X, m_xIndex ); + tag.putInt( NBT_Y, m_yIndex ); + tag.putInt( NBT_WIDTH, m_width ); + tag.putInt( NBT_HEIGHT, m_height ); + return super.toTag( tag ); + } + + @Override + public void fromTag( @Nonnull BlockState state, @Nonnull CompoundTag nbt ) + { + super.fromTag( state, nbt ); + + m_xIndex = nbt.getInt( NBT_X ); + m_yIndex = nbt.getInt( NBT_Y ); + m_width = nbt.getInt( NBT_WIDTH ); + m_height = nbt.getInt( NBT_HEIGHT ); + } + + @Override + public void blockTick() + { + if( m_xIndex != 0 || m_yIndex != 0 || m_serverMonitor == null ) return; + + m_serverMonitor.clearChanged(); + + if( m_serverMonitor.pollResized() ) + { + for( int x = 0; x < m_width; x++ ) + { + for( int y = 0; y < m_height; y++ ) + { + TileMonitor monitor = getNeighbour( x, y ); + if( monitor == null ) continue; + + for( IComputerAccess computer : monitor.m_computers ) + { + computer.queueEvent( "monitor_resize", computer.getAttachmentName() ); + } + } + } + } + + if( m_serverMonitor.pollTerminalChanged() ) MonitorWatcher.enqueue( this ); + } + + @Override + protected void invalidateCaps() + { + super.invalidateCaps(); + peripheralCap = CapabilityUtil.invalidate( peripheralCap ); + } + + @Nonnull + @Override + public LazyOptional getCapability( @Nonnull Capability cap, @Nullable Direction side ) + { + if( cap == CAPABILITY_PERIPHERAL ) + { + createServerMonitor(); // Ensure the monitor is created before doing anything else. + if( peripheral == null ) peripheral = new MonitorPeripheral( this ); + if( peripheralCap == null ) peripheralCap = LazyOptional.of( () -> peripheral ); + return peripheralCap.cast(); + } + return super.getCapability( cap, side ); + } + + public ServerMonitor getCachedServerMonitor() + { + return m_serverMonitor; + } + + private ServerMonitor getServerMonitor() + { + if( m_serverMonitor != null ) return m_serverMonitor; + + TileMonitor origin = getOrigin(); + if( origin == null ) return null; + + return m_serverMonitor = origin.m_serverMonitor; + } + + private ServerMonitor createServerMonitor() + { + if( m_serverMonitor != null ) return m_serverMonitor; + + if( m_xIndex == 0 && m_yIndex == 0 ) + { + // If we're the origin, set up the new monitor + m_serverMonitor = new ServerMonitor( advanced, this ); + m_serverMonitor.rebuild(); + + // And propagate it to child monitors + for( int x = 0; x < m_width; x++ ) + { + for( int y = 0; y < m_height; y++ ) + { + TileMonitor monitor = getNeighbour( x, y ); + if( monitor != null ) monitor.m_serverMonitor = m_serverMonitor; + } + } + + return m_serverMonitor; + } + else + { + // Otherwise fetch the origin and attempt to get its monitor + // Note this may load chunks, but we don't really have a choice here. + BlockPos pos = getPos(); + BlockEntity te = world.getBlockEntity( pos.offset( getRight(), -m_xIndex ).offset( getDown(), -m_yIndex ) ); + if( !(te instanceof TileMonitor) ) return null; + + return m_serverMonitor = ((TileMonitor) te).createServerMonitor(); + } + } + + public ClientMonitor getClientMonitor() + { + if( m_clientMonitor != null ) return m_clientMonitor; + + BlockPos pos = getPos(); + BlockEntity te = world.getBlockEntity( pos.offset( getRight(), -m_xIndex ).offset( getDown(), -m_yIndex ) ); + if( !(te instanceof TileMonitor) ) return null; + + return m_clientMonitor = ((TileMonitor) te).m_clientMonitor; + } + + // Networking stuff + + @Override + protected void writeDescription( @Nonnull CompoundTag nbt ) + { + super.writeDescription( nbt ); + nbt.putInt( NBT_X, m_xIndex ); + nbt.putInt( NBT_Y, m_yIndex ); + nbt.putInt( NBT_WIDTH, m_width ); + nbt.putInt( NBT_HEIGHT, m_height ); + } + + @Override + protected final void readDescription( @Nonnull CompoundTag nbt ) + { + super.readDescription( nbt ); + + int oldXIndex = m_xIndex; + int oldYIndex = m_yIndex; + int oldWidth = m_width; + int oldHeight = m_height; + + m_xIndex = nbt.getInt( NBT_X ); + m_yIndex = nbt.getInt( NBT_Y ); + m_width = nbt.getInt( NBT_WIDTH ); + m_height = nbt.getInt( NBT_HEIGHT ); + + if( oldXIndex != m_xIndex || oldYIndex != m_yIndex ) + { + // If our index has changed then it's possible the origin monitor has changed. Thus + // we'll clear our cache. If we're the origin then we'll need to remove the glList as well. + if( oldXIndex == 0 && oldYIndex == 0 && m_clientMonitor != null ) m_clientMonitor.destroy(); + m_clientMonitor = null; + } + + if( m_xIndex == 0 && m_yIndex == 0 ) + { + // If we're the origin terminal then create it. + if( m_clientMonitor == null ) m_clientMonitor = new ClientMonitor( advanced, this ); + } + + if( oldXIndex != m_xIndex || oldYIndex != m_yIndex || + oldWidth != m_width || oldHeight != m_height ) + { + // One of our properties has changed, so ensure we redraw the block + updateBlock(); + } + } + + public final void read( TerminalState state ) + { + if( m_xIndex != 0 || m_yIndex != 0 ) + { + ComputerCraft.log.warn( "Receiving monitor state for non-origin terminal at {}", getPos() ); + return; + } + + if( m_clientMonitor == null ) m_clientMonitor = new ClientMonitor( advanced, this ); + m_clientMonitor.read( state ); + } + + // Sizing and placement stuff + + private void updateBlockState() + { + getWorld().setBlockState( getPos(), getCachedState() + .with( BlockMonitor.STATE, MonitorEdgeState.fromConnections( + m_yIndex < m_height - 1, m_yIndex > 0, + m_xIndex > 0, m_xIndex < m_width - 1 ) ), 2 ); + } + + // region Sizing and placement stuff + public Direction getDirection() + { + return getCachedState().get( BlockMonitor.FACING ); + } + + public Direction getOrientation() + { + return getCachedState().get( BlockMonitor.ORIENTATION ); + } + + public Direction getFront() + { + Direction orientation = getOrientation(); + return orientation == Direction.NORTH ? getDirection() : orientation; + } + + public Direction getRight() + { + return getDirection().rotateYCounterclockwise(); + } + + public Direction getDown() + { + Direction orientation = getOrientation(); + if( orientation == Direction.NORTH ) return Direction.UP; + return orientation == Direction.DOWN ? getDirection() : getDirection().getOpposite(); + } + + public int getWidth() + { + return m_width; + } + + public int getHeight() + { + return m_height; + } + + public int getXIndex() + { + return m_xIndex; + } + + public int getYIndex() + { + return m_yIndex; + } + + private TileMonitor getSimilarMonitorAt( BlockPos pos ) + { + if( pos.equals( getPos() ) ) return this; + + int y = pos.getY(); + World world = getWorld(); + if( world == null || !world.isAreaLoaded( pos, 0 ) ) return null; + + BlockEntity tile = world.getBlockEntity( pos ); + if( !(tile instanceof TileMonitor) ) return null; + + TileMonitor monitor = (TileMonitor) tile; + return !monitor.visiting && !monitor.m_destroyed && advanced == monitor.advanced + && getDirection() == monitor.getDirection() && getOrientation() == monitor.getOrientation() + ? monitor : null; + } + + private TileMonitor getNeighbour( int x, int y ) + { + BlockPos pos = getPos(); + Direction right = getRight(); + Direction down = getDown(); + int xOffset = -m_xIndex + x; + int yOffset = -m_yIndex + y; + return getSimilarMonitorAt( pos.offset( right, xOffset ).offset( down, yOffset ) ); + } + + private TileMonitor getOrigin() + { + return getNeighbour( 0, 0 ); + } + + private void resize( int width, int height ) + { + // If we're not already the origin then we'll need to generate a new terminal. + if( m_xIndex != 0 || m_yIndex != 0 ) m_serverMonitor = null; + + m_xIndex = 0; + m_yIndex = 0; + m_width = width; + m_height = height; + + // Determine if we actually need a monitor. In order to do this, simply check if + // any component monitor been wrapped as a peripheral. Whilst this flag may be + // out of date, + boolean needsTerminal = false; + terminalCheck: + for( int x = 0; x < width; x++ ) + { + for( int y = 0; y < height; y++ ) + { + TileMonitor monitor = getNeighbour( x, y ); + if( monitor != null && monitor.peripheral != null ) + { + needsTerminal = true; + break terminalCheck; + } + } + } + + // Either delete the current monitor or sync a new one. + if( needsTerminal ) + { + if( m_serverMonitor == null ) m_serverMonitor = new ServerMonitor( advanced, this ); + } + else + { + m_serverMonitor = null; + } + + // Update the terminal's width and height and rebuild it. This ensures the monitor + // is consistent when syncing it to other monitors. + if( m_serverMonitor != null ) m_serverMonitor.rebuild(); + + // Update the other monitors, setting coordinates, dimensions and the server terminal + for( int x = 0; x < width; x++ ) + { + for( int y = 0; y < height; y++ ) + { + TileMonitor monitor = getNeighbour( x, y ); + if( monitor == null ) continue; + + monitor.m_xIndex = x; + monitor.m_yIndex = y; + monitor.m_width = width; + monitor.m_height = height; + monitor.m_serverMonitor = m_serverMonitor; + monitor.updateBlockState(); + monitor.updateBlock(); + } + } + } + + private boolean mergeLeft() + { + TileMonitor left = getNeighbour( -1, 0 ); + if( left == null || left.m_yIndex != 0 || left.m_height != m_height ) return false; + + int width = left.m_width + m_width; + if( width > ComputerCraft.monitorWidth ) return false; + + TileMonitor origin = left.getOrigin(); + if( origin != null ) origin.resize( width, m_height ); + left.expand(); + return true; + } + + private boolean mergeRight() + { + TileMonitor right = getNeighbour( m_width, 0 ); + if( right == null || right.m_yIndex != 0 || right.m_height != m_height ) return false; + + int width = m_width + right.m_width; + if( width > ComputerCraft.monitorWidth ) return false; + + TileMonitor origin = getOrigin(); + if( origin != null ) origin.resize( width, m_height ); + expand(); + return true; + } + + private boolean mergeUp() + { + TileMonitor above = getNeighbour( 0, m_height ); + if( above == null || above.m_xIndex != 0 || above.m_width != m_width ) return false; + + int height = above.m_height + m_height; + if( height > ComputerCraft.monitorHeight ) return false; + + TileMonitor origin = getOrigin(); + if( origin != null ) origin.resize( m_width, height ); + expand(); + return true; + } + + private boolean mergeDown() + { + TileMonitor below = getNeighbour( 0, -1 ); + if( below == null || below.m_xIndex != 0 || below.m_width != m_width ) return false; + + int height = m_height + below.m_height; + if( height > ComputerCraft.monitorHeight ) return false; + + TileMonitor origin = below.getOrigin(); + if( origin != null ) origin.resize( m_width, height ); + below.expand(); + return true; + } + + @SuppressWarnings( "StatementWithEmptyBody" ) + void expand() + { + while( mergeLeft() || mergeRight() || mergeUp() || mergeDown() ) ; + } + + void contractNeighbours() + { + visiting = true; + if( m_xIndex > 0 ) + { + TileMonitor left = getNeighbour( m_xIndex - 1, m_yIndex ); + if( left != null ) left.contract(); + } + if( m_xIndex + 1 < m_width ) + { + TileMonitor right = getNeighbour( m_xIndex + 1, m_yIndex ); + if( right != null ) right.contract(); + } + if( m_yIndex > 0 ) + { + TileMonitor below = getNeighbour( m_xIndex, m_yIndex - 1 ); + if( below != null ) below.contract(); + } + if( m_yIndex + 1 < m_height ) + { + TileMonitor above = getNeighbour( m_xIndex, m_yIndex + 1 ); + if( above != null ) above.contract(); + } + visiting = false; + } + + void contract() + { + int height = m_height; + int width = m_width; + + TileMonitor origin = getOrigin(); + if( origin == null ) + { + TileMonitor right = width > 1 ? getNeighbour( 1, 0 ) : null; + TileMonitor below = height > 1 ? getNeighbour( 0, 1 ) : null; + + if( right != null ) right.resize( width - 1, 1 ); + if( below != null ) below.resize( width, height - 1 ); + if( right != null ) right.expand(); + if( below != null ) below.expand(); + + return; + } + + for( int y = 0; y < height; y++ ) + { + for( int x = 0; x < width; x++ ) + { + TileMonitor monitor = origin.getNeighbour( x, y ); + if( monitor != null ) continue; + + // Decompose + TileMonitor above = null; + TileMonitor left = null; + TileMonitor right = null; + TileMonitor below = null; + + if( y > 0 ) + { + above = origin; + above.resize( width, y ); + } + if( x > 0 ) + { + left = origin.getNeighbour( 0, y ); + left.resize( x, 1 ); + } + if( x + 1 < width ) + { + right = origin.getNeighbour( x + 1, y ); + right.resize( width - (x + 1), 1 ); + } + if( y + 1 < height ) + { + below = origin.getNeighbour( 0, y + 1 ); + below.resize( width, height - (y + 1) ); + } + + // Re-expand + if( above != null ) above.expand(); + if( left != null ) left.expand(); + if( right != null ) right.expand(); + if( below != null ) below.expand(); + return; + } + } + } + + private void monitorTouched( float xPos, float yPos, float zPos ) + { + XYPair pair = XYPair + .of( xPos, yPos, zPos, getDirection(), getOrientation() ) + .add( m_xIndex, m_height - m_yIndex - 1 ); + + if( pair.x > m_width - RENDER_BORDER || pair.y > m_height - RENDER_BORDER || pair.x < RENDER_BORDER || pair.y < RENDER_BORDER ) + { + return; + } + + ServerTerminal serverTerminal = getServerMonitor(); + if( serverTerminal == null || !serverTerminal.isColour() ) return; + + Terminal originTerminal = serverTerminal.getTerminal(); + if( originTerminal == null ) return; + + double xCharWidth = (m_width - (RENDER_BORDER + RENDER_MARGIN) * 2.0) / originTerminal.getWidth(); + double yCharHeight = (m_height - (RENDER_BORDER + RENDER_MARGIN) * 2.0) / originTerminal.getHeight(); + + int xCharPos = (int) Math.min( originTerminal.getWidth(), Math.max( (pair.x - RENDER_BORDER - RENDER_MARGIN) / xCharWidth + 1.0, 1.0 ) ); + int yCharPos = (int) Math.min( originTerminal.getHeight(), Math.max( (pair.y - RENDER_BORDER - RENDER_MARGIN) / yCharHeight + 1.0, 1.0 ) ); + + for( int y = 0; y < m_height; y++ ) + { + for( int x = 0; x < m_width; x++ ) + { + TileMonitor monitor = getNeighbour( x, y ); + if( monitor == null ) continue; + + for( IComputerAccess computer : monitor.m_computers ) + { + computer.queueEvent( "monitor_touch", computer.getAttachmentName(), xCharPos, yCharPos ); + } + } + } + } + // endregion + + void addComputer( IComputerAccess computer ) + { + m_computers.add( computer ); + } + + void removeComputer( IComputerAccess computer ) + { + m_computers.remove( computer ); + } + + @Nonnull + @Override + public Box getRenderBoundingBox() + { + TileMonitor start = getNeighbour( 0, 0 ); + TileMonitor end = getNeighbour( m_width - 1, m_height - 1 ); + if( start != null && end != null ) + { + BlockPos startPos = start.getPos(); + BlockPos endPos = end.getPos(); + int minX = Math.min( startPos.getX(), endPos.getX() ); + int minY = Math.min( startPos.getY(), endPos.getY() ); + int minZ = Math.min( startPos.getZ(), endPos.getZ() ); + int maxX = Math.max( startPos.getX(), endPos.getX() ) + 1; + int maxY = Math.max( startPos.getY(), endPos.getY() ) + 1; + int maxZ = Math.max( startPos.getZ(), endPos.getZ() ) + 1; + return new Box( minX, minY, minZ, maxX, maxY, maxZ ); + } + else + { + BlockPos pos = getPos(); + return new Box( pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1 ); + } + } + + @Override + public double getSquaredRenderDistance() + { + return ComputerCraft.monitorDistanceSq; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/XYPair.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/XYPair.java new file mode 100644 index 000000000..1efa1e2d3 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/XYPair.java @@ -0,0 +1,73 @@ +/* + * 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.monitor; + +import net.minecraft.util.math.Direction; + +public class XYPair +{ + public final float x; + public final float y; + + public XYPair( float x, float y ) + { + this.x = x; + this.y = y; + } + + public XYPair add( float x, float y ) + { + return new XYPair( this.x + x, this.y + y ); + } + + public static XYPair of( float xPos, float yPos, float zPos, Direction facing, Direction orientation ) + { + switch( orientation ) + { + case NORTH: + switch( facing ) + { + case NORTH: + return new XYPair( 1 - xPos, 1 - yPos ); + case SOUTH: + return new XYPair( xPos, 1 - yPos ); + case WEST: + return new XYPair( zPos, 1 - yPos ); + case EAST: + return new XYPair( 1 - zPos, 1 - yPos ); + } + break; + case DOWN: + switch( facing ) + { + case NORTH: + return new XYPair( 1 - xPos, zPos ); + case SOUTH: + return new XYPair( xPos, 1 - zPos ); + case WEST: + return new XYPair( zPos, xPos ); + case EAST: + return new XYPair( 1 - zPos, 1 - xPos ); + } + break; + case UP: + switch( facing ) + { + case NORTH: + return new XYPair( 1 - xPos, 1 - zPos ); + case SOUTH: + return new XYPair( xPos, zPos ); + case WEST: + return new XYPair( zPos, 1 - xPos ); + case EAST: + return new XYPair( 1 - zPos, xPos ); + } + break; + } + + return new XYPair( xPos, zPos ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/BlockPrinter.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/BlockPrinter.java new file mode 100644 index 000000000..31d011850 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/BlockPrinter.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.printer; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.common.BlockGeneric; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.stat.Stats; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.BooleanProperty; +import net.minecraft.state.property.DirectionProperty; +import net.minecraft.state.property.Properties; +import net.minecraft.util.Nameable; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class BlockPrinter extends BlockGeneric +{ + private static final DirectionProperty FACING = Properties.HORIZONTAL_FACING; + static final BooleanProperty TOP = BooleanProperty.of( "top" ); + static final BooleanProperty BOTTOM = BooleanProperty.of( "bottom" ); + + public BlockPrinter( Settings settings ) + { + super( settings, Registry.ModTiles.PRINTER ); + setDefaultState( getStateManager().getDefaultState() + .with( FACING, Direction.NORTH ) + .with( TOP, false ) + .with( BOTTOM, false ) ); + } + + @Override + protected void appendProperties( StateManager.Builder properties ) + { + properties.add( FACING, TOP, BOTTOM ); + } + + @Nullable + @Override + public BlockState getPlacementState( ItemPlacementContext placement ) + { + return getDefaultState().with( FACING, placement.getPlayerFacing().getOpposite() ); + } + + @Override + public void afterBreak( @Nonnull World world, @Nonnull PlayerEntity player, @Nonnull BlockPos pos, @Nonnull BlockState state, @Nullable BlockEntity te, @Nonnull ItemStack stack ) + { + if( te instanceof Nameable && ((Nameable) te).hasCustomName() ) + { + player.incrementStat( Stats.MINED.getOrCreateStat( this ) ); + player.addExhaustion( 0.005F ); + + ItemStack result = new ItemStack( this ); + result.setCustomName( ((Nameable) te).getCustomName() ); + dropStack( world, pos, result ); + } + else + { + super.afterBreak( world, player, pos, state, te, stack ); + } + } + + @Override + public void onPlaced( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState state, LivingEntity placer, ItemStack stack ) + { + if( stack.hasCustomName() ) + { + BlockEntity tileentity = world.getBlockEntity( pos ); + if( tileentity instanceof TilePrinter ) ((TilePrinter) tileentity).customName = stack.getName(); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/ContainerPrinter.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/ContainerPrinter.java new file mode 100644 index 000000000..dd76940f5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/ContainerPrinter.java @@ -0,0 +1,121 @@ +/* + * 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.printer; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.util.SingleIntArray; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SimpleInventory; +import net.minecraft.item.DyeItem; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.ArrayPropertyDelegate; +import net.minecraft.screen.PropertyDelegate; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.Slot; +import javax.annotation.Nonnull; + +public class ContainerPrinter extends ScreenHandler +{ + private final Inventory inventory; + private final PropertyDelegate properties; + + private ContainerPrinter( int id, PlayerInventory player, Inventory inventory, PropertyDelegate properties ) + { + super( Registry.ModContainers.PRINTER.get(), id ); + this.properties = properties; + this.inventory = inventory; + + addProperties( properties ); + + // Ink slot + addSlot( new Slot( inventory, 0, 13, 35 ) ); + + // In-tray + for( int x = 0; x < 6; x++ ) addSlot( new Slot( inventory, x + 1, 61 + x * 18, 22 ) ); + + // Out-tray + for( int x = 0; x < 6; x++ ) addSlot( new Slot( inventory, x + 7, 61 + x * 18, 49 ) ); + + // Player inv + for( int y = 0; y < 3; y++ ) + { + for( int x = 0; x < 9; x++ ) + { + addSlot( new Slot( player, x + y * 9 + 9, 8 + x * 18, 84 + y * 18 ) ); + } + } + + // Player hotbar + for( int x = 0; x < 9; x++ ) + { + addSlot( new Slot( player, x, 8 + x * 18, 142 ) ); + } + } + + public ContainerPrinter( int id, PlayerInventory player ) + { + this( id, player, new SimpleInventory( TilePrinter.SLOTS ), new ArrayPropertyDelegate( 1 ) ); + } + + public ContainerPrinter( int id, PlayerInventory player, TilePrinter printer ) + { + this( id, player, printer, (SingleIntArray) (() -> printer.isPrinting() ? 1 : 0) ); + } + + public boolean isPrinting() + { + return properties.get( 0 ) != 0; + } + + @Override + public boolean canUse( @Nonnull PlayerEntity player ) + { + return inventory.canPlayerUse( player ); + } + + @Nonnull + @Override + public ItemStack transferSlot( @Nonnull PlayerEntity player, int index ) + { + Slot slot = slots.get( index ); + if( slot == null || !slot.hasStack() ) return ItemStack.EMPTY; + ItemStack stack = slot.getStack(); + ItemStack result = stack.copy(); + if( index < 13 ) + { + // Transfer from printer to inventory + if( !insertItem( stack, 13, 49, true ) ) return ItemStack.EMPTY; + } + else + { + // Transfer from inventory to printer + if( stack.getItem() instanceof DyeItem ) + { + if( !insertItem( stack, 0, 1, false ) ) return ItemStack.EMPTY; + } + else //if is paper + { + if( !insertItem( stack, 1, 13, false ) ) return ItemStack.EMPTY; + } + } + + if( stack.isEmpty() ) + { + slot.setStack( ItemStack.EMPTY ); + } + else + { + slot.markDirty(); + } + + if( stack.getCount() == result.getCount() ) return ItemStack.EMPTY; + + slot.onTakeItem( player, stack ); + return result; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java new file mode 100644 index 000000000..7c6543b14 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java @@ -0,0 +1,188 @@ +/* + * 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.printer; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.util.StringUtil; + +import javax.annotation.Nonnull; +import java.util.Optional; + +/** + * The printer peripheral allows pages and books to be printed. + * + * @cc.module printer + */ +public class PrinterPeripheral implements IPeripheral +{ + private final TilePrinter printer; + + public PrinterPeripheral( TilePrinter printer ) + { + this.printer = printer; + } + + @Nonnull + @Override + public String getType() + { + return "printer"; + } + + // FIXME: There's a theoretical race condition here between getCurrentPage and then using the page. Ideally + // we'd lock on the page, consume it, and unlock. + + // FIXME: None of our page modification functions actually mark the tile as dirty, so the page may not be + // persisted correctly. + + /** + * Writes text to the current page. + * + * @param arguments The values to write to the page. + * @throws LuaException If any values couldn't be converted to a string, or if no page is started. + * @cc.tparam string|number ... The values to write to the page. + */ + @LuaFunction + public final void write( IArguments arguments ) throws LuaException + { + String text = StringUtil.toString( arguments.get( 0 ) ); + Terminal page = getCurrentPage(); + page.write( text ); + page.setCursorPos( page.getCursorX() + text.length(), page.getCursorY() ); + } + + /** + * Returns the current position of the cursor on the page. + * + * @return The position of the cursor. + * @throws LuaException If a page isn't being printed. + * @cc.treturn number The X position of the cursor. + * @cc.treturn number The Y position of the cursor. + */ + @LuaFunction + public final Object[] getCursorPos() throws LuaException + { + Terminal page = getCurrentPage(); + int x = page.getCursorX(); + int y = page.getCursorY(); + return new Object[] { x + 1, y + 1 }; + } + + /** + * Sets the position of the cursor on the page. + * + * @param x The X coordinate to set the cursor at. + * @param y The Y coordinate to set the cursor at. + * @throws LuaException If a page isn't being printed. + */ + @LuaFunction + public final void setCursorPos( int x, int y ) throws LuaException + { + Terminal page = getCurrentPage(); + page.setCursorPos( x - 1, y - 1 ); + } + + /** + * Returns the size of the current page. + * + * @return The size of the page. + * @throws LuaException If a page isn't being printed. + * @cc.treturn number The width of the page. + * @cc.treturn number The height of the page. + */ + @LuaFunction + public final Object[] getPageSize() throws LuaException + { + Terminal page = getCurrentPage(); + int width = page.getWidth(); + int height = page.getHeight(); + return new Object[] { width, height }; + } + + /** + * Starts printing a new page. + * + * @return Whether a new page could be started. + */ + @LuaFunction( mainThread = true ) + public final boolean newPage() + { + return printer.startNewPage(); + } + + /** + * Finalizes printing of the current page and outputs it to the tray. + * + * @return Whether the page could be successfully finished. + * @throws LuaException If a page isn't being printed. + */ + @LuaFunction( mainThread = true ) + public final boolean endPage() throws LuaException + { + getCurrentPage(); + return printer.endCurrentPage(); + } + + /** + * Sets the title of the current page. + * + * @param title The title to set for the page. + * @throws LuaException If a page isn't being printed. + */ + @LuaFunction + public final void setPageTitle( Optional title ) throws LuaException + { + getCurrentPage(); + printer.setPageTitle( StringUtil.normaliseLabel( title.orElse( "" ) ) ); + } + + /** + * Returns the amount of ink left in the printer. + * + * @return The amount of ink available to print with. + */ + @LuaFunction + public final int getInkLevel() + { + return printer.getInkLevel(); + } + + /** + * Returns the amount of paper left in the printer. + * + * @return The amount of paper available to print with. + */ + @LuaFunction + public final int getPaperLevel() + { + return printer.getPaperLevel(); + } + + @Override + public boolean equals( IPeripheral other ) + { + return other instanceof PrinterPeripheral && ((PrinterPeripheral) other).printer == printer; + } + + @Nonnull + @Override + public Object getTarget() + { + return printer; + } + + @Nonnull + private Terminal getCurrentPage() throws LuaException + { + Terminal currentPage = printer.getCurrentPage(); + if( currentPage == null ) throw new LuaException( "Page not started" ); + return currentPage; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java new file mode 100644 index 000000000..9c893e39a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java @@ -0,0 +1,494 @@ +/* + * 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.printer; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.media.items.ItemPrintout; +import dan200.computercraft.shared.util.*; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.inventory.Inventories; +import net.minecraft.item.*; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.*; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.fml.network.NetworkHooks; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.wrapper.InvWrapper; +import net.minecraftforge.items.wrapper.SidedInvWrapper; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; +import static net.minecraftforge.items.CapabilityItemHandler.ITEM_HANDLER_CAPABILITY; + +public final class TilePrinter extends TileGeneric implements DefaultSidedInventory, Nameable, NamedScreenHandlerFactory +{ + private static final String NBT_NAME = "CustomName"; + private static final String NBT_PRINTING = "Printing"; + private static final String NBT_PAGE_TITLE = "PageTitle"; + + static final int SLOTS = 13; + + private static final int[] BOTTOM_SLOTS = new int[] { 7, 8, 9, 10, 11, 12 }; + private static final int[] TOP_SLOTS = new int[] { 1, 2, 3, 4, 5, 6 }; + private static final int[] SIDE_SLOTS = new int[] { 0 }; + + Text customName; + + private final DefaultedList m_inventory = DefaultedList.ofSize( SLOTS, ItemStack.EMPTY ); + private final SidedCaps itemHandlerCaps = + SidedCaps.ofNullable( facing -> facing == null ? new InvWrapper( this ) : new SidedInvWrapper( this, facing ) ); + private LazyOptional peripheralCap; + + private final Terminal m_page = new Terminal( ItemPrintout.LINE_MAX_LENGTH, ItemPrintout.LINES_PER_PAGE ); + private String m_pageTitle = ""; + private boolean m_printing = false; + + public TilePrinter( BlockEntityType type ) + { + super( type ); + } + + @Override + public void destroy() + { + ejectContents(); + } + + @Override + protected void invalidateCaps() + { + super.invalidateCaps(); + itemHandlerCaps.invalidate(); + peripheralCap = CapabilityUtil.invalidate( peripheralCap ); + } + + @Nonnull + @Override + public ActionResult onActivate( PlayerEntity player, Hand hand, BlockHitResult hit ) + { + if( player.isInSneakingPose() ) return ActionResult.PASS; + + if( !getWorld().isClient ) NetworkHooks.openGui( (ServerPlayerEntity) player, this ); + return ActionResult.SUCCESS; + } + + @Override + public void fromTag( @Nonnull BlockState state, @Nonnull CompoundTag nbt ) + { + super.fromTag( state, nbt ); + + customName = nbt.contains( NBT_NAME ) ? Text.Serializer.fromJson( nbt.getString( NBT_NAME ) ) : null; + + // Read page + synchronized( m_page ) + { + m_printing = nbt.getBoolean( NBT_PRINTING ); + m_pageTitle = nbt.getString( NBT_PAGE_TITLE ); + m_page.readFromNBT( nbt ); + } + + // Read inventory + Inventories.fromTag( nbt, m_inventory ); + } + + @Nonnull + @Override + public CompoundTag toTag( @Nonnull CompoundTag nbt ) + { + if( customName != null ) nbt.putString( NBT_NAME, Text.Serializer.toJson( customName ) ); + + // Write page + synchronized( m_page ) + { + nbt.putBoolean( NBT_PRINTING, m_printing ); + nbt.putString( NBT_PAGE_TITLE, m_pageTitle ); + m_page.writeToNBT( nbt ); + } + + // Write inventory + Inventories.toTag( nbt, m_inventory ); + + return super.toTag( nbt ); + } + + boolean isPrinting() + { + return m_printing; + } + + // IInventory implementation + @Override + public int size() + { + return m_inventory.size(); + } + + @Override + public boolean isEmpty() + { + for( ItemStack stack : m_inventory ) + { + if( !stack.isEmpty() ) return false; + } + return true; + } + + @Nonnull + @Override + public ItemStack getStack( int slot ) + { + return m_inventory.get( slot ); + } + + @Nonnull + @Override + public ItemStack removeStack( int slot ) + { + ItemStack result = m_inventory.get( slot ); + m_inventory.set( slot, ItemStack.EMPTY ); + markDirty(); + updateBlockState(); + return result; + } + + @Nonnull + @Override + public ItemStack removeStack( int slot, int count ) + { + ItemStack stack = m_inventory.get( slot ); + if( stack.isEmpty() ) return ItemStack.EMPTY; + + if( stack.getCount() <= count ) + { + setStack( slot, ItemStack.EMPTY ); + return stack; + } + + ItemStack part = stack.split( count ); + if( m_inventory.get( slot ).isEmpty() ) + { + m_inventory.set( slot, ItemStack.EMPTY ); + updateBlockState(); + } + markDirty(); + return part; + } + + @Override + public void setStack( int slot, @Nonnull ItemStack stack ) + { + m_inventory.set( slot, stack ); + markDirty(); + updateBlockState(); + } + + @Override + public void clear() + { + for( int i = 0; i < m_inventory.size(); i++ ) m_inventory.set( i, ItemStack.EMPTY ); + markDirty(); + updateBlockState(); + } + + @Override + public boolean isValid( int slot, @Nonnull ItemStack stack ) + { + if( slot == 0 ) + { + return isInk( stack ); + } + else if( slot >= TOP_SLOTS[0] && slot <= TOP_SLOTS[TOP_SLOTS.length - 1] ) + { + return isPaper( stack ); + } + else + { + return false; + } + } + + @Override + public boolean canPlayerUse( @Nonnull PlayerEntity playerEntity ) + { + return isUsable( playerEntity, false ); + } + + // ISidedInventory implementation + + @Nonnull + @Override + public int[] getAvailableSlots( @Nonnull Direction side ) + { + switch( side ) + { + case DOWN: // Bottom (Out tray) + return BOTTOM_SLOTS; + case UP: // Top (In tray) + return TOP_SLOTS; + default: // Sides (Ink) + return SIDE_SLOTS; + } + } + + @Nullable + Terminal getCurrentPage() + { + synchronized( m_page ) + { + return m_printing ? m_page : null; + } + } + + boolean startNewPage() + { + synchronized( m_page ) + { + if( !canInputPage() ) return false; + if( m_printing && !outputPage() ) return false; + return inputPage(); + } + } + + boolean endCurrentPage() + { + synchronized( m_page ) + { + return m_printing && outputPage(); + } + } + + int getInkLevel() + { + ItemStack inkStack = m_inventory.get( 0 ); + return isInk( inkStack ) ? inkStack.getCount() : 0; + } + + int getPaperLevel() + { + int count = 0; + for( int i = 1; i < 7; i++ ) + { + ItemStack paperStack = m_inventory.get( i ); + if( isPaper( paperStack ) ) count += paperStack.getCount(); + } + return count; + } + + void setPageTitle( String title ) + { + synchronized( m_page ) + { + if( m_printing ) m_pageTitle = title; + } + } + + private static boolean isInk( @Nonnull ItemStack stack ) + { + return stack.getItem() instanceof DyeItem; + } + + private static boolean isPaper( @Nonnull ItemStack stack ) + { + Item item = stack.getItem(); + return item == Items.PAPER + || (item instanceof ItemPrintout && ((ItemPrintout) item).getType() == ItemPrintout.Type.PAGE); + } + + private boolean canInputPage() + { + ItemStack inkStack = m_inventory.get( 0 ); + return !inkStack.isEmpty() && isInk( inkStack ) && getPaperLevel() > 0; + } + + private boolean inputPage() + { + ItemStack inkStack = m_inventory.get( 0 ); + if( !isInk( inkStack ) ) return false; + + for( int i = 1; i < 7; i++ ) + { + ItemStack paperStack = m_inventory.get( i ); + if( paperStack.isEmpty() || !isPaper( paperStack ) ) continue; + + // Setup the new page + DyeColor dye = ColourUtils.getStackColour( inkStack ); + m_page.setTextColour( dye != null ? dye.getId() : 15 ); + + m_page.clear(); + if( paperStack.getItem() instanceof ItemPrintout ) + { + m_pageTitle = ItemPrintout.getTitle( paperStack ); + String[] text = ItemPrintout.getText( paperStack ); + String[] textColour = ItemPrintout.getColours( paperStack ); + for( int y = 0; y < m_page.getHeight(); y++ ) + { + m_page.setLine( y, text[y], textColour[y], "" ); + } + } + else + { + m_pageTitle = ""; + } + m_page.setCursorPos( 0, 0 ); + + // Decrement ink + inkStack.decrement( 1 ); + if( inkStack.isEmpty() ) m_inventory.set( 0, ItemStack.EMPTY ); + + // Decrement paper + paperStack.decrement( 1 ); + if( paperStack.isEmpty() ) + { + m_inventory.set( i, ItemStack.EMPTY ); + updateBlockState(); + } + + markDirty(); + m_printing = true; + return true; + } + return false; + } + + private boolean outputPage() + { + int height = m_page.getHeight(); + String[] lines = new String[height]; + String[] colours = new String[height]; + for( int i = 0; i < height; i++ ) + { + lines[i] = m_page.getLine( i ).toString(); + colours[i] = m_page.getTextColourLine( i ).toString(); + } + + ItemStack stack = ItemPrintout.createSingleFromTitleAndText( m_pageTitle, lines, colours ); + for( int slot : BOTTOM_SLOTS ) + { + if( m_inventory.get( slot ).isEmpty() ) + { + setStack( slot, stack ); + m_printing = false; + return true; + } + } + return false; + } + + private void ejectContents() + { + for( int i = 0; i < 13; i++ ) + { + ItemStack stack = m_inventory.get( i ); + if( !stack.isEmpty() ) + { + // Remove the stack from the inventory + setStack( i, ItemStack.EMPTY ); + + // Spawn the item in the world + WorldUtil.dropItemStack( stack, getWorld(), Vec3d.of( getPos() ).add( 0.5, 0.75, 0.5 ) ); + } + } + } + + private void updateBlockState() + { + boolean top = false, bottom = false; + for( int i = 1; i < 7; i++ ) + { + ItemStack stack = m_inventory.get( i ); + if( !stack.isEmpty() && isPaper( stack ) ) + { + top = true; + break; + } + } + for( int i = 7; i < 13; i++ ) + { + ItemStack stack = m_inventory.get( i ); + if( !stack.isEmpty() && isPaper( stack ) ) + { + bottom = true; + break; + } + } + + updateBlockState( top, bottom ); + } + + private void updateBlockState( boolean top, boolean bottom ) + { + if( removed ) return; + + BlockState state = getCachedState(); + if( state.get( BlockPrinter.TOP ) == top & state.get( BlockPrinter.BOTTOM ) == bottom ) return; + + getWorld().setBlockState( getPos(), state.with( BlockPrinter.TOP, top ).with( BlockPrinter.BOTTOM, bottom ) ); + } + + @Nonnull + @Override + public LazyOptional getCapability( @Nonnull Capability capability, @Nullable Direction facing ) + { + if( capability == ITEM_HANDLER_CAPABILITY ) return itemHandlerCaps.get( facing ).cast(); + if( capability == CAPABILITY_PERIPHERAL ) + { + if( peripheralCap == null ) peripheralCap = LazyOptional.of( () -> new PrinterPeripheral( this ) ); + return peripheralCap.cast(); + } + + return super.getCapability( capability, facing ); + } + + @Override + public boolean hasCustomName() + { + return customName != null; + } + + @Nullable + @Override + public Text getCustomName() + { + return customName; + } + + @Nonnull + @Override + public Text getName() + { + return customName != null ? customName : new TranslatableText( getCachedState().getBlock().getTranslationKey() ); + } + + @Override + public Text getDisplayName() + { + return Nameable.super.getDisplayName(); + } + + @Nonnull + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory inventory, @Nonnull PlayerEntity player ) + { + return new ContainerPrinter( id, inventory, this ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/BlockSpeaker.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/BlockSpeaker.java new file mode 100644 index 000000000..8305c06b6 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/BlockSpeaker.java @@ -0,0 +1,42 @@ +/* + * 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.speaker; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.common.BlockGeneric; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.DirectionProperty; +import net.minecraft.state.property.Properties; +import net.minecraft.util.math.Direction; +import javax.annotation.Nullable; + +public class BlockSpeaker extends BlockGeneric +{ + private static final DirectionProperty FACING = Properties.HORIZONTAL_FACING; + + public BlockSpeaker( Settings settings ) + { + super( settings, Registry.ModTiles.SPEAKER ); + setDefaultState( getStateManager().getDefaultState() + .with( FACING, Direction.NORTH ) ); + } + + @Override + protected void appendProperties( StateManager.Builder properties ) + { + properties.add( FACING ); + } + + @Nullable + @Override + public BlockState getPlacementState( ItemPlacementContext placement ) + { + return getDefaultState().with( FACING, placement.getPlayerFacing().getOpposite() ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java new file mode 100644 index 000000000..920f80308 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java @@ -0,0 +1,164 @@ +/* + * 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.speaker; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.block.enums.Instrument; +import net.minecraft.network.packet.s2c.play.PlaySoundIdS2CPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.sound.SoundCategory; +import net.minecraft.util.Identifier; +import net.minecraft.util.InvalidIdentifierException; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import static dan200.computercraft.api.lua.LuaValues.checkFinite; + +/** + * Speakers allow playing notes and other sounds. + * + * @cc.module speaker + */ +public abstract class SpeakerPeripheral implements IPeripheral +{ + private long m_clock = 0; + private long m_lastPlayTime = 0; + private final AtomicInteger m_notesThisTick = new AtomicInteger(); + + public void update() + { + m_clock++; + m_notesThisTick.set( 0 ); + } + + public abstract World getWorld(); + + public abstract Vec3d getPosition(); + + public boolean madeSound( long ticks ) + { + return m_clock - m_lastPlayTime <= ticks; + } + + @Nonnull + @Override + public String getType() + { + return "speaker"; + } + + /** + * Plays a sound through the speaker. + * + * This plays sounds similar to the {@code /playsound} command in Minecraft. + * It takes the namespaced path of a sound (e.g. {@code minecraft:block.note_block.harp}) + * with an optional volume and speed multiplier, and plays it through the speaker. + * + * @param context The Lua context + * @param name The name of the sound to play. + * @param volumeA The volume to play the sound at, from 0.0 to 3.0. Defaults to 1.0. + * @param pitchA The speed to play the sound at, from 0.5 to 2.0. Defaults to 1.0. + * @return Whether the sound could be played. + * @throws LuaException If the sound name couldn't be decoded. + */ + @LuaFunction + public final boolean playSound( ILuaContext context, String name, Optional volumeA, Optional pitchA ) throws LuaException + { + float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) ); + float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) ); + + Identifier identifier; + try + { + identifier = new Identifier( name ); + } + catch( InvalidIdentifierException e ) + { + throw new LuaException( "Malformed sound name '" + name + "' " ); + } + + return playSound( context, identifier, volume, pitch, false ); + } + + /** + * Plays a note block note through the speaker. + * + * This takes the name of a note to play, as well as optionally the volume + * and pitch to play the note at. + * + * The pitch argument uses semitones as the unit. This directly maps to the + * number of clicks on a note block. For reference, 0, 12, and 24 map to F#, + * and 6 and 18 map to C. + * + * @param context The Lua context + * @param name The name of the note to play. + * @param volumeA The volume to play the note at, from 0.0 to 3.0. Defaults to 1.0. + * @param pitchA The pitch to play the note at in semitones, from 0 to 24. Defaults to 12. + * @return Whether the note could be played. + * @throws LuaException If the instrument doesn't exist. + */ + @LuaFunction + public final synchronized boolean playNote( ILuaContext context, String name, Optional volumeA, Optional pitchA ) throws LuaException + { + float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) ); + float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) ); + + Instrument instrument = null; + for( Instrument testInstrument : Instrument.values() ) + { + if( testInstrument.asString().equalsIgnoreCase( name ) ) + { + instrument = testInstrument; + break; + } + } + + // Check if the note exists + if( instrument == null ) throw new LuaException( "Invalid instrument, \"" + name + "\"!" ); + + // If the resource location for note block notes changes, this method call will need to be updated + boolean success = playSound( context, instrument.getSound().getRegistryName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ), true ); + if( success ) m_notesThisTick.incrementAndGet(); + return success; + } + + private synchronized boolean playSound( ILuaContext context, Identifier name, float volume, float pitch, boolean isNote ) throws LuaException + { + if( m_clock - m_lastPlayTime < TileSpeaker.MIN_TICKS_BETWEEN_SOUNDS && + (!isNote || m_clock - m_lastPlayTime != 0 || m_notesThisTick.get() >= ComputerCraft.maxNotesPerTick) ) + { + // Rate limiting occurs when we've already played a sound within the last tick, or we've + // played more notes than allowable within the current tick. + return false; + } + + World world = getWorld(); + Vec3d pos = getPosition(); + + context.issueMainThreadTask( () -> { + MinecraftServer server = world.getServer(); + if( server == null ) return null; + + float adjVolume = Math.min( volume, 3.0f ); + server.getPlayerManager().sendToAround( + null, pos.x, pos.y, pos.z, adjVolume > 1.0f ? 16 * adjVolume : 16.0, world.getRegistryKey(), + new PlaySoundIdS2CPacket( name, SoundCategory.RECORDS, pos, adjVolume, pitch ) + ); + return null; + } ); + + m_lastPlayTime = m_clock; + return true; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java new file mode 100644 index 000000000..35550694d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java @@ -0,0 +1,92 @@ +/* + * 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.speaker; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.util.CapabilityUtil; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.util.Tickable; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; + +public class TileSpeaker extends TileGeneric implements Tickable +{ + public static final int MIN_TICKS_BETWEEN_SOUNDS = 1; + + private final SpeakerPeripheral peripheral; + private LazyOptional peripheralCap; + + public TileSpeaker( BlockEntityType type ) + { + super( type ); + peripheral = new Peripheral( this ); + } + + @Override + public void tick() + { + peripheral.update(); + } + + @Nonnull + @Override + public LazyOptional getCapability( @Nonnull Capability cap, @Nullable Direction side ) + { + if( cap == CAPABILITY_PERIPHERAL ) + { + if( peripheralCap == null ) peripheralCap = LazyOptional.of( () -> peripheral ); + return peripheralCap.cast(); + } + + return super.getCapability( cap, side ); + } + + @Override + protected void invalidateCaps() + { + super.invalidateCaps(); + peripheralCap = CapabilityUtil.invalidate( peripheralCap ); + } + + private static final class Peripheral extends SpeakerPeripheral + { + private final TileSpeaker speaker; + + private Peripheral( TileSpeaker speaker ) + { + this.speaker = speaker; + } + + @Override + public World getWorld() + { + return speaker.getWorld(); + } + + @Override + public Vec3d getPosition() + { + BlockPos pos = speaker.getPos(); + return new Vec3d( pos.getX(), pos.getY(), pos.getZ() ); + } + + @Override + public boolean equals( @Nullable IPeripheral other ) + { + return this == other || (other instanceof Peripheral && speaker == ((Peripheral) other).speaker); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java b/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java new file mode 100644 index 000000000..12ad5b326 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java @@ -0,0 +1,156 @@ +/* + * 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.pocket.apis; + +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.shared.PocketUpgrades; +import dan200.computercraft.shared.pocket.core.PocketServerComputer; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.collection.DefaultedList; +import net.minecraftforge.items.wrapper.PlayerMainInvWrapper; + +/** + * Control the current pocket computer, adding or removing upgrades. + * + * This API is only available on pocket computers. As such, you may use its presence to determine what kind of computer + * you are using: + * + *
+ * if pocket then
+ *   print("On a pocket computer")
+ * else
+ *   print("On something else")
+ * end
+ * 
+ * + * @cc.module pocket + */ +public class PocketAPI implements ILuaAPI +{ + private final PocketServerComputer computer; + + public PocketAPI( PocketServerComputer computer ) + { + this.computer = computer; + } + + @Override + public String[] getNames() + { + return new String[] { "pocket" }; + } + + /** + * Search the player's inventory for another upgrade, replacing the existing one with that item if found. + * + * This inventory search starts from the player's currently selected slot, allowing you to prioritise upgrades. + * + * @return The result of equipping. + * @cc.treturn boolean If an item was equipped. + * @cc.treturn string|nil The reason an item was not equipped. + */ + @LuaFunction( mainThread = true ) + public final Object[] equipBack() + { + Entity entity = computer.getEntity(); + if( !(entity instanceof PlayerEntity) ) return new Object[] { false, "Cannot find player" }; + PlayerEntity player = (PlayerEntity) entity; + PlayerInventory inventory = player.inventory; + IPocketUpgrade previousUpgrade = computer.getUpgrade(); + + // Attempt to find the upgrade, starting in the main segment, and then looking in the opposite + // one. We start from the position the item is currently in and loop round to the start. + IPocketUpgrade newUpgrade = findUpgrade( inventory.main, inventory.selectedSlot, previousUpgrade ); + if( newUpgrade == null ) + { + newUpgrade = findUpgrade( inventory.offHand, 0, previousUpgrade ); + } + if( newUpgrade == null ) return new Object[] { false, "Cannot find a valid upgrade" }; + + // Remove the current upgrade + if( previousUpgrade != null ) + { + ItemStack stack = previousUpgrade.getCraftingItem(); + if( !stack.isEmpty() ) + { + stack = InventoryUtil.storeItems( stack, new PlayerMainInvWrapper( inventory ), inventory.selectedSlot ); + if( !stack.isEmpty() ) + { + WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.getPos() ); + } + } + } + + // Set the new upgrade + computer.setUpgrade( newUpgrade ); + + return new Object[] { true }; + } + + /** + * Remove the pocket computer's current upgrade. + * + * @return The result of unequipping. + * @cc.treturn boolean If the upgrade was unequipped. + * @cc.treturn string|nil The reason an upgrade was not unequipped. + */ + @LuaFunction( mainThread = true ) + public final Object[] unequipBack() + { + Entity entity = computer.getEntity(); + if( !(entity instanceof PlayerEntity) ) return new Object[] { false, "Cannot find player" }; + PlayerEntity player = (PlayerEntity) entity; + PlayerInventory inventory = player.inventory; + IPocketUpgrade previousUpgrade = computer.getUpgrade(); + + if( previousUpgrade == null ) return new Object[] { false, "Nothing to unequip" }; + + computer.setUpgrade( null ); + + ItemStack stack = previousUpgrade.getCraftingItem(); + if( !stack.isEmpty() ) + { + stack = InventoryUtil.storeItems( stack, new PlayerMainInvWrapper( inventory ), inventory.selectedSlot ); + if( stack.isEmpty() ) + { + WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.getPos() ); + } + } + + return new Object[] { true }; + } + + private static IPocketUpgrade findUpgrade( DefaultedList inv, int start, IPocketUpgrade previous ) + { + for( int i = 0; i < inv.size(); i++ ) + { + ItemStack invStack = inv.get( (i + start) % inv.size() ); + if( !invStack.isEmpty() ) + { + IPocketUpgrade newUpgrade = PocketUpgrades.get( invStack ); + + if( newUpgrade != null && newUpgrade != previous ) + { + // Consume an item from this stack and exit the loop + invStack = invStack.copy(); + invStack.decrement( 1 ); + inv.set( (i + start) % inv.size(), invStack.isEmpty() ? ItemStack.EMPTY : invStack ); + + return newUpgrade; + } + } + } + + return null; + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java b/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java new file mode 100644 index 000000000..d2a66473d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java @@ -0,0 +1,197 @@ +/* + * 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.pocket.core; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.pocket.IPocketAccess; +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.shared.common.IColouredItem; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; +import net.minecraftforge.common.util.Constants; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.Map; + +import static dan200.computercraft.shared.pocket.items.ItemPocketComputer.NBT_LIGHT; + +public class PocketServerComputer extends ServerComputer implements IPocketAccess +{ + private IPocketUpgrade m_upgrade; + private Entity m_entity; + private ItemStack m_stack; + + public PocketServerComputer( World world, int computerID, String label, int instanceID, ComputerFamily family ) + { + super( world, computerID, label, instanceID, family, ComputerCraft.pocketTermWidth, ComputerCraft.pocketTermHeight ); + } + + @Nullable + @Override + public Entity getEntity() + { + Entity entity = m_entity; + if( entity == null || m_stack == null || !entity.isAlive() ) return null; + + if( entity instanceof PlayerEntity ) + { + PlayerInventory inventory = ((PlayerEntity) entity).inventory; + return inventory.main.contains( m_stack ) || inventory.offHand.contains( m_stack ) ? entity : null; + } + else if( entity instanceof LivingEntity ) + { + LivingEntity living = (LivingEntity) entity; + return living.getMainHandStack() == m_stack || living.getOffHandStack() == m_stack ? entity : null; + } + else + { + return null; + } + } + + @Override + public int getColour() + { + return IColouredItem.getColourBasic( m_stack ); + } + + @Override + public void setColour( int colour ) + { + IColouredItem.setColourBasic( m_stack, colour ); + updateUpgradeNBTData(); + } + + @Override + public int getLight() + { + CompoundTag tag = getUserData(); + return tag.contains( NBT_LIGHT, Constants.NBT.TAG_ANY_NUMERIC ) ? tag.getInt( NBT_LIGHT ) : -1; + } + + @Override + public void setLight( int colour ) + { + CompoundTag tag = getUserData(); + if( colour >= 0 && colour <= 0xFFFFFF ) + { + if( !tag.contains( NBT_LIGHT, Constants.NBT.TAG_ANY_NUMERIC ) || tag.getInt( NBT_LIGHT ) != colour ) + { + tag.putInt( NBT_LIGHT, colour ); + updateUserData(); + } + } + else if( tag.contains( NBT_LIGHT, Constants.NBT.TAG_ANY_NUMERIC ) ) + { + tag.remove( NBT_LIGHT ); + updateUserData(); + } + } + + @Nonnull + @Override + public CompoundTag getUpgradeNBTData() + { + return ItemPocketComputer.getUpgradeInfo( m_stack ); + } + + @Override + public void updateUpgradeNBTData() + { + if( m_entity instanceof PlayerEntity ) ((PlayerEntity) m_entity).inventory.markDirty(); + } + + @Override + public void invalidatePeripheral() + { + IPeripheral peripheral = m_upgrade == null ? null : m_upgrade.createPeripheral( this ); + setPeripheral( ComputerSide.BACK, peripheral ); + } + + @Nonnull + @Override + public Map getUpgrades() + { + return m_upgrade == null ? Collections.emptyMap() : Collections.singletonMap( m_upgrade.getUpgradeID(), getPeripheral( ComputerSide.BACK ) ); + } + + public IPocketUpgrade getUpgrade() + { + return m_upgrade; + } + + /** + * Set the upgrade for this pocket computer, also updating the item stack. + * + * Note this method is not thread safe - it must be called from the server thread. + * + * @param upgrade The new upgrade to set it to, may be {@code null}. + */ + public void setUpgrade( IPocketUpgrade upgrade ) + { + if( m_upgrade == upgrade ) return; + + synchronized( this ) + { + ItemPocketComputer.setUpgrade( m_stack, upgrade ); + updateUpgradeNBTData(); + m_upgrade = upgrade; + invalidatePeripheral(); + } + } + + public synchronized void updateValues( Entity entity, @Nonnull ItemStack stack, IPocketUpgrade upgrade ) + { + if( entity != null ) + { + setWorld( entity.getEntityWorld() ); + setPosition( entity.getBlockPos() ); + } + + // If a new entity has picked it up then rebroadcast the terminal to them + if( entity != m_entity && entity instanceof ServerPlayerEntity ) markTerminalChanged(); + + m_entity = entity; + m_stack = stack; + + if( m_upgrade != upgrade ) + { + m_upgrade = upgrade; + invalidatePeripheral(); + } + } + + @Override + public void broadcastState( boolean force ) + { + super.broadcastState( force ); + + if( (hasTerminalChanged() || force) && m_entity instanceof ServerPlayerEntity ) + { + // Broadcast the state to the current entity if they're not already interacting with it. + ServerPlayerEntity player = (ServerPlayerEntity) m_entity; + if( player.networkHandler != null && !isInteracting( player ) ) + { + NetworkHandler.sendToPlayer( player, createTerminalPacket() ); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java b/src/main/java/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java new file mode 100644 index 000000000..e830343a6 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java @@ -0,0 +1,68 @@ +/* + * 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.pocket.inventory; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.computer.inventory.ContainerComputerBase; +import dan200.computercraft.shared.network.container.ComputerContainerData; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.text.Text; +import net.minecraft.util.Hand; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class ContainerPocketComputer extends ContainerComputerBase +{ + private ContainerPocketComputer( int id, ServerComputer computer, ItemPocketComputer item, Hand hand ) + { + super( Registry.ModContainers.POCKET_COMPUTER.get(), id, p -> { + ItemStack stack = p.getStackInHand( hand ); + return stack.getItem() == item && ItemPocketComputer.getServerComputer( stack ) == computer; + }, computer, item.getFamily() ); + } + + public ContainerPocketComputer( int id, PlayerInventory player, ComputerContainerData data ) + { + super( Registry.ModContainers.POCKET_COMPUTER.get(), id, player, data ); + } + + public static class Factory implements NamedScreenHandlerFactory + { + private final ServerComputer computer; + private final Text name; + private final ItemPocketComputer item; + private final Hand hand; + + public Factory( ServerComputer computer, ItemStack stack, ItemPocketComputer item, Hand hand ) + { + this.computer = computer; + this.name = stack.getName(); + this.item = item; + this.hand = hand; + } + + + @Nonnull + @Override + public Text getDisplayName() + { + return name; + } + + @Nullable + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory inventory, @Nonnull PlayerEntity entity ) + { + return new ContainerPocketComputer( id, computer, item, hand ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java b/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java new file mode 100644 index 000000000..b92c980b1 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java @@ -0,0 +1,413 @@ +/* + * 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.pocket.items; + +import com.google.common.base.Objects; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.shared.PocketUpgrades; +import dan200.computercraft.shared.common.IColouredItem; +import dan200.computercraft.shared.computer.core.ClientComputer; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ComputerState; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.computer.items.IComputerItem; +import dan200.computercraft.shared.network.container.ComputerContainerData; +import dan200.computercraft.shared.pocket.apis.PocketAPI; +import dan200.computercraft.shared.pocket.core.PocketServerComputer; +import dan200.computercraft.shared.pocket.inventory.ContainerPocketComputer; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Formatting; +import net.minecraft.util.Hand; +import net.minecraft.util.TypedActionResult; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; + +public class ItemPocketComputer extends Item implements IComputerItem, IMedia, IColouredItem +{ + private static final String NBT_UPGRADE = "Upgrade"; + private static final String NBT_UPGRADE_INFO = "UpgradeInfo"; + public static final String NBT_LIGHT = "Light"; + + private static final String NBT_INSTANCE = "Instanceid"; + private static final String NBT_SESSION = "SessionId"; + + private final ComputerFamily family; + + public ItemPocketComputer( Settings settings, ComputerFamily family ) + { + super( settings ); + this.family = family; + } + + public ItemStack create( int id, String label, int colour, IPocketUpgrade upgrade ) + { + ItemStack result = new ItemStack( this ); + if( id >= 0 ) result.getOrCreateTag().putInt( NBT_ID, id ); + if( label != null ) result.setCustomName( new LiteralText( label ) ); + if( upgrade != null ) result.getOrCreateTag().putString( NBT_UPGRADE, upgrade.getUpgradeID().toString() ); + if( colour != -1 ) result.getOrCreateTag().putInt( NBT_COLOUR, colour ); + return result; + } + + @Override + public void appendStacks( @Nonnull ItemGroup group, @Nonnull DefaultedList stacks ) + { + if( !isIn( group ) ) return; + stacks.add( create( -1, null, -1, null ) ); + for( IPocketUpgrade upgrade : PocketUpgrades.getVanillaUpgrades() ) + { + stacks.add( create( -1, null, -1, upgrade ) ); + } + } + + @Override + public void inventoryTick( @Nonnull ItemStack stack, World world, @Nonnull Entity entity, int slotNum, boolean selected ) + { + if( !world.isClient ) + { + // Server side + Inventory inventory = entity instanceof PlayerEntity ? ((PlayerEntity) entity).inventory : null; + PocketServerComputer computer = createServerComputer( world, inventory, entity, stack ); + if( computer != null ) + { + IPocketUpgrade upgrade = getUpgrade( stack ); + + // Ping computer + computer.keepAlive(); + computer.setWorld( world ); + computer.updateValues( entity, stack, upgrade ); + + // Sync ID + int id = computer.getID(); + if( id != getComputerID( stack ) ) + { + setComputerID( stack, id ); + if( inventory != null ) inventory.markDirty(); + } + + // Sync label + String label = computer.getLabel(); + if( !Objects.equal( label, getLabel( stack ) ) ) + { + setLabel( stack, label ); + if( inventory != null ) inventory.markDirty(); + } + + // Update pocket upgrade + if( upgrade != null ) + { + upgrade.update( computer, computer.getPeripheral( ComputerSide.BACK ) ); + } + } + } + else + { + // Client side + createClientComputer( stack ); + } + } + + @Nonnull + @Override + public TypedActionResult use( World world, PlayerEntity player, @Nonnull Hand hand ) + { + ItemStack stack = player.getStackInHand( hand ); + if( !world.isClient ) + { + PocketServerComputer computer = createServerComputer( world, player.inventory, player, stack ); + + boolean stop = false; + if( computer != null ) + { + computer.turnOn(); + + IPocketUpgrade upgrade = getUpgrade( stack ); + if( upgrade != null ) + { + computer.updateValues( player, stack, upgrade ); + stop = upgrade.onRightClick( world, computer, computer.getPeripheral( ComputerSide.BACK ) ); + } + } + + if( !stop && computer != null ) + { + new ComputerContainerData( computer ).open( player, new ContainerPocketComputer.Factory( computer, stack, this, hand ) ); + } + } + return new TypedActionResult<>( ActionResult.SUCCESS, stack ); + } + + @Nonnull + @Override + public Text getName( @Nonnull ItemStack stack ) + { + String baseString = getTranslationKey( stack ); + IPocketUpgrade upgrade = getUpgrade( stack ); + if( upgrade != null ) + { + return new TranslatableText( baseString + ".upgraded", + new TranslatableText( upgrade.getUnlocalisedAdjective() ) + ); + } + else + { + return super.getName( stack ); + } + } + + + @Override + public void appendTooltip( @Nonnull ItemStack stack, @Nullable World world, @Nonnull List list, TooltipContext flag ) + { + if( flag.isAdvanced() || getLabel( stack ) == null ) + { + int id = getComputerID( stack ); + if( id >= 0 ) + { + list.add( new TranslatableText( "gui.computercraft.tooltip.computer_id", id ) + .formatted( Formatting.GRAY ) ); + } + } + } + + @Nullable + @Override + public String getCreatorModId( ItemStack stack ) + { + IPocketUpgrade upgrade = getUpgrade( stack ); + if( upgrade != null ) + { + // If we're a non-vanilla, non-CC upgrade then return whichever mod this upgrade + // belongs to. + String mod = PocketUpgrades.getOwner( upgrade ); + if( mod != null && !mod.equals( ComputerCraft.MOD_ID ) ) return mod; + } + + return super.getCreatorModId( stack ); + } + + public PocketServerComputer createServerComputer( final World world, Inventory inventory, Entity entity, @Nonnull ItemStack stack ) + { + if( world.isClient ) return null; + + PocketServerComputer computer; + int instanceID = getInstanceID( stack ); + int sessionID = getSessionID( stack ); + int correctSessionID = ComputerCraft.serverComputerRegistry.getSessionID(); + + if( instanceID >= 0 && sessionID == correctSessionID && + ComputerCraft.serverComputerRegistry.contains( instanceID ) ) + { + computer = (PocketServerComputer) ComputerCraft.serverComputerRegistry.get( instanceID ); + } + else + { + if( instanceID < 0 || sessionID != correctSessionID ) + { + instanceID = ComputerCraft.serverComputerRegistry.getUnusedInstanceID(); + setInstanceID( stack, instanceID ); + setSessionID( stack, correctSessionID ); + } + int computerID = getComputerID( stack ); + if( computerID < 0 ) + { + computerID = ComputerCraftAPI.createUniqueNumberedSaveDir( world, "computer" ); + setComputerID( stack, computerID ); + } + computer = new PocketServerComputer( + world, + computerID, + getLabel( stack ), + instanceID, + getFamily() + ); + computer.updateValues( entity, stack, getUpgrade( stack ) ); + computer.addAPI( new PocketAPI( computer ) ); + ComputerCraft.serverComputerRegistry.add( instanceID, computer ); + if( inventory != null ) inventory.markDirty(); + } + computer.setWorld( world ); + return computer; + } + + public static ServerComputer getServerComputer( @Nonnull ItemStack stack ) + { + int instanceID = getInstanceID( stack ); + return instanceID >= 0 ? ComputerCraft.serverComputerRegistry.get( instanceID ) : null; + } + + public static ClientComputer createClientComputer( @Nonnull ItemStack stack ) + { + int instanceID = getInstanceID( stack ); + if( instanceID >= 0 ) + { + if( !ComputerCraft.clientComputerRegistry.contains( instanceID ) ) + { + ComputerCraft.clientComputerRegistry.add( instanceID, new ClientComputer( instanceID ) ); + } + return ComputerCraft.clientComputerRegistry.get( instanceID ); + } + return null; + } + + private static ClientComputer getClientComputer( @Nonnull ItemStack stack ) + { + int instanceID = getInstanceID( stack ); + return instanceID >= 0 ? ComputerCraft.clientComputerRegistry.get( instanceID ) : null; + } + + // IComputerItem implementation + + private static void setComputerID( @Nonnull ItemStack stack, int computerID ) + { + stack.getOrCreateTag().putInt( NBT_ID, computerID ); + } + + @Override + public String getLabel( @Nonnull ItemStack stack ) + { + return IComputerItem.super.getLabel( stack ); + } + + @Override + public ComputerFamily getFamily() + { + return family; + } + + @Override + public ItemStack withFamily( @Nonnull ItemStack stack, @Nonnull ComputerFamily family ) + { + return PocketComputerItemFactory.create( + getComputerID( stack ), getLabel( stack ), getColour( stack ), + family, getUpgrade( stack ) + ); + } + + // IMedia + + @Override + public boolean setLabel( @Nonnull ItemStack stack, String label ) + { + if( label != null ) + { + stack.setCustomName( new LiteralText( label ) ); + } + else + { + stack.removeCustomName(); + } + return true; + } + + @Override + public IMount createDataMount( @Nonnull ItemStack stack, @Nonnull World world ) + { + int id = getComputerID( stack ); + if( id >= 0 ) + { + return ComputerCraftAPI.createSaveDirMount( world, "computer/" + id, ComputerCraft.computerSpaceLimit ); + } + return null; + } + + private static int getInstanceID( @Nonnull ItemStack stack ) + { + CompoundTag nbt = stack.getTag(); + return nbt != null && nbt.contains( NBT_INSTANCE ) ? nbt.getInt( NBT_INSTANCE ) : -1; + } + + private static void setInstanceID( @Nonnull ItemStack stack, int instanceID ) + { + stack.getOrCreateTag().putInt( NBT_INSTANCE, instanceID ); + } + + private static int getSessionID( @Nonnull ItemStack stack ) + { + CompoundTag nbt = stack.getTag(); + return nbt != null && nbt.contains( NBT_SESSION ) ? nbt.getInt( NBT_SESSION ) : -1; + } + + private static void setSessionID( @Nonnull ItemStack stack, int sessionID ) + { + stack.getOrCreateTag().putInt( NBT_SESSION, sessionID ); + } + + @Environment(EnvType.CLIENT) + public static ComputerState getState( @Nonnull ItemStack stack ) + { + ClientComputer computer = getClientComputer( stack ); + return computer == null ? ComputerState.OFF : computer.getState(); + } + + @Environment(EnvType.CLIENT) + public static int getLightState( @Nonnull ItemStack stack ) + { + ClientComputer computer = getClientComputer( stack ); + if( computer != null && computer.isOn() ) + { + CompoundTag computerNBT = computer.getUserData(); + if( computerNBT != null && computerNBT.contains( NBT_LIGHT ) ) + { + return computerNBT.getInt( NBT_LIGHT ); + } + } + return -1; + } + + public static IPocketUpgrade getUpgrade( @Nonnull ItemStack stack ) + { + CompoundTag compound = stack.getTag(); + return compound != null && compound.contains( NBT_UPGRADE ) + ? PocketUpgrades.get( compound.getString( NBT_UPGRADE ) ) : null; + + } + + public static void setUpgrade( @Nonnull ItemStack stack, IPocketUpgrade upgrade ) + { + CompoundTag compound = stack.getOrCreateTag(); + + if( upgrade == null ) + { + compound.remove( NBT_UPGRADE ); + } + else + { + compound.putString( NBT_UPGRADE, upgrade.getUpgradeID().toString() ); + } + + compound.remove( NBT_UPGRADE_INFO ); + } + + public static CompoundTag getUpgradeInfo( @Nonnull ItemStack stack ) + { + return stack.getOrCreateSubTag( NBT_UPGRADE_INFO ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItemFactory.java b/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItemFactory.java new file mode 100644 index 000000000..25b2210d1 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItemFactory.java @@ -0,0 +1,32 @@ +/* + * 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.pocket.items; + +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; + +public final class PocketComputerItemFactory +{ + private PocketComputerItemFactory() {} + + @Nonnull + public static ItemStack create( int id, String label, int colour, ComputerFamily family, IPocketUpgrade upgrade ) + { + switch( family ) + { + case NORMAL: + return Registry.ModItems.POCKET_COMPUTER_NORMAL.get().create( id, label, colour, upgrade ); + case ADVANCED: + return Registry.ModItems.POCKET_COMPUTER_ADVANCED.get().create( id, label, colour, upgrade ); + default: + return ItemStack.EMPTY; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketModem.java b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketModem.java new file mode 100644 index 000000000..eb968493f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketModem.java @@ -0,0 +1,54 @@ +/* + * 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.pocket.peripherals; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.pocket.AbstractPocketUpgrade; +import dan200.computercraft.api.pocket.IPocketAccess; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.peripheral.modem.ModemState; +import net.minecraft.entity.Entity; +import net.minecraft.util.Identifier; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class PocketModem extends AbstractPocketUpgrade +{ + private final boolean advanced; + + public PocketModem( boolean advanced ) + { + super( + new Identifier( "computercraft", advanced ? "wireless_modem_advanced" : "wireless_modem_normal" ), + advanced + ? Registry.ModBlocks.WIRELESS_MODEM_ADVANCED + : Registry.ModBlocks.WIRELESS_MODEM_NORMAL + ); + this.advanced = advanced; + } + + @Nullable + @Override + public IPeripheral createPeripheral( @Nonnull IPocketAccess access ) + { + return new PocketModemPeripheral( advanced ); + } + + @Override + public void update( @Nonnull IPocketAccess access, @Nullable IPeripheral peripheral ) + { + if( !(peripheral instanceof PocketModemPeripheral) ) return; + + Entity entity = access.getEntity(); + + PocketModemPeripheral modem = (PocketModemPeripheral) peripheral; + + if( entity != null ) modem.setLocation( entity.getEntityWorld(), entity.getCameraPosVec( 1 ) ); + + ModemState state = modem.getModemState(); + if( state.pollChanged() ) access.setLight( state.isOpen() ? 0xBA0000 : -1 ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketModemPeripheral.java b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketModemPeripheral.java new file mode 100644 index 000000000..12e98c000 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketModemPeripheral.java @@ -0,0 +1,51 @@ +/* + * 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.pocket.peripherals; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.peripheral.modem.ModemState; +import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public class PocketModemPeripheral extends WirelessModemPeripheral +{ + private World world = null; + private Vec3d position = Vec3d.ZERO; + + public PocketModemPeripheral( boolean advanced ) + { + super( new ModemState(), advanced ); + } + + void setLocation( World world, Vec3d position ) + { + this.position = position; + this.world = world; + } + + @Nonnull + @Override + public World getWorld() + { + return world; + } + + @Nonnull + @Override + public Vec3d getPosition() + { + return position; + } + + @Override + public boolean equals( IPeripheral other ) + { + return other instanceof PocketModemPeripheral; + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeaker.java b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeaker.java new file mode 100644 index 000000000..44594858b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeaker.java @@ -0,0 +1,47 @@ +/* + * 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.pocket.peripherals; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.pocket.AbstractPocketUpgrade; +import dan200.computercraft.api.pocket.IPocketAccess; +import dan200.computercraft.shared.Registry; +import net.minecraft.entity.Entity; +import net.minecraft.util.Identifier; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class PocketSpeaker extends AbstractPocketUpgrade +{ + public PocketSpeaker() + { + super( new Identifier( "computercraft", "speaker" ), Registry.ModBlocks.SPEAKER ); + } + + @Nullable + @Override + public IPeripheral createPeripheral( @Nonnull IPocketAccess access ) + { + return new PocketSpeakerPeripheral(); + } + + @Override + public void update( @Nonnull IPocketAccess access, @Nullable IPeripheral peripheral ) + { + if( !(peripheral instanceof PocketSpeakerPeripheral) ) return; + + PocketSpeakerPeripheral speaker = (PocketSpeakerPeripheral) peripheral; + + Entity entity = access.getEntity(); + if( entity != null ) + { + speaker.setLocation( entity.getEntityWorld(), entity.getCameraPosVec( 1 ) ); + } + + speaker.update(); + access.setLight( speaker.madeSound( 20 ) ? 0x3320fc : -1 ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java new file mode 100644 index 000000000..66182ff4d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java @@ -0,0 +1,41 @@ +/* + * 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.pocket.peripherals; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +public class PocketSpeakerPeripheral extends SpeakerPeripheral +{ + private World world = null; + private Vec3d position = Vec3d.ZERO; + + void setLocation( World world, Vec3d position ) + { + this.position = position; + this.world = world; + } + + @Override + public World getWorld() + { + return world; + } + + @Override + public Vec3d getPosition() + { + return world != null ? position : null; + } + + @Override + public boolean equals( IPeripheral other ) + { + return other instanceof PocketSpeakerPeripheral; + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java b/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java new file mode 100644 index 000000000..9ed34e2b9 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java @@ -0,0 +1,117 @@ +/* + * 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.pocket.recipes; + +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.shared.PocketUpgrades; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import dan200.computercraft.shared.pocket.items.PocketComputerItemFactory; +import net.minecraft.inventory.CraftingInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.SpecialCraftingRecipe; +import net.minecraft.recipe.SpecialRecipeSerializer; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public final class PocketComputerUpgradeRecipe extends SpecialCraftingRecipe +{ + private PocketComputerUpgradeRecipe( Identifier identifier ) + { + super( identifier ); + } + + @Override + public boolean fits( int x, int y ) + { + return x >= 2 && y >= 2; + } + + @Nonnull + @Override + public ItemStack getOutput() + { + return PocketComputerItemFactory.create( -1, null, -1, ComputerFamily.NORMAL, null ); + } + + @Override + public boolean matches( @Nonnull CraftingInventory inventory, @Nonnull World world ) + { + return !getCraftingResult( inventory ).isEmpty(); + } + + @Nonnull + @Override + public ItemStack getCraftingResult( @Nonnull CraftingInventory inventory ) + { + // Scan the grid for a pocket computer + ItemStack computer = ItemStack.EMPTY; + int computerX = -1; + int computerY = -1; + computer: + for( int y = 0; y < inventory.getHeight(); y++ ) + { + for( int x = 0; x < inventory.getWidth(); x++ ) + { + ItemStack item = inventory.getStack( x + y * inventory.getWidth() ); + if( !item.isEmpty() && item.getItem() instanceof ItemPocketComputer ) + { + computer = item; + computerX = x; + computerY = y; + break computer; + } + } + } + + if( computer.isEmpty() ) return ItemStack.EMPTY; + + ItemPocketComputer itemComputer = (ItemPocketComputer) computer.getItem(); + if( ItemPocketComputer.getUpgrade( computer ) != null ) return ItemStack.EMPTY; + + // Check for upgrades around the item + IPocketUpgrade upgrade = null; + for( int y = 0; y < inventory.getHeight(); y++ ) + { + for( int x = 0; x < inventory.getWidth(); x++ ) + { + ItemStack item = inventory.getStack( x + y * inventory.getWidth() ); + if( x == computerX && y == computerY ) continue; + + if( x == computerX && y == computerY - 1 ) + { + upgrade = PocketUpgrades.get( item ); + if( upgrade == null ) return ItemStack.EMPTY; + } + else if( !item.isEmpty() ) + { + return ItemStack.EMPTY; + } + } + } + + if( upgrade == null ) return ItemStack.EMPTY; + + // Construct the new stack + ComputerFamily family = itemComputer.getFamily(); + int computerID = itemComputer.getComputerID( computer ); + String label = itemComputer.getLabel( computer ); + int colour = itemComputer.getColour( computer ); + return PocketComputerItemFactory.create( computerID, label, colour, family, upgrade ); + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + public static final RecipeSerializer SERIALIZER = new SpecialRecipeSerializer<>( PocketComputerUpgradeRecipe::new ); +} diff --git a/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java b/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java new file mode 100644 index 000000000..bc0cb3514 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java @@ -0,0 +1,193 @@ +/* + * 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.proxy; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.computer.MainThread; +import dan200.computercraft.core.tracking.Tracking; +import dan200.computercraft.shared.command.CommandComputerCraft; +import dan200.computercraft.shared.command.arguments.ArgumentSerializers; +import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider; +import dan200.computercraft.shared.computer.core.IComputer; +import dan200.computercraft.shared.computer.core.IContainerComputer; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.data.BlockNamedEntityLootCondition; +import dan200.computercraft.shared.data.HasComputerIdLootCondition; +import dan200.computercraft.shared.data.PlayerCreativeLootCondition; +import dan200.computercraft.shared.media.items.RecordMedia; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.peripheral.modem.wireless.WirelessNetwork; +import dan200.computercraft.shared.util.NullStorage; +import net.minecraft.item.Item; +import net.minecraft.item.MusicDiscItem; +import net.minecraft.loot.*; +import net.minecraft.loot.condition.LootConditionType; +import net.minecraft.loot.entry.LootTableEntry; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; +import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.event.LootTableLoadEvent; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerContainerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.event.server.FMLServerStartedEvent; +import net.minecraftforge.fml.event.server.FMLServerStoppedEvent; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD ) +public final class ComputerCraftProxyCommon +{ + @SubscribeEvent + @SuppressWarnings( "deprecation" ) + public static void init( FMLCommonSetupEvent event ) + { + NetworkHandler.setup(); + + net.minecraftforge.fml.DeferredWorkQueue.runLater( () -> { + registerProviders(); + ArgumentSerializers.register(); + registerLoot(); + } ); + } + + public static void registerLoot() + { + registerCondition( "block_named", BlockNamedEntityLootCondition.TYPE ); + registerCondition( "player_creative", PlayerCreativeLootCondition.TYPE ); + registerCondition( "has_id", HasComputerIdLootCondition.TYPE ); + } + + private static void registerCondition( String name, LootConditionType serializer ) + { + Registry.register( Registry.LOOT_CONDITION_TYPE, new Identifier( ComputerCraft.MOD_ID, name ), serializer ); + } + + private static void registerProviders() + { + // Register bundled power providers + ComputerCraftAPI.registerBundledRedstoneProvider( new DefaultBundledRedstoneProvider() ); + + // Register media providers + ComputerCraftAPI.registerMediaProvider( stack -> { + Item item = stack.getItem(); + if( item instanceof IMedia ) return (IMedia) item; + if( item instanceof MusicDiscItem ) return RecordMedia.INSTANCE; + return null; + } ); + + // Register capabilities + CapabilityManager.INSTANCE.register( IWiredElement.class, new NullStorage<>(), () -> null ); + CapabilityManager.INSTANCE.register( IPeripheral.class, new NullStorage<>(), () -> null ); + } + + @Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID ) + public static final class ForgeHandlers + { + private ForgeHandlers() + { + } + + /* + @SubscribeEvent + public static void onConnectionOpened( FMLNetworkEvent.ClientConnectedToServerEvent event ) + { + ComputerCraft.clientComputerRegistry.reset(); + } + + @SubscribeEvent + public static void onConnectionClosed( FMLNetworkEvent.ClientDisconnectionFromServerEvent event ) + { + ComputerCraft.clientComputerRegistry.reset(); + } + */ + + @SubscribeEvent + public static void onServerTick( TickEvent.ServerTickEvent event ) + { + if( event.phase == TickEvent.Phase.START ) + { + MainThread.executePendingTasks(); + ComputerCraft.serverComputerRegistry.update(); + } + } + + @SubscribeEvent + public static void onContainerOpen( PlayerContainerEvent.Open event ) + { + // If we're opening a computer container then broadcast the terminal state + ScreenHandler container = event.getContainer(); + if( container instanceof IContainerComputer ) + { + IComputer computer = ((IContainerComputer) container).getComputer(); + if( computer instanceof ServerComputer ) + { + ((ServerComputer) computer).sendTerminalState( event.getPlayer() ); + } + } + } + + @SubscribeEvent + public static void onRegisterCommand( RegisterCommandsEvent event ) + { + CommandComputerCraft.register( event.getDispatcher() ); + } + + @SubscribeEvent + public static void onServerStarted( FMLServerStartedEvent event ) + { + ComputerCraft.serverComputerRegistry.reset(); + WirelessNetwork.resetNetworks(); + Tracking.reset(); + } + + @SubscribeEvent + public static void onServerStopped( FMLServerStoppedEvent event ) + { + ComputerCraft.serverComputerRegistry.reset(); + WirelessNetwork.resetNetworks(); + Tracking.reset(); + } + + public static final Identifier LOOT_TREASURE_DISK = new Identifier( ComputerCraft.MOD_ID, "treasure_disk" ); + + private static final Set TABLES = new HashSet<>( Arrays.asList( + LootTables.SIMPLE_DUNGEON_CHEST, + LootTables.ABANDONED_MINESHAFT_CHEST, + LootTables.STRONGHOLD_CORRIDOR_CHEST, + LootTables.STRONGHOLD_CROSSING_CHEST, + LootTables.STRONGHOLD_LIBRARY_CHEST, + LootTables.DESERT_PYRAMID_CHEST, + LootTables.JUNGLE_TEMPLE_CHEST, + LootTables.IGLOO_CHEST_CHEST, + LootTables.WOODLAND_MANSION_CHEST, + LootTables.VILLAGE_CARTOGRAPHER_CHEST + ) ); + + @SubscribeEvent + public static void lootLoad( LootTableLoadEvent event ) + { + Identifier name = event.getName(); + if( !name.getNamespace().equals( "minecraft" ) || !TABLES.contains( name ) ) return; + + event.getTable().addPool( LootPool.builder() + .with( LootTableEntry.builder( LOOT_TREASURE_DISK ) ) + .rolls( ConstantLootTableRange.create( 1 ) ) + .name( "computercraft_treasure" ) + .build() ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java b/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java new file mode 100644 index 000000000..d70cbcf9b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java @@ -0,0 +1,63 @@ +/* + * 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.turtle; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.event.TurtleRefuelEvent; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.item.ItemStack; +import net.minecraftforge.common.ForgeHooks; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import javax.annotation.Nonnull; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID ) +public final class FurnaceRefuelHandler implements TurtleRefuelEvent.Handler +{ + private static final FurnaceRefuelHandler INSTANCE = new FurnaceRefuelHandler(); + + private FurnaceRefuelHandler() + { + } + + @Override + public int refuel( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack currentStack, int slot, int limit ) + { + int fuelSpaceLeft = turtle.getFuelLimit() - turtle.getFuelLevel(); + int fuelPerItem = getFuelPerItem( turtle.getItemHandler().getStackInSlot( slot ) ); + int fuelItemLimit = (int) Math.ceil( fuelSpaceLeft / (double) fuelPerItem ); + if( limit > fuelItemLimit ) limit = fuelItemLimit; + + ItemStack stack = turtle.getItemHandler().extractItem( slot, limit, false ); + int fuelToGive = fuelPerItem * stack.getCount(); + // Store the replacement item in the inventory + ItemStack replacementStack = stack.getItem().getContainerItem( stack ); + if( !replacementStack.isEmpty() ) + { + ItemStack remainder = InventoryUtil.storeItems( replacementStack, turtle.getItemHandler(), turtle.getSelectedSlot() ); + if( !remainder.isEmpty() ) + { + WorldUtil.dropItemStack( remainder, turtle.getWorld(), turtle.getPosition(), turtle.getDirection().getOpposite() ); + } + } + + return fuelToGive; + } + + private static int getFuelPerItem( @Nonnull ItemStack stack ) + { + return (ForgeHooks.getBurnTime( stack ) * 5) / 100; + } + + @SubscribeEvent + public static void onTurtleRefuel( TurtleRefuelEvent event ) + { + if( event.getHandler() == null && getFuelPerItem( event.getStack() ) > 0 ) event.setHandler( INSTANCE ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java new file mode 100644 index 000000000..41f0f49ff --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -0,0 +1,634 @@ +/* + * 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.turtle.apis; + +import dan200.computercraft.api.lua.*; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.turtle.event.TurtleActionEvent; +import dan200.computercraft.api.turtle.event.TurtleInspectItemEvent; +import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.asm.TaskCallback; +import dan200.computercraft.core.tracking.TrackingField; +import dan200.computercraft.shared.peripheral.generic.data.ItemData; +import dan200.computercraft.shared.turtle.core.*; +import net.minecraft.item.ItemStack; +import net.minecraftforge.common.MinecraftForge; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * The turtle API allows you to control your turtle. + * + * @cc.module turtle + */ +public class TurtleAPI implements ILuaAPI +{ + private final IAPIEnvironment environment; + private final ITurtleAccess turtle; + + public TurtleAPI( IAPIEnvironment environment, ITurtleAccess turtle ) + { + this.environment = environment; + this.turtle = turtle; + } + + @Override + public String[] getNames() + { + return new String[] { "turtle" }; + } + + private MethodResult trackCommand( ITurtleCommand command ) + { + environment.addTrackingChange( TrackingField.TURTLE_OPS ); + return turtle.executeCommand( command ); + } + + /** + * Move the turtle forward one block. + * + * @return The turtle command result. + * @cc.treturn boolean Whether the turtle could successfully move. + * @cc.treturn string|nil The reason the turtle could not move. + */ + @LuaFunction + public final MethodResult forward() + { + return trackCommand( new TurtleMoveCommand( MoveDirection.FORWARD ) ); + } + + /** + * Move the turtle backwards one block. + * + * @return The turtle command result. + * @cc.treturn boolean Whether the turtle could successfully move. + * @cc.treturn string|nil The reason the turtle could not move. + */ + @LuaFunction + public final MethodResult back() + { + return trackCommand( new TurtleMoveCommand( MoveDirection.BACK ) ); + } + + /** + * Move the turtle up one block. + * + * @return The turtle command result. + * @cc.treturn boolean Whether the turtle could successfully move. + * @cc.treturn string|nil The reason the turtle could not move. + */ + @LuaFunction + public final MethodResult up() + { + return trackCommand( new TurtleMoveCommand( MoveDirection.UP ) ); + } + + /** + * Move the turtle down one block. + * + * @return The turtle command result. + * @cc.treturn boolean Whether the turtle could successfully move. + * @cc.treturn string|nil The reason the turtle could not move. + */ + @LuaFunction + public final MethodResult down() + { + return trackCommand( new TurtleMoveCommand( MoveDirection.DOWN ) ); + } + + /** + * Rotate the turtle 90 degress to the left. + * + * @return The turtle command result. + * @cc.treturn boolean Whether the turtle could successfully turn. + * @cc.treturn string|nil The reason the turtle could not turn. + */ + @LuaFunction + public final MethodResult turnLeft() + { + return trackCommand( new TurtleTurnCommand( TurnDirection.LEFT ) ); + } + + /** + * Rotate the turtle 90 degress to the right. + * + * @return The turtle command result. + * @cc.treturn boolean Whether the turtle could successfully turn. + * @cc.treturn string|nil The reason the turtle could not turn. + */ + @LuaFunction + public final MethodResult turnRight() + { + return trackCommand( new TurtleTurnCommand( TurnDirection.RIGHT ) ); + } + + /** + * Attempt to break the block in front of the turtle. + * + * This requires a turtle tool capable of breaking the block. Diamond pickaxes + * (mining turtles) can break any vanilla block, but other tools (such as axes) + * are more limited. + * + * @param side The specific tool to use. Should be "left" or "right". + * @return The turtle command result. + * @cc.treturn boolean Whether a block was broken. + * @cc.treturn string|nil The reason no block was broken. + */ + @LuaFunction + public final MethodResult dig( Optional side ) + { + environment.addTrackingChange( TrackingField.TURTLE_OPS ); + return trackCommand( TurtleToolCommand.dig( InteractDirection.FORWARD, side.orElse( null ) ) ); + } + + /** + * Attempt to break the block above the turtle. See {@link #dig} for full details. + * + * @param side The specific tool to use. + * @return The turtle command result. + * @cc.treturn boolean Whether a block was broken. + * @cc.treturn string|nil The reason no block was broken. + */ + @LuaFunction + public final MethodResult digUp( Optional side ) + { + environment.addTrackingChange( TrackingField.TURTLE_OPS ); + return trackCommand( TurtleToolCommand.dig( InteractDirection.UP, side.orElse( null ) ) ); + } + + /** + * Attempt to break the block below the turtle. See {@link #dig} for full details. + * + * @param side The specific tool to use. + * @return The turtle command result. + * @cc.treturn boolean Whether a block was broken. + * @cc.treturn string|nil The reason no block was broken. + */ + @LuaFunction + public final MethodResult digDown( Optional side ) + { + environment.addTrackingChange( TrackingField.TURTLE_OPS ); + return trackCommand( TurtleToolCommand.dig( InteractDirection.DOWN, side.orElse( null ) ) ); + } + + /** + * Place a block or item into the world in front of the turtle. + * + * @param args Arguments to place. + * @return The turtle command result. + * @cc.tparam [opt] string text When placing a sign, set its contents to this text. + * @cc.treturn boolean Whether the block could be placed. + * @cc.treturn string|nil The reason the block was not placed. + */ + @LuaFunction + public final MethodResult place( IArguments args ) + { + return trackCommand( new TurtlePlaceCommand( InteractDirection.FORWARD, args.getAll() ) ); + } + + /** + * Place a block or item into the world above the turtle. + * + * @param args Arguments to place. + * @return The turtle command result. + * @cc.tparam [opt] string text When placing a sign, set its contents to this text. + * @cc.treturn boolean Whether the block could be placed. + * @cc.treturn string|nil The reason the block was not placed. + */ + @LuaFunction + public final MethodResult placeUp( IArguments args ) + { + return trackCommand( new TurtlePlaceCommand( InteractDirection.UP, args.getAll() ) ); + } + + /** + * Place a block or item into the world below the turtle. + * + * @param args Arguments to place. + * @return The turtle command result. + * @cc.tparam [opt] string text When placing a sign, set its contents to this text. + * @cc.treturn boolean Whether the block could be placed. + * @cc.treturn string|nil The reason the block was not placed. + */ + @LuaFunction + public final MethodResult placeDown( IArguments args ) + { + return trackCommand( new TurtlePlaceCommand( InteractDirection.DOWN, args.getAll() ) ); + } + + /** + * Drop the currently selected stack into the inventory in front of the turtle, or as an item into the world if + * there is no inventory. + * + * @param count The number of items to drop. If not given, the entire stack will be dropped. + * @return The turtle command result. + * @throws LuaException If dropping an invalid number of items. + * @cc.treturn boolean Whether items were dropped. + * @cc.treturn string|nil The reason the no items were dropped. + * @see #select + */ + @LuaFunction + public final MethodResult drop( Optional count ) throws LuaException + { + return trackCommand( new TurtleDropCommand( InteractDirection.FORWARD, checkCount( count ) ) ); + } + + /** + * Drop the currently selected stack into the inventory above the turtle, or as an item into the world if there is + * no inventory. + * + * @param count The number of items to drop. If not given, the entire stack will be dropped. + * @return The turtle command result. + * @throws LuaException If dropping an invalid number of items. + * @cc.treturn boolean Whether items were dropped. + * @cc.treturn string|nil The reason the no items were dropped. + * @see #select + */ + @LuaFunction + public final MethodResult dropUp( Optional count ) throws LuaException + { + return trackCommand( new TurtleDropCommand( InteractDirection.UP, checkCount( count ) ) ); + } + + /** + * Drop the currently selected stack into the inventory in front of the turtle, or as an item into the world if + * there is no inventory. + * + * @param count The number of items to drop. If not given, the entire stack will be dropped. + * @return The turtle command result. + * @throws LuaException If dropping an invalid number of items. + * @cc.treturn boolean Whether items were dropped. + * @cc.treturn string|nil The reason the no items were dropped. + * @see #select + */ + @LuaFunction + public final MethodResult dropDown( Optional count ) throws LuaException + { + return trackCommand( new TurtleDropCommand( InteractDirection.DOWN, checkCount( count ) ) ); + } + + /** + * Change the currently selected slot. + * + * The selected slot is determines what slot actions like {@link #drop} or {@link #getItemCount} act on. + * + * @param slot The slot to select. + * @return The turtle command result. + * @throws LuaException If the slot is out of range. + * @cc.treturn true When the slot has been selected. + * @see #getSelectedSlot + */ + + @LuaFunction + public final MethodResult select( int slot ) throws LuaException + { + int actualSlot = checkSlot( slot ); + return turtle.executeCommand( turtle -> { + turtle.setSelectedSlot( actualSlot ); + return TurtleCommandResult.success(); + } ); + } + + /** + * Get the number of items in the given slot. + * + * @param slot The slot we wish to check. Defaults to the {@link #select selected slot}. + * @return The number of items in this slot. + * @throws LuaException If the slot is out of range. + */ + @LuaFunction + public final int getItemCount( Optional slot ) throws LuaException + { + int actualSlot = checkSlot( slot ).orElse( turtle.getSelectedSlot() ); + return turtle.getInventory().getStack( actualSlot ).getCount(); + } + + /** + * Get the remaining number of items which may be stored in this stack. + * + * For instance, if a slot contains 13 blocks of dirt, it has room for another 51. + * + * @param slot The slot we wish to check. Defaults to the {@link #select selected slot}. + * @return The space left in in this slot. + * @throws LuaException If the slot is out of range. + */ + @LuaFunction + public final int getItemSpace( Optional slot ) throws LuaException + { + int actualSlot = checkSlot( slot ).orElse( turtle.getSelectedSlot() ); + ItemStack stack = turtle.getInventory().getStack( actualSlot ); + return stack.isEmpty() ? 64 : Math.min( stack.getMaxCount(), 64 ) - stack.getCount(); + } + + /** + * Check if there is a solid block in front of the turtle. In this case, solid refers to any non-air or liquid + * block. + * + * @return The turtle command result. + * @cc.treturn boolean If there is a solid block in front. + */ + @LuaFunction + public final MethodResult detect() + { + return trackCommand( new TurtleDetectCommand( InteractDirection.FORWARD ) ); + } + + /** + * Check if there is a solid block above the turtle. In this case, solid refers to any non-air or liquid block. + * + * @return The turtle command result. + * @cc.treturn boolean If there is a solid block in front. + */ + @LuaFunction + public final MethodResult detectUp() + { + return trackCommand( new TurtleDetectCommand( InteractDirection.UP ) ); + } + + /** + * Check if there is a solid block below the turtle. In this case, solid refers to any non-air or liquid block. + * + * @return The turtle command result. + * @cc.treturn boolean If there is a solid block in front. + */ + @LuaFunction + public final MethodResult detectDown() + { + return trackCommand( new TurtleDetectCommand( InteractDirection.DOWN ) ); + } + + @LuaFunction + public final MethodResult compare() + { + return trackCommand( new TurtleCompareCommand( InteractDirection.FORWARD ) ); + } + + @LuaFunction + public final MethodResult compareUp() + { + return trackCommand( new TurtleCompareCommand( InteractDirection.UP ) ); + } + + @LuaFunction + public final MethodResult compareDown() + { + return trackCommand( new TurtleCompareCommand( InteractDirection.DOWN ) ); + } + + /** + * Attack the entity in front of the turtle. + * + * @param side The specific tool to use. + * @return The turtle command result. + * @cc.treturn boolean Whether an entity was attacked. + * @cc.treturn string|nil The reason nothing was attacked. + */ + @LuaFunction + public final MethodResult attack( Optional side ) + { + return trackCommand( TurtleToolCommand.attack( InteractDirection.FORWARD, side.orElse( null ) ) ); + } + + /** + * Attack the entity above the turtle. + * + * @param side The specific tool to use. + * @return The turtle command result. + * @cc.treturn boolean Whether an entity was attacked. + * @cc.treturn string|nil The reason nothing was attacked. + */ + @LuaFunction + public final MethodResult attackUp( Optional side ) + { + return trackCommand( TurtleToolCommand.attack( InteractDirection.UP, side.orElse( null ) ) ); + } + + /** + * Attack the entity below the turtle. + * + * @param side The specific tool to use. + * @return The turtle command result. + * @cc.treturn boolean Whether an entity was attacked. + * @cc.treturn string|nil The reason nothing was attacked. + */ + @LuaFunction + public final MethodResult attackDown( Optional side ) + { + return trackCommand( TurtleToolCommand.attack( InteractDirection.DOWN, side.orElse( null ) ) ); + } + + /** + * Suck an item from the inventory in front of the turtle, or from an item floating in the world. + * + * This will pull items into the first acceptable slot, starting at the {@link #select currently selected} one. + * + * @param count The number of items to suck. If not given, up to a stack of items will be picked up. + * @return The turtle command result. + * @throws LuaException If given an invalid number of items. + * @cc.treturn boolean Whether items were picked up. + * @cc.treturn string|nil The reason the no items were picked up. + */ + @LuaFunction + public final MethodResult suck( Optional count ) throws LuaException + { + return trackCommand( new TurtleSuckCommand( InteractDirection.FORWARD, checkCount( count ) ) ); + } + + /** + * Suck an item from the inventory above the turtle, or from an item floating in the world. + * + * @param count The number of items to suck. If not given, up to a stack of items will be picked up. + * @return The turtle command result. + * @throws LuaException If given an invalid number of items. + * @cc.treturn boolean Whether items were picked up. + * @cc.treturn string|nil The reason the no items were picked up. + */ + @LuaFunction + public final MethodResult suckUp( Optional count ) throws LuaException + { + return trackCommand( new TurtleSuckCommand( InteractDirection.UP, checkCount( count ) ) ); + } + + /** + * Suck an item from the inventory below the turtle, or from an item floating in the world. + * + * @param count The number of items to suck. If not given, up to a stack of items will be picked up. + * @return The turtle command result. + * @throws LuaException If given an invalid number of items. + * @cc.treturn boolean Whether items were picked up. + * @cc.treturn string|nil The reason the no items were picked up. + */ + @LuaFunction + public final MethodResult suckDown( Optional count ) throws LuaException + { + return trackCommand( new TurtleSuckCommand( InteractDirection.DOWN, checkCount( count ) ) ); + } + + @LuaFunction + public final Object getFuelLevel() + { + return turtle.isFuelNeeded() ? turtle.getFuelLevel() : "unlimited"; + } + + @LuaFunction + public final MethodResult refuel( Optional countA ) throws LuaException + { + int count = countA.orElse( Integer.MAX_VALUE ); + if( count < 0 ) throw new LuaException( "Refuel count " + count + " out of range" ); + return trackCommand( new TurtleRefuelCommand( count ) ); + } + + @LuaFunction + public final MethodResult compareTo( int slot ) throws LuaException + { + return trackCommand( new TurtleCompareToCommand( checkSlot( slot ) ) ); + } + + @LuaFunction + public final MethodResult transferTo( int slotArg, Optional countArg ) throws LuaException + { + int slot = checkSlot( slotArg ); + int count = checkCount( countArg ); + return trackCommand( new TurtleTransferToCommand( slot, count ) ); + } + + /** + * Get the currently sleected slot. + * + * @return The current slot. + * @see #select + */ + @LuaFunction + public final int getSelectedSlot() + { + return turtle.getSelectedSlot() + 1; + } + + @LuaFunction + public final Object getFuelLimit() + { + return turtle.isFuelNeeded() ? turtle.getFuelLimit() : "unlimited"; + } + + @LuaFunction + public final MethodResult equipLeft() + { + return trackCommand( new TurtleEquipCommand( TurtleSide.LEFT ) ); + } + + @LuaFunction + public final MethodResult equipRight() + { + return trackCommand( new TurtleEquipCommand( TurtleSide.RIGHT ) ); + } + + /** + * Get information about the block in front of the turtle. + * + * @return The turtle command result. + * @cc.treturn boolean Whether there is a block in front of the turtle. + * @cc.treturn table|string Information about the block in front, or a message explaining that there is no block. + */ + @LuaFunction + public final MethodResult inspect() + { + return trackCommand( new TurtleInspectCommand( InteractDirection.FORWARD ) ); + } + + /** + * Get information about the block above the turtle. + * + * @return The turtle command result. + * @cc.treturn boolean Whether there is a block above the turtle. + * @cc.treturn table|string Information about the above below, or a message explaining that there is no block. + */ + @LuaFunction + public final MethodResult inspectUp() + { + return trackCommand( new TurtleInspectCommand( InteractDirection.UP ) ); + } + + /** + * Get information about the block below the turtle. + * + * @return The turtle command result. + * @cc.treturn boolean Whether there is a block below the turtle. + * @cc.treturn table|string Information about the block below, or a message explaining that there is no block. + */ + @LuaFunction + public final MethodResult inspectDown() + { + return trackCommand( new TurtleInspectCommand( InteractDirection.DOWN ) ); + } + + /** + * Get detailed information about the items in the given slot. + * + * @param context The Lua context + * @param slot The slot to get information about. Defaults to the {@link #select selected slot}. + * @param detailed Whether to include "detailed" information. When {@code true} the method will contain much + * more information about the item at the cost of taking longer to run. + * @return The command result. + * @throws LuaException If the slot is out of range. + * @cc.treturn nil|table Information about the given slot, or {@code nil} if it is empty. + * @cc.usage Print the current slot, assuming it contains 13 dirt. + * + *
{@code
+     * print(textutils.serialize(turtle.getItemDetail()))
+     * -- => {
+     * --  name = "minecraft:dirt",
+     * --  count = 13,
+     * -- }
+     * }
+ */ + @LuaFunction + public final MethodResult getItemDetail( ILuaContext context, Optional slot, Optional detailed ) throws LuaException + { + int actualSlot = checkSlot( slot ).orElse( turtle.getSelectedSlot() ); + return detailed.orElse( false ) + ? TaskCallback.make( context, () -> getItemDetail( actualSlot, true ) ) + : MethodResult.of( getItemDetail( actualSlot, false ) ); + } + + private Object[] getItemDetail( int slot, boolean detailed ) + { + ItemStack stack = turtle.getInventory().getStack( slot ); + if( stack.isEmpty() ) return new Object[] { null }; + + Map table = detailed + ? ItemData.fill( new HashMap<>(), stack ) + : ItemData.fillBasicSafe( new HashMap<>(), stack ); + + TurtleActionEvent event = new TurtleInspectItemEvent( turtle, stack, table, detailed ); + if( MinecraftForge.EVENT_BUS.post( event ) ) return new Object[] { false, event.getFailureMessage() }; + + return new Object[] { table }; + } + + + private static int checkSlot( int slot ) throws LuaException + { + if( slot < 1 || slot > 16 ) throw new LuaException( "Slot number " + slot + " out of range" ); + return slot - 1; + } + + private static Optional checkSlot( Optional slot ) throws LuaException + { + return slot.isPresent() ? Optional.of( checkSlot( slot.get() ) ) : Optional.empty(); + } + + private static int checkCount( Optional countArg ) throws LuaException + { + int count = countArg.orElse( 64 ); + if( count < 0 || count > 64 ) throw new LuaException( "Item count " + count + " out of range" ); + return count; + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java b/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java new file mode 100644 index 000000000..7b50c614f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java @@ -0,0 +1,174 @@ +/* + * 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.turtle.blocks; + +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.computer.blocks.BlockComputerBase; +import dan200.computercraft.shared.computer.blocks.TileComputerBase; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.turtle.core.TurtleBrain; +import dan200.computercraft.shared.turtle.items.ITurtleItem; +import dan200.computercraft.shared.turtle.items.TurtleItemFactory; +import net.minecraft.block.Block; +import net.minecraft.block.BlockRenderType; +import net.minecraft.block.BlockState; +import net.minecraft.block.ShapeContext; +import net.minecraft.block.Waterloggable; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.projectile.ExplosiveProjectileEntity; +import net.minecraft.fluid.FluidState; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.DirectionProperty; +import net.minecraft.state.property.Properties; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; +import net.minecraft.world.BlockView; +import net.minecraft.world.World; +import net.minecraft.world.WorldAccess; +import net.minecraft.world.explosion.Explosion; +import net.minecraftforge.fml.RegistryObject; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static dan200.computercraft.shared.util.WaterloggableHelpers.*; +import static net.minecraft.state.property.Properties.WATERLOGGED; + +public class BlockTurtle extends BlockComputerBase implements Waterloggable +{ + public static final DirectionProperty FACING = Properties.HORIZONTAL_FACING; + + private static final VoxelShape DEFAULT_SHAPE = VoxelShapes.cuboid( + 0.125, 0.125, 0.125, + 0.875, 0.875, 0.875 + ); + + public BlockTurtle( Settings settings, ComputerFamily family, RegistryObject> type ) + { + super( settings, family, type ); + setDefaultState( getStateManager().getDefaultState() + .with( FACING, Direction.NORTH ) + .with( WATERLOGGED, false ) + ); + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( FACING, WATERLOGGED ); + } + + @Nonnull + @Override + @Deprecated + public BlockRenderType getRenderType( @Nonnull BlockState state ) + { + return BlockRenderType.ENTITYBLOCK_ANIMATED; + } + + @Nonnull + @Override + @Deprecated + public VoxelShape getOutlineShape( @Nonnull BlockState state, BlockView world, @Nonnull BlockPos pos, @Nonnull ShapeContext context ) + { + BlockEntity tile = world.getBlockEntity( pos ); + Vec3d offset = tile instanceof TileTurtle ? ((TileTurtle) tile).getRenderOffset( 1.0f ) : Vec3d.ZERO; + return offset.equals( Vec3d.ZERO ) ? DEFAULT_SHAPE : DEFAULT_SHAPE.offset( offset.x, offset.y, offset.z ); + } + + @Nullable + @Override + public BlockState getPlacementState( ItemPlacementContext placement ) + { + return getDefaultState() + .with( FACING, placement.getPlayerFacing() ) + .with( WATERLOGGED, getWaterloggedStateForPlacement( placement ) ); + } + + @Nonnull + @Override + @Deprecated + public FluidState getFluidState( @Nonnull BlockState state ) + { + return getWaterloggedFluidState( state ); + } + + @Nonnull + @Override + @Deprecated + public BlockState getStateForNeighborUpdate( @Nonnull BlockState state, @Nonnull Direction side, @Nonnull BlockState otherState, @Nonnull WorldAccess world, @Nonnull BlockPos pos, @Nonnull BlockPos otherPos ) + { + updateWaterloggedPostPlacement( state, world, pos ); + return state; + } + + @Override + public void onPlaced( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState state, @Nullable LivingEntity player, @Nonnull ItemStack stack ) + { + super.onPlaced( world, pos, state, player, stack ); + + BlockEntity tile = world.getBlockEntity( pos ); + if( !world.isClient && tile instanceof TileTurtle ) + { + TileTurtle turtle = (TileTurtle) tile; + + if( player instanceof PlayerEntity ) + { + ((TileTurtle) tile).setOwningPlayer( ((PlayerEntity) player).getGameProfile() ); + } + + if( stack.getItem() instanceof ITurtleItem ) + { + ITurtleItem item = (ITurtleItem) stack.getItem(); + + // Set Upgrades + for( TurtleSide side : TurtleSide.values() ) + { + turtle.getAccess().setUpgrade( side, item.getUpgrade( stack, side ) ); + } + + turtle.getAccess().setFuelLevel( item.getFuelLevel( stack ) ); + + // Set colour + int colour = item.getColour( stack ); + if( colour != -1 ) turtle.getAccess().setColour( colour ); + + // Set overlay + Identifier overlay = item.getOverlay( stack ); + if( overlay != null ) ((TurtleBrain) turtle.getAccess()).setOverlay( overlay ); + } + } + } + + @Override + public float getExplosionResistance( BlockState state, BlockView world, BlockPos pos, Explosion explosion ) + { + Entity exploder = explosion.getExploder(); + if( getFamily() == ComputerFamily.ADVANCED || exploder instanceof LivingEntity || exploder instanceof ExplosiveProjectileEntity ) + { + return 2000; + } + + return super.getExplosionResistance( state, world, pos, explosion ); + } + + @Nonnull + @Override + protected ItemStack getItem( TileComputerBase tile ) + { + return tile instanceof TileTurtle ? TurtleItemFactory.create( (TileTurtle) tile ) : ItemStack.EMPTY; + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/blocks/ITurtleTile.java b/src/main/java/dan200/computercraft/shared/turtle/blocks/ITurtleTile.java new file mode 100644 index 000000000..c088a242b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/blocks/ITurtleTile.java @@ -0,0 +1,30 @@ +/* + * 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.turtle.blocks; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.computer.blocks.IComputerTile; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; + +public interface ITurtleTile extends IComputerTile +{ + int getColour(); + + Identifier getOverlay(); + + ITurtleUpgrade getUpgrade( TurtleSide side ); + + ITurtleAccess getAccess(); + + Vec3d getRenderOffset( float f ); + + float getRenderYaw( float f ); + + float getToolRenderAngle( TurtleSide side, float f ); +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java b/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java new file mode 100644 index 000000000..177560dab --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java @@ -0,0 +1,579 @@ +/* + * 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.turtle.blocks; + +import com.mojang.authlib.GameProfile; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.computer.blocks.ComputerPeripheral; +import dan200.computercraft.shared.computer.blocks.ComputerProxy; +import dan200.computercraft.shared.computer.blocks.TileComputerBase; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ComputerState; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.turtle.apis.TurtleAPI; +import dan200.computercraft.shared.turtle.blocks.TileTurtle.MoveState; +import dan200.computercraft.shared.turtle.core.TurtleBrain; +import dan200.computercraft.shared.turtle.inventory.ContainerTurtle; +import dan200.computercraft.shared.util.*; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.DyeItem; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.util.*; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.Constants; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.items.IItemHandlerModifiable; +import net.minecraftforge.items.wrapper.InvWrapper; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; + +import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; +import static net.minecraftforge.items.CapabilityItemHandler.ITEM_HANDLER_CAPABILITY; + +public class TileTurtle extends TileComputerBase implements ITurtleTile, DefaultInventory +{ + public static final int INVENTORY_SIZE = 16; + public static final int INVENTORY_WIDTH = 4; + public static final int INVENTORY_HEIGHT = 4; + + enum MoveState + { + NOT_MOVED, + IN_PROGRESS, + MOVED + } + + private final DefaultedList m_inventory = DefaultedList.ofSize( INVENTORY_SIZE, ItemStack.EMPTY ); + private final DefaultedList m_previousInventory = DefaultedList.ofSize( INVENTORY_SIZE, ItemStack.EMPTY ); + private final IItemHandlerModifiable m_itemHandler = new InvWrapper( this ); + private LazyOptional itemHandlerCap; + private boolean m_inventoryChanged = false; + private TurtleBrain m_brain = new TurtleBrain( this ); + private MoveState m_moveState = MoveState.NOT_MOVED; + private LazyOptional peripheral; + + public TileTurtle( BlockEntityType type, ComputerFamily family ) + { + super( type, family ); + } + + private boolean hasMoved() + { + return m_moveState == MoveState.MOVED; + } + + @Override + protected ServerComputer createComputer( int instanceID, int id ) + { + ServerComputer computer = new ServerComputer( + getWorld(), id, label, instanceID, getFamily(), + ComputerCraft.turtleTermWidth, ComputerCraft.turtleTermHeight + ); + computer.setPosition( getPos() ); + computer.addAPI( new TurtleAPI( computer.getAPIEnvironment(), getAccess() ) ); + m_brain.setupComputer( computer ); + return computer; + } + + public ComputerProxy createProxy() + { + return m_brain.getProxy(); + } + + @Override + public void destroy() + { + if( !hasMoved() ) + { + // Stop computer + super.destroy(); + + // Drop contents + if( !getWorld().isClient ) + { + int size = size(); + for( int i = 0; i < size; i++ ) + { + ItemStack stack = getStack( i ); + if( !stack.isEmpty() ) + { + WorldUtil.dropItemStack( stack, getWorld(), getPos() ); + } + } + } + } + else + { + // Just turn off any redstone we had on + for( Direction dir : DirectionUtil.FACINGS ) + { + RedstoneUtil.propagateRedstoneOutput( getWorld(), getPos(), dir ); + } + } + } + + @Override + protected void unload() + { + if( !hasMoved() ) + { + super.unload(); + } + } + + @Override + protected void invalidateCaps() + { + super.invalidateCaps(); + itemHandlerCap = CapabilityUtil.invalidate( itemHandlerCap ); + peripheral = CapabilityUtil.invalidate( peripheral ); + } + + @Nonnull + @Override + public ActionResult onActivate( PlayerEntity player, Hand hand, BlockHitResult hit ) + { + // Apply dye + ItemStack currentItem = player.getStackInHand( hand ); + if( !currentItem.isEmpty() ) + { + if( currentItem.getItem() instanceof DyeItem ) + { + // Dye to change turtle colour + if( !getWorld().isClient ) + { + DyeColor dye = ((DyeItem) currentItem.getItem()).getColor(); + if( m_brain.getDyeColour() != dye ) + { + m_brain.setDyeColour( dye ); + if( !player.isCreative() ) + { + currentItem.decrement( 1 ); + } + } + } + return ActionResult.SUCCESS; + } + else if( currentItem.getItem() == Items.WATER_BUCKET && m_brain.getColour() != -1 ) + { + // Water to remove turtle colour + if( !getWorld().isClient ) + { + if( m_brain.getColour() != -1 ) + { + m_brain.setColour( -1 ); + if( !player.isCreative() ) + { + player.setStackInHand( hand, new ItemStack( Items.BUCKET ) ); + player.inventory.markDirty(); + } + } + } + return ActionResult.SUCCESS; + } + } + + // Open GUI or whatever + return super.onActivate( player, hand, hit ); + } + + @Override + protected boolean canNameWithTag( PlayerEntity player ) + { + return true; + } + + @Override + protected double getInteractRange( PlayerEntity player ) + { + return 12.0; + } + + @Override + public void tick() + { + super.tick(); + m_brain.update(); + if( !getWorld().isClient && m_inventoryChanged ) + { + ServerComputer computer = getServerComputer(); + if( computer != null ) computer.queueEvent( "turtle_inventory" ); + + m_inventoryChanged = false; + for( int n = 0; n < size(); n++ ) + { + m_previousInventory.set( n, getStack( n ).copy() ); + } + } + } + + @Override + protected void updateBlockState( ComputerState newState ) + { + } + + @Override + public void onNeighbourChange( @Nonnull BlockPos neighbour ) + { + if( m_moveState == MoveState.NOT_MOVED ) super.onNeighbourChange( neighbour ); + } + + @Override + public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour ) + { + if( m_moveState == MoveState.NOT_MOVED ) super.onNeighbourTileEntityChange( neighbour ); + } + + public void notifyMoveStart() + { + if( m_moveState == MoveState.NOT_MOVED ) m_moveState = MoveState.IN_PROGRESS; + } + + public void notifyMoveEnd() + { + // MoveState.MOVED is final + if( m_moveState == MoveState.IN_PROGRESS ) m_moveState = MoveState.NOT_MOVED; + } + + @Override + public void fromTag( @Nonnull BlockState state, @Nonnull CompoundTag nbt ) + { + super.fromTag( state, nbt ); + + // Read inventory + ListTag nbttaglist = nbt.getList( "Items", Constants.NBT.TAG_COMPOUND ); + m_inventory.clear(); + m_previousInventory.clear(); + for( int i = 0; i < nbttaglist.size(); i++ ) + { + CompoundTag tag = nbttaglist.getCompound( i ); + int slot = tag.getByte( "Slot" ) & 0xff; + if( slot < size() ) + { + m_inventory.set( slot, ItemStack.fromTag( tag ) ); + m_previousInventory.set( slot, m_inventory.get( slot ).copy() ); + } + } + + // Read state + m_brain.readFromNBT( nbt ); + } + + @Nonnull + @Override + public CompoundTag toTag( @Nonnull CompoundTag nbt ) + { + // Write inventory + ListTag nbttaglist = new ListTag(); + for( int i = 0; i < INVENTORY_SIZE; i++ ) + { + if( !m_inventory.get( i ).isEmpty() ) + { + CompoundTag tag = new CompoundTag(); + tag.putByte( "Slot", (byte) i ); + m_inventory.get( i ).toTag( tag ); + nbttaglist.add( tag ); + } + } + nbt.put( "Items", nbttaglist ); + + // Write brain + nbt = m_brain.writeToNBT( nbt ); + + return super.toTag( nbt ); + } + + @Override + protected boolean isPeripheralBlockedOnSide( ComputerSide localSide ) + { + return hasPeripheralUpgradeOnSide( localSide ); + } + + // IDirectionalTile + + @Override + public Direction getDirection() + { + return getCachedState().get( BlockTurtle.FACING ); + } + + public void setDirection( Direction dir ) + { + if( dir.getAxis() == Direction.Axis.Y ) dir = Direction.NORTH; + world.setBlockState( pos, getCachedState().with( BlockTurtle.FACING, dir ) ); + updateOutput(); + updateInput(); + onTileEntityChange(); + } + + // ITurtleTile + + @Override + public ITurtleUpgrade getUpgrade( TurtleSide side ) + { + return m_brain.getUpgrade( side ); + } + + @Override + public int getColour() + { + return m_brain.getColour(); + } + + @Override + public Identifier getOverlay() + { + return m_brain.getOverlay(); + } + + @Override + public ITurtleAccess getAccess() + { + return m_brain; + } + + @Override + public Vec3d getRenderOffset( float f ) + { + return m_brain.getRenderOffset( f ); + } + + @Override + public float getRenderYaw( float f ) + { + return m_brain.getVisualYaw( f ); + } + + @Override + public float getToolRenderAngle( TurtleSide side, float f ) + { + return m_brain.getToolRenderAngle( side, f ); + } + + void setOwningPlayer( GameProfile player ) + { + m_brain.setOwningPlayer( player ); + markDirty(); + } + + // IInventory + + @Override + public int size() + { + return INVENTORY_SIZE; + } + + @Override + public boolean isEmpty() + { + for( ItemStack stack : m_inventory ) + { + if( !stack.isEmpty() ) return false; + } + return true; + } + + @Nonnull + @Override + public ItemStack getStack( int slot ) + { + return slot >= 0 && slot < INVENTORY_SIZE ? m_inventory.get( slot ) : ItemStack.EMPTY; + } + + @Nonnull + @Override + public ItemStack removeStack( int slot ) + { + ItemStack result = getStack( slot ); + setStack( slot, ItemStack.EMPTY ); + return result; + } + + @Nonnull + @Override + public ItemStack removeStack( int slot, int count ) + { + if( count == 0 ) return ItemStack.EMPTY; + + ItemStack stack = getStack( slot ); + if( stack.isEmpty() ) return ItemStack.EMPTY; + + if( stack.getCount() <= count ) + { + setStack( slot, ItemStack.EMPTY ); + return stack; + } + + ItemStack part = stack.split( count ); + onInventoryDefinitelyChanged(); + return part; + } + + @Override + public void setStack( int i, @Nonnull ItemStack stack ) + { + if( i >= 0 && i < INVENTORY_SIZE && !InventoryUtil.areItemsEqual( stack, m_inventory.get( i ) ) ) + { + m_inventory.set( i, stack ); + onInventoryDefinitelyChanged(); + } + } + + @Override + public void clear() + { + boolean changed = false; + for( int i = 0; i < INVENTORY_SIZE; i++ ) + { + if( !m_inventory.get( i ).isEmpty() ) + { + m_inventory.set( i, ItemStack.EMPTY ); + changed = true; + } + } + + if( changed ) onInventoryDefinitelyChanged(); + } + + @Override + public void markDirty() + { + super.markDirty(); + if( !m_inventoryChanged ) + { + for( int n = 0; n < size(); n++ ) + { + if( !ItemStack.areEqual( getStack( n ), m_previousInventory.get( n ) ) ) + { + m_inventoryChanged = true; + break; + } + } + } + } + + @Override + public boolean canPlayerUse( @Nonnull PlayerEntity player ) + { + return isUsable( player, false ); + } + + private void onInventoryDefinitelyChanged() + { + super.markDirty(); + m_inventoryChanged = true; + } + + public void onTileEntityChange() + { + super.markDirty(); + } + + // Networking stuff + + @Override + protected void writeDescription( @Nonnull CompoundTag nbt ) + { + super.writeDescription( nbt ); + m_brain.writeDescription( nbt ); + } + + @Override + protected void readDescription( @Nonnull CompoundTag nbt ) + { + super.readDescription( nbt ); + m_brain.readDescription( nbt ); + } + + // Privates + + private boolean hasPeripheralUpgradeOnSide( ComputerSide side ) + { + ITurtleUpgrade upgrade; + switch( side ) + { + case RIGHT: + upgrade = getUpgrade( TurtleSide.RIGHT ); + break; + case LEFT: + upgrade = getUpgrade( TurtleSide.LEFT ); + break; + default: + return false; + } + return upgrade != null && upgrade.getType().isPeripheral(); + } + + public void transferStateFrom( TileTurtle copy ) + { + super.transferStateFrom( copy ); + Collections.copy( m_inventory, copy.m_inventory ); + Collections.copy( m_previousInventory, copy.m_previousInventory ); + m_inventoryChanged = copy.m_inventoryChanged; + m_brain = copy.m_brain; + m_brain.setOwner( this ); + + // Mark the other turtle as having moved, and so its peripheral is dead. + copy.m_moveState = MoveState.MOVED; + copy.peripheral = CapabilityUtil.invalidate( copy.peripheral ); + } + + public IItemHandlerModifiable getItemHandler() + { + return m_itemHandler; + } + + @Nonnull + @Override + public LazyOptional getCapability( @Nonnull Capability cap, @Nullable Direction side ) + { + if( cap == ITEM_HANDLER_CAPABILITY ) + { + if( itemHandlerCap == null ) itemHandlerCap = LazyOptional.of( () -> new InvWrapper( this ) ); + return itemHandlerCap.cast(); + } + + if( cap == CAPABILITY_PERIPHERAL ) + { + if( hasMoved() ) return LazyOptional.empty(); + if( peripheral == null ) + { + peripheral = LazyOptional.of( () -> new ComputerPeripheral( "turtle", createProxy() ) ); + } + return peripheral.cast(); + } + + return super.getCapability( cap, side ); + } + + @Nullable + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory inventory, @Nonnull PlayerEntity player ) + { + return new ContainerTurtle( id, inventory, m_brain ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/InteractDirection.java b/src/main/java/dan200/computercraft/shared/turtle/core/InteractDirection.java new file mode 100644 index 000000000..cb60c8200 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/InteractDirection.java @@ -0,0 +1,30 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import net.minecraft.util.math.Direction; + +public enum InteractDirection +{ + FORWARD, + UP, + DOWN; + + public Direction toWorldDir( ITurtleAccess turtle ) + { + switch( this ) + { + case FORWARD: + default: + return turtle.getDirection(); + case UP: + return Direction.UP; + case DOWN: + return Direction.DOWN; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/MoveDirection.java b/src/main/java/dan200/computercraft/shared/turtle/core/MoveDirection.java new file mode 100644 index 000000000..a422e1b4a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/MoveDirection.java @@ -0,0 +1,33 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import net.minecraft.util.math.Direction; + +public enum MoveDirection +{ + FORWARD, + BACK, + UP, + DOWN; + + public Direction toWorldDir( ITurtleAccess turtle ) + { + switch( this ) + { + case FORWARD: + default: + return turtle.getDirection(); + case BACK: + return turtle.getDirection().getOpposite(); + case UP: + return Direction.UP; + case DOWN: + return Direction.DOWN; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurnDirection.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurnDirection.java new file mode 100644 index 000000000..cebc87c2b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurnDirection.java @@ -0,0 +1,12 @@ +/* + * 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.turtle.core; + +public enum TurnDirection +{ + LEFT, + RIGHT, +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java new file mode 100644 index 000000000..d858b28cf --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java @@ -0,0 +1,954 @@ +/* + * 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.turtle.core; + +import com.google.common.base.Objects; +import com.mojang.authlib.GameProfile; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.ILuaCallback; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.turtle.*; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.shared.TurtleUpgrades; +import dan200.computercraft.shared.computer.blocks.ComputerProxy; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.turtle.blocks.TileTurtle; +import dan200.computercraft.shared.util.Colour; +import dan200.computercraft.shared.util.Holiday; +import dan200.computercraft.shared.util.HolidayUtil; +import dan200.computercraft.shared.util.InventoryDelegate; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.MovementType; +import net.minecraft.fluid.FluidState; +import net.minecraft.inventory.Inventory; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.tag.FluidTags; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.common.util.Constants; +import net.minecraftforge.items.IItemHandlerModifiable; +import net.minecraftforge.items.wrapper.InvWrapper; + +import javax.annotation.Nonnull; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static dan200.computercraft.shared.common.IColouredItem.NBT_COLOUR; +import static dan200.computercraft.shared.util.WaterloggableHelpers.WATERLOGGED; + +public class TurtleBrain implements ITurtleAccess +{ + public static final String NBT_RIGHT_UPGRADE = "RightUpgrade"; + public static final String NBT_RIGHT_UPGRADE_DATA = "RightUpgradeNbt"; + public static final String NBT_LEFT_UPGRADE = "LeftUpgrade"; + public static final String NBT_LEFT_UPGRADE_DATA = "LeftUpgradeNbt"; + public static final String NBT_FUEL = "Fuel"; + public static final String NBT_OVERLAY = "Overlay"; + + private static final String NBT_SLOT = "Slot"; + + private static final int ANIM_DURATION = 8; + + private TileTurtle m_owner; + private ComputerProxy m_proxy; + private GameProfile m_owningPlayer; + + private final Inventory m_inventory = (InventoryDelegate) () -> m_owner; + private final IItemHandlerModifiable m_inventoryWrapper = new InvWrapper( m_inventory ); + + private final Queue m_commandQueue = new ArrayDeque<>(); + private int m_commandsIssued = 0; + + private final Map m_upgrades = new EnumMap<>( TurtleSide.class ); + private final Map peripherals = new EnumMap<>( TurtleSide.class ); + private final Map m_upgradeNBTData = new EnumMap<>( TurtleSide.class ); + + private int m_selectedSlot = 0; + private int m_fuelLevel = 0; + private int m_colourHex = -1; + private Identifier m_overlay = null; + + private TurtleAnimation m_animation = TurtleAnimation.NONE; + private int m_animationProgress = 0; + private int m_lastAnimationProgress = 0; + + TurtlePlayer m_cachedPlayer; + + public TurtleBrain( TileTurtle turtle ) + { + m_owner = turtle; + } + + public void setOwner( TileTurtle owner ) + { + m_owner = owner; + } + + public TileTurtle getOwner() + { + return m_owner; + } + + public ComputerProxy getProxy() + { + if( m_proxy == null ) m_proxy = new ComputerProxy( () -> m_owner ); + return m_proxy; + } + + public ComputerFamily getFamily() + { + return m_owner.getFamily(); + } + + public void setupComputer( ServerComputer computer ) + { + updatePeripherals( computer ); + } + + public void update() + { + World world = getWorld(); + if( !world.isClient ) + { + // Advance movement + updateCommands(); + } + + // Advance animation + updateAnimation(); + + // Advance upgrades + if( !m_upgrades.isEmpty() ) + { + for( Map.Entry entry : m_upgrades.entrySet() ) + { + entry.getValue().update( this, entry.getKey() ); + } + } + } + + /** + * Read common data for saving and client synchronisation. + * + * @param nbt The tag to read from + */ + private void readCommon( CompoundTag nbt ) + { + // Read fields + m_colourHex = nbt.contains( NBT_COLOUR ) ? nbt.getInt( NBT_COLOUR ) : -1; + m_fuelLevel = nbt.contains( NBT_FUEL ) ? nbt.getInt( NBT_FUEL ) : 0; + m_overlay = nbt.contains( NBT_OVERLAY ) ? new Identifier( nbt.getString( NBT_OVERLAY ) ) : null; + + // Read upgrades + setUpgrade( TurtleSide.LEFT, nbt.contains( NBT_LEFT_UPGRADE ) ? TurtleUpgrades.get( nbt.getString( NBT_LEFT_UPGRADE ) ) : null ); + setUpgrade( TurtleSide.RIGHT, nbt.contains( NBT_RIGHT_UPGRADE ) ? TurtleUpgrades.get( nbt.getString( NBT_RIGHT_UPGRADE ) ) : null ); + + // NBT + m_upgradeNBTData.clear(); + if( nbt.contains( NBT_LEFT_UPGRADE_DATA ) ) + { + m_upgradeNBTData.put( TurtleSide.LEFT, nbt.getCompound( NBT_LEFT_UPGRADE_DATA ).copy() ); + } + if( nbt.contains( NBT_RIGHT_UPGRADE_DATA ) ) + { + m_upgradeNBTData.put( TurtleSide.RIGHT, nbt.getCompound( NBT_RIGHT_UPGRADE_DATA ).copy() ); + } + } + + private void writeCommon( CompoundTag nbt ) + { + nbt.putInt( NBT_FUEL, m_fuelLevel ); + if( m_colourHex != -1 ) nbt.putInt( NBT_COLOUR, m_colourHex ); + if( m_overlay != null ) nbt.putString( NBT_OVERLAY, m_overlay.toString() ); + + // Write upgrades + String leftUpgradeId = getUpgradeId( getUpgrade( TurtleSide.LEFT ) ); + if( leftUpgradeId != null ) nbt.putString( NBT_LEFT_UPGRADE, leftUpgradeId ); + String rightUpgradeId = getUpgradeId( getUpgrade( TurtleSide.RIGHT ) ); + if( rightUpgradeId != null ) nbt.putString( NBT_RIGHT_UPGRADE, rightUpgradeId ); + + // Write upgrade NBT + if( m_upgradeNBTData.containsKey( TurtleSide.LEFT ) ) + { + nbt.put( NBT_LEFT_UPGRADE_DATA, getUpgradeNBTData( TurtleSide.LEFT ).copy() ); + } + if( m_upgradeNBTData.containsKey( TurtleSide.RIGHT ) ) + { + nbt.put( NBT_RIGHT_UPGRADE_DATA, getUpgradeNBTData( TurtleSide.RIGHT ).copy() ); + } + } + + public void readFromNBT( CompoundTag nbt ) + { + readCommon( nbt ); + + // Read state + m_selectedSlot = nbt.getInt( NBT_SLOT ); + + // Read owner + if( nbt.contains( "Owner", Constants.NBT.TAG_COMPOUND ) ) + { + CompoundTag owner = nbt.getCompound( "Owner" ); + m_owningPlayer = new GameProfile( + new UUID( owner.getLong( "UpperId" ), owner.getLong( "LowerId" ) ), + owner.getString( "Name" ) + ); + } + else + { + m_owningPlayer = null; + } + } + + public CompoundTag writeToNBT( CompoundTag nbt ) + { + writeCommon( nbt ); + + // Write state + nbt.putInt( NBT_SLOT, m_selectedSlot ); + + // Write owner + if( m_owningPlayer != null ) + { + CompoundTag owner = new CompoundTag(); + nbt.put( "Owner", owner ); + + owner.putLong( "UpperId", m_owningPlayer.getId().getMostSignificantBits() ); + owner.putLong( "LowerId", m_owningPlayer.getId().getLeastSignificantBits() ); + owner.putString( "Name", m_owningPlayer.getName() ); + } + + return nbt; + } + + private static String getUpgradeId( ITurtleUpgrade upgrade ) + { + return upgrade != null ? upgrade.getUpgradeID().toString() : null; + } + + public void readDescription( CompoundTag nbt ) + { + readCommon( nbt ); + + // Animation + TurtleAnimation anim = TurtleAnimation.values()[nbt.getInt( "Animation" )]; + if( anim != m_animation && + anim != TurtleAnimation.WAIT && + anim != TurtleAnimation.SHORT_WAIT && + anim != TurtleAnimation.NONE ) + { + m_animation = anim; + m_animationProgress = 0; + m_lastAnimationProgress = 0; + } + } + + public void writeDescription( CompoundTag nbt ) + { + writeCommon( nbt ); + nbt.putInt( "Animation", m_animation.ordinal() ); + } + + @Nonnull + @Override + public World getWorld() + { + return m_owner.getWorld(); + } + + @Nonnull + @Override + public BlockPos getPosition() + { + return m_owner.getPos(); + } + + @Override + public boolean teleportTo( @Nonnull World world, @Nonnull BlockPos pos ) + { + if( world.isClient || getWorld().isClient ) + { + throw new UnsupportedOperationException( "Cannot teleport on the client" ); + } + + // Cache info about the old turtle (so we don't access this after we delete ourselves) + World oldWorld = getWorld(); + TileTurtle oldOwner = m_owner; + BlockPos oldPos = m_owner.getPos(); + BlockState oldBlock = m_owner.getCachedState(); + + if( oldWorld == world && oldPos.equals( pos ) ) + { + // Teleporting to the current position is a no-op + return true; + } + + // Ensure the chunk is loaded + if( !world.isAreaLoaded( pos, 0 ) ) return false; + + // Ensure we're inside the world border + if( !world.getWorldBorder().contains( pos ) ) return false; + + FluidState existingFluid = world.getBlockState( pos ).getFluidState(); + BlockState newState = oldBlock + // We only mark this as waterlogged when travelling into a source block. This prevents us from spreading + // fluid by creating a new source when moving into a block, causing the next block to be almost full and + // then moving into that. + .with( WATERLOGGED, existingFluid.isIn( FluidTags.WATER ) && existingFluid.isStill() ); + + oldOwner.notifyMoveStart(); + + try + { + // Create a new turtle + if( world.setBlockState( pos, newState, 0 ) ) + { + Block block = world.getBlockState( pos ).getBlock(); + if( block == oldBlock.getBlock() ) + { + BlockEntity newTile = world.getBlockEntity( pos ); + if( newTile instanceof TileTurtle ) + { + // Copy the old turtle state into the new turtle + TileTurtle newTurtle = (TileTurtle) newTile; + newTurtle.setLocation( world, pos ); + newTurtle.transferStateFrom( oldOwner ); + newTurtle.createServerComputer().setWorld( world ); + newTurtle.createServerComputer().setPosition( pos ); + + // Remove the old turtle + oldWorld.removeBlock( oldPos, false ); + + // Make sure everybody knows about it + newTurtle.updateBlock(); + newTurtle.updateInput(); + newTurtle.updateOutput(); + return true; + } + } + + // Something went wrong, remove the newly created turtle + world.removeBlock( pos, false ); + } + } + finally + { + // whatever happens, unblock old turtle in case it's still in world + oldOwner.notifyMoveEnd(); + } + + return false; + } + + @Nonnull + @Override + public Vec3d getVisualPosition( float f ) + { + Vec3d offset = getRenderOffset( f ); + BlockPos pos = m_owner.getPos(); + return new Vec3d( + pos.getX() + 0.5 + offset.x, + pos.getY() + 0.5 + offset.y, + pos.getZ() + 0.5 + offset.z + ); + } + + @Override + public float getVisualYaw( float f ) + { + float yaw = getDirection().asRotation(); + switch( m_animation ) + { + case TURN_LEFT: + { + yaw += 90.0f * (1.0f - getAnimationFraction( f )); + if( yaw >= 360.0f ) + { + yaw -= 360.0f; + } + break; + } + case TURN_RIGHT: + { + yaw += -90.0f * (1.0f - getAnimationFraction( f )); + if( yaw < 0.0f ) + { + yaw += 360.0f; + } + break; + } + } + return yaw; + } + + @Nonnull + @Override + public Direction getDirection() + { + return m_owner.getDirection(); + } + + @Override + public void setDirection( @Nonnull Direction dir ) + { + m_owner.setDirection( dir ); + } + + @Override + public int getSelectedSlot() + { + return m_selectedSlot; + } + + @Override + public void setSelectedSlot( int slot ) + { + if( getWorld().isClient ) throw new UnsupportedOperationException( "Cannot set the slot on the client" ); + + if( slot >= 0 && slot < m_owner.size() ) + { + m_selectedSlot = slot; + m_owner.onTileEntityChange(); + } + } + + @Nonnull + @Override + public Inventory getInventory() + { + return m_inventory; + } + + @Nonnull + @Override + public IItemHandlerModifiable getItemHandler() + { + return m_inventoryWrapper; + } + + @Override + public boolean isFuelNeeded() + { + return ComputerCraft.turtlesNeedFuel; + } + + @Override + public int getFuelLevel() + { + return Math.min( m_fuelLevel, getFuelLimit() ); + } + + @Override + public void setFuelLevel( int level ) + { + m_fuelLevel = Math.min( level, getFuelLimit() ); + m_owner.onTileEntityChange(); + } + + @Override + public int getFuelLimit() + { + if( m_owner.getFamily() == ComputerFamily.ADVANCED ) + { + return ComputerCraft.advancedTurtleFuelLimit; + } + else + { + return ComputerCraft.turtleFuelLimit; + } + } + + @Override + public boolean consumeFuel( int fuel ) + { + if( getWorld().isClient ) throw new UnsupportedOperationException( "Cannot consume fuel on the client" ); + + if( !isFuelNeeded() ) return true; + + int consumption = Math.max( fuel, 0 ); + if( getFuelLevel() >= consumption ) + { + setFuelLevel( getFuelLevel() - consumption ); + return true; + } + return false; + } + + @Override + public void addFuel( int fuel ) + { + if( getWorld().isClient ) throw new UnsupportedOperationException( "Cannot add fuel on the client" ); + + int addition = Math.max( fuel, 0 ); + setFuelLevel( getFuelLevel() + addition ); + } + + private int issueCommand( ITurtleCommand command ) + { + m_commandQueue.offer( new TurtleCommandQueueEntry( ++m_commandsIssued, command ) ); + return m_commandsIssued; + } + + @Nonnull + @Override + public MethodResult executeCommand( @Nonnull ITurtleCommand command ) + { + if( getWorld().isClient ) throw new UnsupportedOperationException( "Cannot run commands on the client" ); + + // Issue command + int commandID = issueCommand( command ); + return new CommandCallback( commandID ).pull; + } + + @Override + public void playAnimation( @Nonnull TurtleAnimation animation ) + { + if( getWorld().isClient ) throw new UnsupportedOperationException( "Cannot play animations on the client" ); + + m_animation = animation; + if( m_animation == TurtleAnimation.SHORT_WAIT ) + { + m_animationProgress = ANIM_DURATION / 2; + m_lastAnimationProgress = ANIM_DURATION / 2; + } + else + { + m_animationProgress = 0; + m_lastAnimationProgress = 0; + } + m_owner.updateBlock(); + } + + public Identifier getOverlay() + { + return m_overlay; + } + + public void setOverlay( Identifier overlay ) + { + if( !Objects.equal( m_overlay, overlay ) ) + { + m_overlay = overlay; + m_owner.updateBlock(); + } + } + + public DyeColor getDyeColour() + { + if( m_colourHex == -1 ) return null; + Colour colour = Colour.fromHex( m_colourHex ); + return colour == null ? null : DyeColor.byId( 15 - colour.ordinal() ); + } + + public void setDyeColour( DyeColor dyeColour ) + { + int newColour = -1; + if( dyeColour != null ) + { + newColour = Colour.values()[15 - dyeColour.getId()].getHex(); + } + if( m_colourHex != newColour ) + { + m_colourHex = newColour; + m_owner.updateBlock(); + } + } + + @Override + public void setColour( int colour ) + { + if( colour >= 0 && colour <= 0xFFFFFF ) + { + if( m_colourHex != colour ) + { + m_colourHex = colour; + m_owner.updateBlock(); + } + } + else if( m_colourHex != -1 ) + { + m_colourHex = -1; + m_owner.updateBlock(); + } + } + + @Override + public int getColour() + { + return m_colourHex; + } + + public void setOwningPlayer( GameProfile profile ) + { + m_owningPlayer = profile; + } + + @Nonnull + @Override + public GameProfile getOwningPlayer() + { + return m_owningPlayer; + } + + @Override + public ITurtleUpgrade getUpgrade( @Nonnull TurtleSide side ) + { + return m_upgrades.get( side ); + } + + @Override + public void setUpgrade( @Nonnull TurtleSide side, ITurtleUpgrade upgrade ) + { + // Remove old upgrade + if( m_upgrades.containsKey( side ) ) + { + if( m_upgrades.get( side ) == upgrade ) return; + m_upgrades.remove( side ); + } + else + { + if( upgrade == null ) return; + } + + m_upgradeNBTData.remove( side ); + + // Set new upgrade + if( upgrade != null ) m_upgrades.put( side, upgrade ); + + // Notify clients and create peripherals + if( m_owner.getWorld() != null ) + { + updatePeripherals( m_owner.createServerComputer() ); + m_owner.updateBlock(); + } + } + + @Override + public IPeripheral getPeripheral( @Nonnull TurtleSide side ) + { + return peripherals.get( side ); + } + + @Nonnull + @Override + public CompoundTag getUpgradeNBTData( TurtleSide side ) + { + CompoundTag nbt = m_upgradeNBTData.get( side ); + if( nbt == null ) m_upgradeNBTData.put( side, nbt = new CompoundTag() ); + return nbt; + } + + @Override + public void updateUpgradeNBTData( @Nonnull TurtleSide side ) + { + m_owner.updateBlock(); + } + + public Vec3d getRenderOffset( float f ) + { + switch( m_animation ) + { + case MOVE_FORWARD: + case MOVE_BACK: + case MOVE_UP: + case MOVE_DOWN: + { + // Get direction + Direction dir; + switch( m_animation ) + { + case MOVE_FORWARD: + default: + dir = getDirection(); + break; + case MOVE_BACK: + dir = getDirection().getOpposite(); + break; + case MOVE_UP: + dir = Direction.UP; + break; + case MOVE_DOWN: + dir = Direction.DOWN; + break; + } + + double distance = -1.0 + getAnimationFraction( f ); + return new Vec3d( + distance * dir.getOffsetX(), + distance * dir.getOffsetY(), + distance * dir.getOffsetZ() + ); + } + default: + { + return Vec3d.ZERO; + } + } + } + + public float getToolRenderAngle( TurtleSide side, float f ) + { + return (side == TurtleSide.LEFT && m_animation == TurtleAnimation.SWING_LEFT_TOOL) || + (side == TurtleSide.RIGHT && m_animation == TurtleAnimation.SWING_RIGHT_TOOL) + ? 45.0f * (float) Math.sin( getAnimationFraction( f ) * Math.PI ) + : 0.0f; + } + + private static ComputerSide toDirection( TurtleSide side ) + { + switch( side ) + { + case LEFT: + return ComputerSide.LEFT; + case RIGHT: + default: + return ComputerSide.RIGHT; + } + } + + private void updatePeripherals( ServerComputer serverComputer ) + { + if( serverComputer == null ) return; + + // Update peripherals + for( TurtleSide side : TurtleSide.values() ) + { + ITurtleUpgrade upgrade = getUpgrade( side ); + IPeripheral peripheral = null; + if( upgrade != null && upgrade.getType().isPeripheral() ) + { + peripheral = upgrade.createPeripheral( this, side ); + } + + IPeripheral existing = peripherals.get( side ); + if( existing == peripheral || (existing != null && peripheral != null && existing.equals( peripheral )) ) + { + // If the peripheral is the same, just use that. + peripheral = existing; + } + else + { + // Otherwise update our map + peripherals.put( side, peripheral ); + } + + // Always update the computer: it may not be the same computer as before! + serverComputer.setPeripheral( toDirection( side ), peripheral ); + } + } + + private void updateCommands() + { + if( m_animation != TurtleAnimation.NONE || m_commandQueue.isEmpty() ) return; + + // If we've got a computer, ensure that we're allowed to perform work. + ServerComputer computer = m_owner.getServerComputer(); + if( computer != null && !computer.getComputer().getMainThreadMonitor().canWork() ) return; + + // Pull a new command + TurtleCommandQueueEntry nextCommand = m_commandQueue.poll(); + if( nextCommand == null ) return; + + // Execute the command + long start = System.nanoTime(); + TurtleCommandResult result = nextCommand.command.execute( this ); + long end = System.nanoTime(); + + // Dispatch the callback + if( computer == null ) return; + computer.getComputer().getMainThreadMonitor().trackWork( end - start, TimeUnit.NANOSECONDS ); + int callbackID = nextCommand.callbackID; + if( callbackID < 0 ) return; + + if( result != null && result.isSuccess() ) + { + Object[] results = result.getResults(); + if( results != null ) + { + Object[] arguments = new Object[results.length + 2]; + arguments[0] = callbackID; + arguments[1] = true; + System.arraycopy( results, 0, arguments, 2, results.length ); + computer.queueEvent( "turtle_response", arguments ); + } + else + { + computer.queueEvent( "turtle_response", new Object[] { + callbackID, true, + } ); + } + } + else + { + computer.queueEvent( "turtle_response", new Object[] { + callbackID, false, result != null ? result.getErrorMessage() : null, + } ); + } + } + + private void updateAnimation() + { + if( m_animation != TurtleAnimation.NONE ) + { + World world = getWorld(); + + if( ComputerCraft.turtlesCanPush ) + { + // Advance entity pushing + if( m_animation == TurtleAnimation.MOVE_FORWARD || + m_animation == TurtleAnimation.MOVE_BACK || + m_animation == TurtleAnimation.MOVE_UP || + m_animation == TurtleAnimation.MOVE_DOWN ) + { + BlockPos pos = getPosition(); + Direction moveDir; + switch( m_animation ) + { + case MOVE_FORWARD: + default: + moveDir = getDirection(); + break; + case MOVE_BACK: + moveDir = getDirection().getOpposite(); + break; + case MOVE_UP: + moveDir = Direction.UP; + break; + case MOVE_DOWN: + moveDir = Direction.DOWN; + break; + } + + double minX = pos.getX(); + double minY = pos.getY(); + double minZ = pos.getZ(); + double maxX = minX + 1.0; + double maxY = minY + 1.0; + double maxZ = minZ + 1.0; + + float pushFrac = 1.0f - (float) (m_animationProgress + 1) / ANIM_DURATION; + float push = Math.max( pushFrac + 0.0125f, 0.0f ); + if( moveDir.getOffsetX() < 0 ) + { + minX += moveDir.getOffsetX() * push; + } + else + { + maxX -= moveDir.getOffsetX() * push; + } + + if( moveDir.getOffsetY() < 0 ) + { + minY += moveDir.getOffsetY() * push; + } + else + { + maxY -= moveDir.getOffsetY() * push; + } + + if( moveDir.getOffsetZ() < 0 ) + { + minZ += moveDir.getOffsetZ() * push; + } + else + { + maxZ -= moveDir.getOffsetZ() * push; + } + + Box aabb = new Box( minX, minY, minZ, maxX, maxY, maxZ ); + List list = world.getEntitiesByClass( Entity.class, aabb, EntityPredicates.EXCEPT_SPECTATOR ); + if( !list.isEmpty() ) + { + double pushStep = 1.0f / ANIM_DURATION; + double pushStepX = moveDir.getOffsetX() * pushStep; + double pushStepY = moveDir.getOffsetY() * pushStep; + double pushStepZ = moveDir.getOffsetZ() * pushStep; + for( Entity entity : list ) + { + entity.move( MovementType.PISTON, new Vec3d( pushStepX, pushStepY, pushStepZ ) ); + } + } + } + } + + // Advance valentines day easter egg + if( world.isClient && m_animation == TurtleAnimation.MOVE_FORWARD && m_animationProgress == 4 ) + { + // Spawn love pfx if valentines day + Holiday currentHoliday = HolidayUtil.getCurrentHoliday(); + if( currentHoliday == Holiday.VALENTINES ) + { + Vec3d position = getVisualPosition( 1.0f ); + if( position != null ) + { + double x = position.x + world.random.nextGaussian() * 0.1; + double y = position.y + 0.5 + world.random.nextGaussian() * 0.1; + double z = position.z + world.random.nextGaussian() * 0.1; + world.addParticle( + ParticleTypes.HEART, x, y, z, + world.random.nextGaussian() * 0.02, + world.random.nextGaussian() * 0.02, + world.random.nextGaussian() * 0.02 + ); + } + } + } + + // Wait for anim completion + m_lastAnimationProgress = m_animationProgress; + if( ++m_animationProgress >= ANIM_DURATION ) + { + m_animation = TurtleAnimation.NONE; + m_animationProgress = 0; + m_lastAnimationProgress = 0; + } + } + } + + private float getAnimationFraction( float f ) + { + float next = (float) m_animationProgress / ANIM_DURATION; + float previous = (float) m_lastAnimationProgress / ANIM_DURATION; + return previous + (next - previous) * f; + } + + private static final class CommandCallback implements ILuaCallback + { + final MethodResult pull = MethodResult.pullEvent( "turtle_response", this ); + private final int command; + + CommandCallback( int command ) + { + this.command = command; + } + + @Nonnull + @Override + public MethodResult resume( Object[] response ) + { + if( response.length < 3 || !(response[1] instanceof Number) || !(response[2] instanceof Boolean) ) + { + return pull; + } + + if( ((Number) response[1]).intValue() != command ) return pull; + + return MethodResult.of( Arrays.copyOfRange( response, 2, response.length ) ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCommandQueueEntry.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCommandQueueEntry.java new file mode 100644 index 000000000..64a2aa655 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCommandQueueEntry.java @@ -0,0 +1,20 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleCommand; + +public class TurtleCommandQueueEntry +{ + public final int callbackID; + public final ITurtleCommand command; + + public TurtleCommandQueueEntry( int callbackID, ITurtleCommand command ) + { + this.callbackID = callbackID; + this.command = command; + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareCommand.java new file mode 100644 index 000000000..965007222 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareCommand.java @@ -0,0 +1,99 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.item.ItemStack; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.fml.common.ObfuscationReflectionHelper; + +import javax.annotation.Nonnull; +import java.lang.reflect.Method; +import java.util.List; + +public class TurtleCompareCommand implements ITurtleCommand +{ + private final InteractDirection m_direction; + + public TurtleCompareCommand( InteractDirection direction ) + { + m_direction = direction; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Get world direction from direction + Direction direction = m_direction.toWorldDir( turtle ); + + // Get currently selected stack + ItemStack selectedStack = turtle.getInventory().getStack( turtle.getSelectedSlot() ); + + // Get stack representing thing in front + World world = turtle.getWorld(); + BlockPos oldPosition = turtle.getPosition(); + BlockPos newPosition = oldPosition.offset( direction ); + + ItemStack lookAtStack = ItemStack.EMPTY; + if( !world.isAir( newPosition ) ) + { + BlockState lookAtState = world.getBlockState( newPosition ); + Block lookAtBlock = lookAtState.getBlock(); + if( !lookAtBlock.isAir( lookAtState, world, newPosition ) ) + { + // Try getSilkTouchDrop first + if( !lookAtBlock.hasTileEntity( lookAtState ) ) + { + try + { + Method method = ObfuscationReflectionHelper.findMethod( Block.class, "func_180643_i", BlockState.class ); + lookAtStack = (ItemStack) method.invoke( lookAtBlock, lookAtState ); + } + catch( ReflectiveOperationException | RuntimeException ignored ) + { + } + } + + // See if the block drops anything with the same ID as itself + // (try 5 times to try and beat random number generators) + for( int i = 0; i < 5 && lookAtStack.isEmpty(); i++ ) + { + List drops = Block.getDroppedStacks( lookAtState, (ServerWorld) world, newPosition, world.getBlockEntity( newPosition ) ); + if( !drops.isEmpty() ) + { + for( ItemStack drop : drops ) + { + if( drop.getItem() == lookAtBlock.asItem() ) + { + lookAtStack = drop; + break; + } + } + } + } + + // Last resort: roll our own (which will probably be wrong) + if( lookAtStack.isEmpty() ) + { + lookAtStack = new ItemStack( lookAtBlock ); + } + } + } + + // Compare them + return selectedStack.getItem() == lookAtStack.getItem() + ? TurtleCommandResult.success() + : TurtleCommandResult.failure(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareToCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareToCommand.java new file mode 100644 index 000000000..15e0cf222 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareToCommand.java @@ -0,0 +1,40 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.shared.util.InventoryUtil; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; + +public class TurtleCompareToCommand implements ITurtleCommand +{ + private final int m_slot; + + public TurtleCompareToCommand( int slot ) + { + m_slot = slot; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + ItemStack selectedStack = turtle.getInventory().getStack( turtle.getSelectedSlot() ); + ItemStack stack = turtle.getInventory().getStack( m_slot ); + if( InventoryUtil.areItemsStackable( selectedStack, stack ) ) + { + return TurtleCommandResult.success(); + } + else + { + return TurtleCommandResult.failure(); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCraftCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCraftCommand.java new file mode 100644 index 000000000..5d2ed6597 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCraftCommand.java @@ -0,0 +1,51 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleAnimation; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.shared.turtle.upgrades.TurtleInventoryCrafting; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; +import java.util.List; + +public class TurtleCraftCommand implements ITurtleCommand +{ + private final int limit; + + public TurtleCraftCommand( int limit ) + { + this.limit = limit; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Craft the item + TurtleInventoryCrafting crafting = new TurtleInventoryCrafting( turtle ); + List results = crafting.doCrafting( turtle.getWorld(), limit ); + if( results == null ) return TurtleCommandResult.failure( "No matching recipes" ); + + // Store or drop any remainders + for( ItemStack stack : results ) + { + ItemStack remainder = InventoryUtil.storeItems( stack, turtle.getItemHandler(), turtle.getSelectedSlot() ); + if( !remainder.isEmpty() ) + { + WorldUtil.dropItemStack( remainder, turtle.getWorld(), turtle.getPosition(), turtle.getDirection() ); + } + } + + if( !results.isEmpty() ) turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDetectCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDetectCommand.java new file mode 100644 index 000000000..c1b35cba7 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDetectCommand.java @@ -0,0 +1,43 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public class TurtleDetectCommand implements ITurtleCommand +{ + private final InteractDirection m_direction; + + public TurtleDetectCommand( InteractDirection direction ) + { + m_direction = direction; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Get world direction from direction + Direction direction = m_direction.toWorldDir( turtle ); + + // Check if thing in front is air or not + World world = turtle.getWorld(); + BlockPos oldPosition = turtle.getPosition(); + BlockPos newPosition = oldPosition.offset( direction ); + + return !WorldUtil.isLiquidBlock( world, newPosition ) && !world.isAir( newPosition ) + ? TurtleCommandResult.success() + : TurtleCommandResult.failure(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java new file mode 100644 index 000000000..b95aa31f7 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java @@ -0,0 +1,103 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleAnimation; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.api.turtle.event.TurtleInventoryEvent; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.items.IItemHandler; + +import javax.annotation.Nonnull; + +public class TurtleDropCommand implements ITurtleCommand +{ + private final InteractDirection m_direction; + private final int m_quantity; + + public TurtleDropCommand( InteractDirection direction, int quantity ) + { + m_direction = direction; + m_quantity = quantity; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Dropping nothing is easy + if( m_quantity == 0 ) + { + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + + // Get world direction from direction + Direction direction = m_direction.toWorldDir( turtle ); + + // Get things to drop + ItemStack stack = InventoryUtil.takeItems( m_quantity, turtle.getItemHandler(), turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() ); + if( stack.isEmpty() ) + { + return TurtleCommandResult.failure( "No items to drop" ); + } + + // Get inventory for thing in front + World world = turtle.getWorld(); + BlockPos oldPosition = turtle.getPosition(); + BlockPos newPosition = oldPosition.offset( direction ); + Direction side = direction.getOpposite(); + + IItemHandler inventory = InventoryUtil.getInventory( world, newPosition, side ); + + // Fire the event, restoring the inventory and exiting if it is cancelled. + TurtlePlayer player = TurtlePlaceCommand.createPlayer( turtle, oldPosition, direction ); + TurtleInventoryEvent.Drop event = new TurtleInventoryEvent.Drop( turtle, player, world, newPosition, inventory, stack ); + if( MinecraftForge.EVENT_BUS.post( event ) ) + { + InventoryUtil.storeItems( stack, turtle.getItemHandler(), turtle.getSelectedSlot() ); + return TurtleCommandResult.failure( event.getFailureMessage() ); + } + + if( inventory != null ) + { + // Drop the item into the inventory + ItemStack remainder = InventoryUtil.storeItems( stack, inventory ); + if( !remainder.isEmpty() ) + { + // Put the remainder back in the turtle + InventoryUtil.storeItems( remainder, turtle.getItemHandler(), turtle.getSelectedSlot() ); + } + + // Return true if we stored anything + if( remainder != stack ) + { + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + else + { + return TurtleCommandResult.failure( "No space for items" ); + } + } + else + { + // Drop the item into the world + WorldUtil.dropItemStack( stack, world, oldPosition, direction ); + world.syncGlobalEvent( 1000, newPosition, 0 ); + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java new file mode 100644 index 000000000..7c4990f35 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.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.shared.turtle.core; + +import dan200.computercraft.api.turtle.*; +import dan200.computercraft.api.turtle.event.TurtleAction; +import dan200.computercraft.api.turtle.event.TurtleActionEvent; +import dan200.computercraft.shared.TurtleUpgrades; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.items.IItemHandler; + +import javax.annotation.Nonnull; + +public class TurtleEquipCommand implements ITurtleCommand +{ + private final TurtleSide m_side; + + public TurtleEquipCommand( TurtleSide side ) + { + m_side = side; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Determine the upgrade to equipLeft + ITurtleUpgrade newUpgrade; + ItemStack newUpgradeStack; + IItemHandler inventory = turtle.getItemHandler(); + ItemStack selectedStack = inventory.getStackInSlot( turtle.getSelectedSlot() ); + if( !selectedStack.isEmpty() ) + { + newUpgradeStack = selectedStack.copy(); + newUpgrade = TurtleUpgrades.get( newUpgradeStack ); + if( newUpgrade == null || !TurtleUpgrades.suitableForFamily( ((TurtleBrain) turtle).getFamily(), newUpgrade ) ) + { + return TurtleCommandResult.failure( "Not a valid upgrade" ); + } + } + else + { + newUpgradeStack = null; + newUpgrade = null; + } + + // Determine the upgrade to replace + ItemStack oldUpgradeStack; + ITurtleUpgrade oldUpgrade = turtle.getUpgrade( m_side ); + if( oldUpgrade != null ) + { + ItemStack craftingItem = oldUpgrade.getCraftingItem(); + oldUpgradeStack = !craftingItem.isEmpty() ? craftingItem.copy() : null; + } + else + { + oldUpgradeStack = null; + } + + TurtleActionEvent event = new TurtleActionEvent( turtle, TurtleAction.EQUIP ); + if( MinecraftForge.EVENT_BUS.post( event ) ) + { + return TurtleCommandResult.failure( event.getFailureMessage() ); + } + + // Do the swapping: + if( newUpgradeStack != null ) + { + // Consume new upgrades item + InventoryUtil.takeItems( 1, inventory, turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() ); + } + if( oldUpgradeStack != null ) + { + // Store old upgrades item + ItemStack remainder = InventoryUtil.storeItems( oldUpgradeStack, inventory, turtle.getSelectedSlot() ); + if( !remainder.isEmpty() ) + { + // If there's no room for the items, drop them + BlockPos position = turtle.getPosition(); + WorldUtil.dropItemStack( remainder, turtle.getWorld(), position, turtle.getDirection() ); + } + } + turtle.setUpgrade( m_side, newUpgrade ); + + // Animate + if( newUpgrade != null || oldUpgrade != null ) + { + turtle.playAnimation( TurtleAnimation.WAIT ); + } + + return TurtleCommandResult.success(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java new file mode 100644 index 000000000..c5e2b50c9 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java @@ -0,0 +1,60 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.api.turtle.event.TurtleBlockEvent; +import dan200.computercraft.shared.peripheral.generic.data.BlockData; +import net.minecraft.block.BlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.common.MinecraftForge; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; + +public class TurtleInspectCommand implements ITurtleCommand +{ + private final InteractDirection direction; + + public TurtleInspectCommand( InteractDirection direction ) + { + this.direction = direction; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Get world direction from direction + Direction direction = this.direction.toWorldDir( turtle ); + + // Check if thing in front is air or not + World world = turtle.getWorld(); + BlockPos oldPosition = turtle.getPosition(); + BlockPos newPosition = oldPosition.offset( direction ); + + BlockState state = world.getBlockState( newPosition ); + if( state.getBlock().isAir( state, world, newPosition ) ) + { + return TurtleCommandResult.failure( "No block to inspect" ); + } + + Map table = BlockData.fill( new HashMap<>(), state ); + + // Fire the event, exiting if it is cancelled + TurtlePlayer turtlePlayer = TurtlePlaceCommand.createPlayer( turtle, oldPosition, direction ); + TurtleBlockEvent.Inspect event = new TurtleBlockEvent.Inspect( turtle, turtlePlayer, world, newPosition, state, table ); + if( MinecraftForge.EVENT_BUS.post( event ) ) return TurtleCommandResult.failure( event.getFailureMessage() ); + + return TurtleCommandResult.success( new Object[] { table } ); + + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java new file mode 100644 index 000000000..bd6f89301 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java @@ -0,0 +1,163 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleAnimation; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.api.turtle.event.TurtleBlockEvent; +import dan200.computercraft.shared.TurtlePermissions; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.block.BlockState; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; +import net.minecraft.world.World; +import net.minecraftforge.common.MinecraftForge; + +import javax.annotation.Nonnull; +import java.util.List; + +public class TurtleMoveCommand implements ITurtleCommand +{ + private final MoveDirection m_direction; + + public TurtleMoveCommand( MoveDirection direction ) + { + m_direction = direction; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Get world direction from direction + Direction direction = m_direction.toWorldDir( turtle ); + + // Check if we can move + World oldWorld = turtle.getWorld(); + BlockPos oldPosition = turtle.getPosition(); + BlockPos newPosition = oldPosition.offset( direction ); + + TurtlePlayer turtlePlayer = TurtlePlaceCommand.createPlayer( turtle, oldPosition, direction ); + TurtleCommandResult canEnterResult = canEnter( turtlePlayer, oldWorld, newPosition ); + if( !canEnterResult.isSuccess() ) + { + return canEnterResult; + } + + // Check existing block is air or replaceable + BlockState state = oldWorld.getBlockState( newPosition ); + if( !oldWorld.isAir( newPosition ) && + !WorldUtil.isLiquidBlock( oldWorld, newPosition ) && + !state.getMaterial().isReplaceable() ) + { + return TurtleCommandResult.failure( "Movement obstructed" ); + } + + // Check there isn't anything in the way + VoxelShape collision = state.getCollisionShape( oldWorld, oldPosition ).offset( + newPosition.getX(), + newPosition.getY(), + newPosition.getZ() + ); + + if( !oldWorld.intersectsEntities( null, collision ) ) + { + if( !ComputerCraft.turtlesCanPush || m_direction == MoveDirection.UP || m_direction == MoveDirection.DOWN ) + { + return TurtleCommandResult.failure( "Movement obstructed" ); + } + + // Check there is space for all the pushable entities to be pushed + List list = oldWorld.getEntitiesByClass( Entity.class, getBox( collision ), x -> x != null && x.isAlive() && x.inanimate ); + for( Entity entity : list ) + { + Box pushedBB = entity.getBoundingBox().offset( + direction.getOffsetX(), + direction.getOffsetY(), + direction.getOffsetZ() + ); + if( !oldWorld.intersectsEntities( null, VoxelShapes.cuboid( pushedBB ) ) ) + { + return TurtleCommandResult.failure( "Movement obstructed" ); + } + } + } + + TurtleBlockEvent.Move moveEvent = new TurtleBlockEvent.Move( turtle, turtlePlayer, oldWorld, newPosition ); + if( MinecraftForge.EVENT_BUS.post( moveEvent ) ) + { + return TurtleCommandResult.failure( moveEvent.getFailureMessage() ); + } + + // Check fuel level + if( turtle.isFuelNeeded() && turtle.getFuelLevel() < 1 ) + { + return TurtleCommandResult.failure( "Out of fuel" ); + } + + // Move + if( !turtle.teleportTo( oldWorld, newPosition ) ) return TurtleCommandResult.failure( "Movement failed" ); + + // Consume fuel + turtle.consumeFuel( 1 ); + + // Animate + switch( m_direction ) + { + case FORWARD: + default: + turtle.playAnimation( TurtleAnimation.MOVE_FORWARD ); + break; + case BACK: + turtle.playAnimation( TurtleAnimation.MOVE_BACK ); + break; + case UP: + turtle.playAnimation( TurtleAnimation.MOVE_UP ); + break; + case DOWN: + turtle.playAnimation( TurtleAnimation.MOVE_DOWN ); + break; + } + return TurtleCommandResult.success(); + } + + private static TurtleCommandResult canEnter( TurtlePlayer turtlePlayer, World world, BlockPos position ) + { + if( World.isHeightInvalid( position ) ) + { + return TurtleCommandResult.failure( position.getY() < 0 ? "Too low to move" : "Too high to move" ); + } + if( !World.method_24794( position ) ) return TurtleCommandResult.failure( "Cannot leave the world" ); + + // Check spawn protection + if( ComputerCraft.turtlesObeyBlockProtection && !TurtlePermissions.isBlockEnterable( world, position, turtlePlayer ) ) + { + return TurtleCommandResult.failure( "Cannot enter protected area" ); + } + + if( !world.isAreaLoaded( position, 0 ) ) return TurtleCommandResult.failure( "Cannot leave loaded world" ); + if( !world.getWorldBorder().contains( position ) ) + { + return TurtleCommandResult.failure( "Cannot pass the world border" ); + } + + return TurtleCommandResult.success(); + } + + private static Box getBox( VoxelShape shape ) + { + return shape.isEmpty() ? EMPTY_BOX : shape.getBoundingBox(); + } + + private static final Box EMPTY_BOX = new Box( 0, 0, 0, 0, 0, 0 ); +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java new file mode 100644 index 000000000..9973c56dd --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java @@ -0,0 +1,445 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleAnimation; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.api.turtle.event.TurtleBlockEvent; +import dan200.computercraft.shared.TurtlePermissions; +import dan200.computercraft.shared.util.DirectionUtil; +import dan200.computercraft.shared.util.DropConsumer; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.SignBlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.*; +import net.minecraft.text.LiteralText; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.TypedActionResult; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.common.ForgeHooks; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.PlayerInteractEvent; +import net.minecraftforge.eventbus.api.Event; +import org.apache.commons.lang3.tuple.Pair; + +import javax.annotation.Nonnull; +import java.util.List; + +public class TurtlePlaceCommand implements ITurtleCommand +{ + private final InteractDirection m_direction; + private final Object[] m_extraArguments; + + public TurtlePlaceCommand( InteractDirection direction, Object[] arguments ) + { + m_direction = direction; + m_extraArguments = arguments; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Get thing to place + ItemStack stack = turtle.getInventory().getStack( turtle.getSelectedSlot() ); + if( stack.isEmpty() ) + { + return TurtleCommandResult.failure( "No items to place" ); + } + + // Remember old block + Direction direction = m_direction.toWorldDir( turtle ); + BlockPos coordinates = turtle.getPosition().offset( direction ); + + // Create a fake player, and orient it appropriately + BlockPos playerPosition = turtle.getPosition().offset( direction ); + TurtlePlayer turtlePlayer = createPlayer( turtle, playerPosition, direction ); + + TurtleBlockEvent.Place place = new TurtleBlockEvent.Place( turtle, turtlePlayer, turtle.getWorld(), coordinates, stack ); + if( MinecraftForge.EVENT_BUS.post( place ) ) + { + return TurtleCommandResult.failure( place.getFailureMessage() ); + } + + // Do the deploying + String[] errorMessage = new String[1]; + ItemStack remainder = deploy( stack, turtle, turtlePlayer, direction, m_extraArguments, errorMessage ); + if( remainder != stack ) + { + // Put the remaining items back + turtle.getInventory().setStack( turtle.getSelectedSlot(), remainder ); + turtle.getInventory().markDirty(); + + // Animate and return success + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + else + { + if( errorMessage[0] != null ) + { + return TurtleCommandResult.failure( errorMessage[0] ); + } + else if( stack.getItem() instanceof BlockItem ) + { + return TurtleCommandResult.failure( "Cannot place block here" ); + } + else + { + return TurtleCommandResult.failure( "Cannot place item here" ); + } + } + } + + public static ItemStack deploy( @Nonnull ItemStack stack, ITurtleAccess turtle, Direction direction, Object[] extraArguments, String[] outErrorMessage ) + { + // Create a fake player, and orient it appropriately + BlockPos playerPosition = turtle.getPosition().offset( direction ); + TurtlePlayer turtlePlayer = createPlayer( turtle, playerPosition, direction ); + + return deploy( stack, turtle, turtlePlayer, direction, extraArguments, outErrorMessage ); + } + + public static ItemStack deploy( @Nonnull ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, Direction direction, Object[] extraArguments, String[] outErrorMessage ) + { + // Deploy on an entity + ItemStack remainder = deployOnEntity( stack, turtle, turtlePlayer, direction, extraArguments, outErrorMessage ); + if( remainder != stack ) + { + return remainder; + } + + // Deploy on the block immediately in front + BlockPos position = turtle.getPosition(); + BlockPos newPosition = position.offset( direction ); + remainder = deployOnBlock( stack, turtle, turtlePlayer, newPosition, direction.getOpposite(), extraArguments, true, outErrorMessage ); + if( remainder != stack ) + { + return remainder; + } + + // Deploy on the block one block away + remainder = deployOnBlock( stack, turtle, turtlePlayer, newPosition.offset( direction ), direction.getOpposite(), extraArguments, false, outErrorMessage ); + if( remainder != stack ) + { + return remainder; + } + + if( direction.getAxis() != Direction.Axis.Y ) + { + // Deploy down on the block in front + remainder = deployOnBlock( stack, turtle, turtlePlayer, newPosition.down(), Direction.UP, extraArguments, false, outErrorMessage ); + if( remainder != stack ) + { + return remainder; + } + } + + // Deploy back onto the turtle + remainder = deployOnBlock( stack, turtle, turtlePlayer, position, direction, extraArguments, false, outErrorMessage ); + if( remainder != stack ) + { + return remainder; + } + + // If nothing worked, return the original stack unchanged + return stack; + } + + public static TurtlePlayer createPlayer( ITurtleAccess turtle, BlockPos position, Direction direction ) + { + TurtlePlayer turtlePlayer = TurtlePlayer.get( turtle ); + orientPlayer( turtle, turtlePlayer, position, direction ); + return turtlePlayer; + } + + private static void orientPlayer( ITurtleAccess turtle, TurtlePlayer turtlePlayer, BlockPos position, Direction direction ) + { + double posX = position.getX() + 0.5; + double posY = position.getY() + 0.5; + double posZ = position.getZ() + 0.5; + + // Stop intersection with the turtle itself + if( turtle.getPosition().equals( position ) ) + { + posX += 0.48 * direction.getOffsetX(); + posY += 0.48 * direction.getOffsetY(); + posZ += 0.48 * direction.getOffsetZ(); + } + + if( direction.getAxis() != Direction.Axis.Y ) + { + turtlePlayer.yaw = direction.asRotation(); + turtlePlayer.pitch = 0.0f; + } + else + { + turtlePlayer.yaw = turtle.getDirection().asRotation(); + turtlePlayer.pitch = DirectionUtil.toPitchAngle( direction ); + } + + turtlePlayer.setPos( posX, posY, posZ ); + turtlePlayer.prevX = posX; + turtlePlayer.prevY = posY; + turtlePlayer.prevZ = posZ; + turtlePlayer.prevPitch = turtlePlayer.pitch; + turtlePlayer.prevYaw = turtlePlayer.yaw; + + turtlePlayer.headYaw = turtlePlayer.yaw; + turtlePlayer.prevHeadYaw = turtlePlayer.headYaw; + } + + @Nonnull + private static ItemStack deployOnEntity( @Nonnull ItemStack stack, final ITurtleAccess turtle, TurtlePlayer turtlePlayer, Direction direction, Object[] extraArguments, String[] outErrorMessage ) + { + // See if there is an entity present + final World world = turtle.getWorld(); + final BlockPos position = turtle.getPosition(); + Vec3d turtlePos = turtlePlayer.getPos(); + Vec3d rayDir = turtlePlayer.getRotationVec( 1.0f ); + Pair hit = WorldUtil.rayTraceEntities( world, turtlePos, rayDir, 1.5 ); + if( hit == null ) + { + return stack; + } + + // Load up the turtle's inventory + ItemStack stackCopy = stack.copy(); + turtlePlayer.loadInventory( stackCopy ); + + // Start claiming entity drops + Entity hitEntity = hit.getKey(); + Vec3d hitPos = hit.getValue(); + DropConsumer.set( + hitEntity, + drop -> InventoryUtil.storeItems( drop, turtle.getItemHandler(), turtle.getSelectedSlot() ) + ); + + // Place on the entity + boolean placed = false; + ActionResult cancelResult = ForgeHooks.onInteractEntityAt( turtlePlayer, hitEntity, hitPos, Hand.MAIN_HAND ); + if( cancelResult == null ) + { + cancelResult = hitEntity.interactAt( turtlePlayer, hitPos, Hand.MAIN_HAND ); + } + + if( cancelResult.isAccepted() ) + { + placed = true; + } + else + { + // See EntityPlayer.interactOn + cancelResult = ForgeHooks.onInteractEntity( turtlePlayer, hitEntity, Hand.MAIN_HAND ); + if( cancelResult != null && cancelResult.isAccepted() ) + { + placed = true; + } + else if( cancelResult == null ) + { + if( hitEntity.interact( turtlePlayer, Hand.MAIN_HAND ) == ActionResult.CONSUME ) + { + placed = true; + } + else if( hitEntity instanceof LivingEntity ) + { + placed = stackCopy.useOnEntity( turtlePlayer, (LivingEntity) hitEntity, Hand.MAIN_HAND ).isAccepted(); + if( placed ) turtlePlayer.loadInventory( stackCopy ); + } + } + } + + // Stop claiming drops + List remainingDrops = DropConsumer.clear(); + for( ItemStack remaining : remainingDrops ) + { + WorldUtil.dropItemStack( remaining, world, position, turtle.getDirection().getOpposite() ); + } + + // Put everything we collected into the turtles inventory, then return + ItemStack remainder = turtlePlayer.unloadInventory( turtle ); + if( !placed && ItemStack.areEqual( stack, remainder ) ) + { + return stack; + } + else if( !remainder.isEmpty() ) + { + return remainder; + } + else + { + return ItemStack.EMPTY; + } + } + + private static boolean canDeployOnBlock( @Nonnull ItemPlacementContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position, Direction side, boolean allowReplaceable, String[] outErrorMessage ) + { + World world = turtle.getWorld(); + if( !World.method_24794( position ) || world.isAir( position ) || + (context.getStack().getItem() instanceof BlockItem && WorldUtil.isLiquidBlock( world, position )) ) + { + return false; + } + + BlockState state = world.getBlockState( position ); + + boolean replaceable = state.canReplace( context ); + if( !allowReplaceable && replaceable ) return false; + + if( ComputerCraft.turtlesObeyBlockProtection ) + { + // Check spawn protection + boolean editable = replaceable + ? TurtlePermissions.isBlockEditable( world, position, player ) + : TurtlePermissions.isBlockEditable( world, position.offset( side ), player ); + if( !editable ) + { + if( outErrorMessage != null ) outErrorMessage[0] = "Cannot place in protected area"; + return false; + } + } + + return true; + } + + @Nonnull + private static ItemStack deployOnBlock( @Nonnull ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, BlockPos position, Direction side, Object[] extraArguments, boolean allowReplace, String[] outErrorMessage ) + { + // Re-orient the fake player + Direction playerDir = side.getOpposite(); + BlockPos playerPosition = position.offset( side ); + orientPlayer( turtle, turtlePlayer, playerPosition, playerDir ); + + ItemStack stackCopy = stack.copy(); + turtlePlayer.loadInventory( stackCopy ); + + // Calculate where the turtle would hit the block + float hitX = 0.5f + side.getOffsetX() * 0.5f; + float hitY = 0.5f + side.getOffsetY() * 0.5f; + float hitZ = 0.5f + side.getOffsetZ() * 0.5f; + if( Math.abs( hitY - 0.5f ) < 0.01f ) + { + hitY = 0.45f; + } + + // Check if there's something suitable to place onto + BlockHitResult hit = new BlockHitResult( new Vec3d( hitX, hitY, hitZ ), side, position, false ); + ItemUsageContext context = new ItemUsageContext( turtlePlayer, Hand.MAIN_HAND, hit ); + if( !canDeployOnBlock( new ItemPlacementContext( context ), turtle, turtlePlayer, position, side, allowReplace, outErrorMessage ) ) + { + return stack; + } + + // Load up the turtle's inventory + Item item = stack.getItem(); + + // Do the deploying (put everything in the players inventory) + boolean placed = false; + BlockEntity existingTile = turtle.getWorld().getBlockEntity( position ); + + // See PlayerInteractionManager.processRightClickBlock + PlayerInteractEvent.RightClickBlock event = ForgeHooks.onRightClickBlock( turtlePlayer, Hand.MAIN_HAND, position, side ); + if( !event.isCanceled() ) + { + if( item.onItemUseFirst( stack, context ).isAccepted() ) + { + placed = true; + turtlePlayer.loadInventory( stackCopy ); + } + else if( event.getUseItem() != Event.Result.DENY && stackCopy.useOnBlock( context ).isAccepted() ) + { + placed = true; + turtlePlayer.loadInventory( stackCopy ); + } + } + + if( !placed && (item instanceof BucketItem || item instanceof BoatItem || item instanceof LilyPadItem || item instanceof GlassBottleItem) ) + { + ActionResult actionResult = ForgeHooks.onItemRightClick( turtlePlayer, Hand.MAIN_HAND ); + if( actionResult != null && actionResult.isAccepted() ) + { + placed = true; + } + else if( actionResult == null ) + { + TypedActionResult result = stackCopy.use( turtle.getWorld(), turtlePlayer, Hand.MAIN_HAND ); + if( result.getResult().isAccepted() && !ItemStack.areEqual( stack, result.getValue() ) ) + { + placed = true; + turtlePlayer.loadInventory( result.getValue() ); + } + } + } + + // Set text on signs + if( placed && item instanceof SignItem ) + { + if( extraArguments != null && extraArguments.length >= 1 && extraArguments[0] instanceof String ) + { + World world = turtle.getWorld(); + BlockEntity tile = world.getBlockEntity( position ); + if( tile == null || tile == existingTile ) + { + tile = world.getBlockEntity( position.offset( side ) ); + } + if( tile instanceof SignBlockEntity ) + { + SignBlockEntity signTile = (SignBlockEntity) tile; + String s = (String) extraArguments[0]; + String[] split = s.split( "\n" ); + int firstLine = split.length <= 2 ? 1 : 0; + for( int i = 0; i < 4; i++ ) + { + if( i >= firstLine && i < firstLine + split.length ) + { + if( split[i - firstLine].length() > 15 ) + { + signTile.setTextOnRow( i, new LiteralText( split[i - firstLine].substring( 0, 15 ) ) ); + } + else + { + signTile.setTextOnRow( i, new LiteralText( split[i - firstLine] ) ); + } + } + else + { + signTile.setTextOnRow( i, new LiteralText( "" ) ); + } + } + signTile.markDirty(); + world.updateListeners( tile.getPos(), tile.getCachedState(), tile.getCachedState(), 3 ); + } + } + } + + // Put everything we collected into the turtles inventory, then return + ItemStack remainder = turtlePlayer.unloadInventory( turtle ); + if( !placed && ItemStack.areEqual( stack, remainder ) ) + { + return stack; + } + else if( !remainder.isEmpty() ) + { + return remainder; + } + else + { + return ItemStack.EMPTY; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java new file mode 100644 index 000000000..0a11b3545 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java @@ -0,0 +1,220 @@ +/* + * 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.turtle.core; + +import com.mojang.authlib.GameProfile; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.util.FakeNetHandler; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.block.entity.SignBlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityDimensions; +import net.minecraft.entity.EntityPose; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.passive.HorseBaseEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Hand; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.common.util.FakePlayer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.OptionalInt; +import java.util.UUID; + +public final class TurtlePlayer extends FakePlayer +{ + private static final GameProfile DEFAULT_PROFILE = new GameProfile( + UUID.fromString( "0d0c4ca0-4ff1-11e4-916c-0800200c9a66" ), + "[ComputerCraft]" + ); + + private TurtlePlayer( ITurtleAccess turtle ) + { + super( (ServerWorld) turtle.getWorld(), getProfile( turtle.getOwningPlayer() ) ); + this.networkHandler = new FakeNetHandler( this ); + setState( turtle ); + } + + private static GameProfile getProfile( @Nullable GameProfile profile ) + { + return profile != null && profile.isComplete() ? profile : DEFAULT_PROFILE; + } + + private void setState( ITurtleAccess turtle ) + { + if( currentScreenHandler != null ) + { + ComputerCraft.log.warn( "Turtle has open container ({})", currentScreenHandler ); + currentScreenHandler.close( this ); + currentScreenHandler = null; + } + + BlockPos position = turtle.getPosition(); + setPos( position.getX() + 0.5, position.getY() + 0.5, position.getZ() + 0.5 ); + + yaw = turtle.getDirection().asRotation(); + pitch = 0.0f; + + inventory.clear(); + } + + public static TurtlePlayer get( ITurtleAccess access ) + { + if( !(access instanceof TurtleBrain) ) return new TurtlePlayer( access ); + + TurtleBrain brain = (TurtleBrain) access; + TurtlePlayer player = brain.m_cachedPlayer; + if( player == null || player.getGameProfile() != getProfile( access.getOwningPlayer() ) + || player.getEntityWorld() != access.getWorld() ) + { + player = brain.m_cachedPlayer = new TurtlePlayer( brain ); + } + else + { + player.setState( access ); + } + + return player; + } + + public void loadInventory( @Nonnull ItemStack currentStack ) + { + // Load up the fake inventory + inventory.selectedSlot = 0; + inventory.setStack( 0, currentStack ); + } + + public ItemStack unloadInventory( ITurtleAccess turtle ) + { + // Get the item we placed with + ItemStack results = inventory.getStack( 0 ); + inventory.setStack( 0, ItemStack.EMPTY ); + + // Store (or drop) anything else we found + BlockPos dropPosition = turtle.getPosition(); + Direction dropDirection = turtle.getDirection().getOpposite(); + for( int i = 0; i < inventory.size(); i++ ) + { + ItemStack stack = inventory.getStack( i ); + if( !stack.isEmpty() ) + { + ItemStack remainder = InventoryUtil.storeItems( stack, turtle.getItemHandler(), turtle.getSelectedSlot() ); + if( !remainder.isEmpty() ) + { + WorldUtil.dropItemStack( remainder, turtle.getWorld(), dropPosition, dropDirection ); + } + inventory.setStack( i, ItemStack.EMPTY ); + } + } + inventory.markDirty(); + return results; + } + + @Nonnull + @Override + public EntityType getType() + { + return Registry.ModEntities.TURTLE_PLAYER.get(); + } + + @Override + public Vec3d getPos() + { + return new Vec3d( getX(), getY(), getZ() ); + } + + @Override + public float getEyeHeight( @Nonnull EntityPose pose ) + { + return 0; + } + + @Override + public float getActiveEyeHeight( @Nonnull EntityPose pose, @Nonnull EntityDimensions size ) + { + return 0; + } + + //region Code which depends on the connection + @Nonnull + @Override + public OptionalInt openHandledScreen( @Nullable NamedScreenHandlerFactory prover ) + { + return OptionalInt.empty(); + } + + @Override + public void enterCombat() + { + } + + @Override + public void endCombat() + { + } + + @Override + public boolean startRiding( @Nonnull Entity entityIn, boolean force ) + { + return false; + } + + @Override + public void stopRiding() + { + } + + @Override + public void openEditSignScreen( @Nonnull SignBlockEntity signTile ) + { + } + + @Override + public void openHorseInventory( @Nonnull HorseBaseEntity horse, @Nonnull Inventory inventory ) + { + } + + @Override + public void openEditBookScreen( @Nonnull ItemStack stack, @Nonnull Hand hand ) + { + } + + @Override + public void closeHandledScreen() + { + } + + @Override + public void updateCursorStack() + { + } + + @Override + protected void onStatusEffectApplied( @Nonnull StatusEffectInstance id ) + { + } + + @Override + protected void onStatusEffectUpgraded( @Nonnull StatusEffectInstance id, boolean apply ) + { + } + + @Override + protected void onStatusEffectRemoved( @Nonnull StatusEffectInstance effect ) + { + } + //endregion +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleRefuelCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleRefuelCommand.java new file mode 100644 index 000000000..6fef3384f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleRefuelCommand.java @@ -0,0 +1,47 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleAnimation; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.api.turtle.event.TurtleRefuelEvent; +import net.minecraft.item.ItemStack; +import net.minecraftforge.common.MinecraftForge; + +import javax.annotation.Nonnull; + +public class TurtleRefuelCommand implements ITurtleCommand +{ + private final int limit; + + public TurtleRefuelCommand( int limit ) + { + this.limit = limit; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + int slot = turtle.getSelectedSlot(); + ItemStack stack = turtle.getInventory().getStack( slot ); + if( stack.isEmpty() ) return TurtleCommandResult.failure( "No items to combust" ); + + TurtleRefuelEvent event = new TurtleRefuelEvent( turtle, stack ); + if( MinecraftForge.EVENT_BUS.post( event ) ) return TurtleCommandResult.failure( event.getFailureMessage() ); + if( event.getHandler() == null ) return TurtleCommandResult.failure( "Items not combustible" ); + + if( limit != 0 ) + { + turtle.addFuel( event.getHandler().refuel( turtle, stack, slot, limit ) ); + turtle.playAnimation( TurtleAnimation.WAIT ); + } + + return TurtleCommandResult.success(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java new file mode 100644 index 000000000..a3b064734 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java @@ -0,0 +1,154 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleAnimation; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.api.turtle.event.TurtleInventoryEvent; +import dan200.computercraft.shared.util.InventoryUtil; +import net.minecraft.entity.ItemEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.items.IItemHandler; + +import javax.annotation.Nonnull; +import java.util.List; + +public class TurtleSuckCommand implements ITurtleCommand +{ + private final InteractDirection m_direction; + private final int m_quantity; + + public TurtleSuckCommand( InteractDirection direction, int quantity ) + { + m_direction = direction; + m_quantity = quantity; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Sucking nothing is easy + if( m_quantity == 0 ) + { + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + + // Get world direction from direction + Direction direction = m_direction.toWorldDir( turtle ); + + // Get inventory for thing in front + World world = turtle.getWorld(); + BlockPos turtlePosition = turtle.getPosition(); + BlockPos blockPosition = turtlePosition.offset( direction ); + Direction side = direction.getOpposite(); + + IItemHandler inventory = InventoryUtil.getInventory( world, blockPosition, side ); + + // Fire the event, exiting if it is cancelled. + TurtlePlayer player = TurtlePlaceCommand.createPlayer( turtle, turtlePosition, direction ); + TurtleInventoryEvent.Suck event = new TurtleInventoryEvent.Suck( turtle, player, world, blockPosition, inventory ); + if( MinecraftForge.EVENT_BUS.post( event ) ) + { + return TurtleCommandResult.failure( event.getFailureMessage() ); + } + + if( inventory != null ) + { + // Take from inventory of thing in front + ItemStack stack = InventoryUtil.takeItems( m_quantity, inventory ); + if( stack.isEmpty() ) return TurtleCommandResult.failure( "No items to take" ); + + // Try to place into the turtle + ItemStack remainder = InventoryUtil.storeItems( stack, turtle.getItemHandler(), turtle.getSelectedSlot() ); + if( !remainder.isEmpty() ) + { + // Put the remainder back in the inventory + InventoryUtil.storeItems( remainder, inventory ); + } + + // Return true if we consumed anything + if( remainder != stack ) + { + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + else + { + return TurtleCommandResult.failure( "No space for items" ); + } + } + else + { + // Suck up loose items off the ground + Box aabb = new Box( + blockPosition.getX(), blockPosition.getY(), blockPosition.getZ(), + blockPosition.getX() + 1.0, blockPosition.getY() + 1.0, blockPosition.getZ() + 1.0 + ); + List list = world.getEntitiesByClass( ItemEntity.class, aabb, EntityPredicates.VALID_ENTITY ); + if( list.isEmpty() ) return TurtleCommandResult.failure( "No items to take" ); + + for( ItemEntity entity : list ) + { + // Suck up the item + ItemStack stack = entity.getStack().copy(); + + ItemStack storeStack; + ItemStack leaveStack; + if( stack.getCount() > m_quantity ) + { + storeStack = stack.split( m_quantity ); + leaveStack = stack; + } + else + { + storeStack = stack; + leaveStack = ItemStack.EMPTY; + } + + ItemStack remainder = InventoryUtil.storeItems( storeStack, turtle.getItemHandler(), turtle.getSelectedSlot() ); + + if( remainder != storeStack ) + { + if( remainder.isEmpty() && leaveStack.isEmpty() ) + { + entity.remove(); + } + else if( remainder.isEmpty() ) + { + entity.setStack( leaveStack ); + } + else if( leaveStack.isEmpty() ) + { + entity.setStack( remainder ); + } + else + { + leaveStack.increment( remainder.getCount() ); + entity.setStack( leaveStack ); + } + + // Play fx + world.syncGlobalEvent( 1000, turtlePosition, 0 ); // BLOCK_DISPENSER_DISPENSE + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + } + + + return TurtleCommandResult.failure( "No space for items" ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleToolCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleToolCommand.java new file mode 100644 index 000000000..9575090e8 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleToolCommand.java @@ -0,0 +1,74 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.*; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Locale; + +public class TurtleToolCommand implements ITurtleCommand +{ + private final TurtleVerb verb; + private final InteractDirection direction; + private final TurtleSide side; + + public TurtleToolCommand( TurtleVerb verb, InteractDirection direction, TurtleSide side ) + { + this.verb = verb; + this.direction = direction; + this.side = side; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + TurtleCommandResult firstFailure = null; + for( TurtleSide side : TurtleSide.values() ) + { + if( this.side != null && this.side != side ) continue; + + ITurtleUpgrade upgrade = turtle.getUpgrade( side ); + if( upgrade == null || !upgrade.getType().isTool() ) continue; + + TurtleCommandResult result = upgrade.useTool( turtle, side, verb, direction.toWorldDir( turtle ) ); + if( result.isSuccess() ) + { + switch( side ) + { + case LEFT: + turtle.playAnimation( TurtleAnimation.SWING_LEFT_TOOL ); + break; + case RIGHT: + turtle.playAnimation( TurtleAnimation.SWING_RIGHT_TOOL ); + break; + default: + turtle.playAnimation( TurtleAnimation.WAIT ); + break; + } + return result; + } + else if( firstFailure == null ) + { + firstFailure = result; + } + } + return firstFailure != null ? firstFailure + : TurtleCommandResult.failure( "No tool to " + verb.name().toLowerCase( Locale.ROOT ) + " with" ); + } + + public static TurtleToolCommand attack( InteractDirection direction, @Nullable TurtleSide side ) + { + return new TurtleToolCommand( TurtleVerb.ATTACK, direction, side ); + } + + public static TurtleToolCommand dig( InteractDirection direction, @Nullable TurtleSide side ) + { + return new TurtleToolCommand( TurtleVerb.DIG, direction, side ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTransferToCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTransferToCommand.java new file mode 100644 index 000000000..a1b7a9e7a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTransferToCommand.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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleAnimation; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.shared.util.InventoryUtil; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; + +public class TurtleTransferToCommand implements ITurtleCommand +{ + private final int m_slot; + private final int m_quantity; + + public TurtleTransferToCommand( int slot, int limit ) + { + m_slot = slot; + m_quantity = limit; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Take stack + ItemStack stack = InventoryUtil.takeItems( m_quantity, turtle.getItemHandler(), turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() ); + if( stack.isEmpty() ) + { + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + + // Store stack + ItemStack remainder = InventoryUtil.storeItems( stack, turtle.getItemHandler(), m_slot, 1, m_slot ); + if( !remainder.isEmpty() ) + { + // Put the remainder back + InventoryUtil.storeItems( remainder, turtle.getItemHandler(), turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() ); + } + + // Return true if we moved anything + if( remainder != stack ) + { + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + else + { + return TurtleCommandResult.failure( "No space for items" ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTurnCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTurnCommand.java new file mode 100644 index 000000000..2365451ef --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTurnCommand.java @@ -0,0 +1,57 @@ +/* + * 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.turtle.core; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleCommand; +import dan200.computercraft.api.turtle.TurtleAnimation; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.api.turtle.event.TurtleAction; +import dan200.computercraft.api.turtle.event.TurtleActionEvent; +import net.minecraftforge.common.MinecraftForge; + +import javax.annotation.Nonnull; + +public class TurtleTurnCommand implements ITurtleCommand +{ + private final TurnDirection m_direction; + + public TurtleTurnCommand( TurnDirection direction ) + { + m_direction = direction; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + TurtleActionEvent event = new TurtleActionEvent( turtle, TurtleAction.TURN ); + if( MinecraftForge.EVENT_BUS.post( event ) ) + { + return TurtleCommandResult.failure( event.getFailureMessage() ); + } + + switch( m_direction ) + { + case LEFT: + { + turtle.setDirection( turtle.getDirection().rotateYCounterclockwise() ); + turtle.playAnimation( TurtleAnimation.TURN_LEFT ); + return TurtleCommandResult.success(); + } + case RIGHT: + { + turtle.setDirection( turtle.getDirection().rotateYClockwise() ); + turtle.playAnimation( TurtleAnimation.TURN_RIGHT ); + return TurtleCommandResult.success(); + } + default: + { + return TurtleCommandResult.failure( "Unknown direction" ); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/inventory/ContainerTurtle.java b/src/main/java/dan200/computercraft/shared/turtle/inventory/ContainerTurtle.java new file mode 100644 index 000000000..21e8cb7c1 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/inventory/ContainerTurtle.java @@ -0,0 +1,139 @@ +/* + * 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.turtle.inventory; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.IComputer; +import dan200.computercraft.shared.computer.inventory.ContainerComputerBase; +import dan200.computercraft.shared.network.container.ComputerContainerData; +import dan200.computercraft.shared.turtle.blocks.TileTurtle; +import dan200.computercraft.shared.turtle.core.TurtleBrain; +import dan200.computercraft.shared.util.SingleIntArray; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SimpleInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.ArrayPropertyDelegate; +import net.minecraft.screen.PropertyDelegate; +import net.minecraft.screen.slot.Slot; +import javax.annotation.Nonnull; +import java.util.function.Predicate; + +public class ContainerTurtle extends ContainerComputerBase +{ + public static final int PLAYER_START_Y = 134; + public static final int TURTLE_START_X = 175; + + private final PropertyDelegate properties; + + private ContainerTurtle( + int id, Predicate canUse, IComputer computer, ComputerFamily family, + PlayerInventory playerInventory, Inventory inventory, PropertyDelegate properties + ) + { + super( Registry.ModContainers.TURTLE.get(), id, canUse, computer, family ); + this.properties = properties; + + addProperties( properties ); + + // Turtle inventory + for( int y = 0; y < 4; y++ ) + { + for( int x = 0; x < 4; x++ ) + { + addSlot( new Slot( inventory, x + y * 4, TURTLE_START_X + 1 + x * 18, PLAYER_START_Y + 1 + y * 18 ) ); + } + } + + // Player inventory + for( int y = 0; y < 3; y++ ) + { + for( int x = 0; x < 9; x++ ) + { + addSlot( new Slot( playerInventory, x + y * 9 + 9, 8 + x * 18, PLAYER_START_Y + 1 + y * 18 ) ); + } + } + + // Player hotbar + for( int x = 0; x < 9; x++ ) + { + addSlot( new Slot( playerInventory, x, 8 + x * 18, PLAYER_START_Y + 3 * 18 + 5 ) ); + } + } + + public ContainerTurtle( int id, PlayerInventory player, TurtleBrain turtle ) + { + this( + id, p -> turtle.getOwner().canPlayerUse( p ), turtle.getOwner().createServerComputer(), turtle.getFamily(), + player, turtle.getInventory(), (SingleIntArray) turtle::getSelectedSlot + ); + } + + public ContainerTurtle( int id, PlayerInventory player, ComputerContainerData data ) + { + this( + id, x -> true, getComputer( player, data ), data.getFamily(), + player, new SimpleInventory( TileTurtle.INVENTORY_SIZE ), new ArrayPropertyDelegate( 1 ) + ); + } + + public int getSelectedSlot() + { + return properties.get( 0 ); + } + + @Nonnull + private ItemStack tryItemMerge( PlayerEntity player, int slotNum, int firstSlot, int lastSlot, boolean reverse ) + { + Slot slot = slots.get( slotNum ); + ItemStack originalStack = ItemStack.EMPTY; + if( slot != null && slot.hasStack() ) + { + ItemStack clickedStack = slot.getStack(); + originalStack = clickedStack.copy(); + if( !insertItem( clickedStack, firstSlot, lastSlot, reverse ) ) + { + return ItemStack.EMPTY; + } + + if( clickedStack.isEmpty() ) + { + slot.setStack( ItemStack.EMPTY ); + } + else + { + slot.markDirty(); + } + + if( clickedStack.getCount() != originalStack.getCount() ) + { + slot.onTakeItem( player, clickedStack ); + } + else + { + return ItemStack.EMPTY; + } + } + return originalStack; + } + + @Nonnull + @Override + public ItemStack transferSlot( @Nonnull PlayerEntity player, int slotNum ) + { + if( slotNum >= 0 && slotNum < 16 ) + { + return tryItemMerge( player, slotNum, 16, 52, true ); + } + else if( slotNum >= 16 ) + { + return tryItemMerge( player, slotNum, 0, 16, false ); + } + return ItemStack.EMPTY; + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/items/ITurtleItem.java b/src/main/java/dan200/computercraft/shared/turtle/items/ITurtleItem.java new file mode 100644 index 000000000..239fd4c5e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/items/ITurtleItem.java @@ -0,0 +1,26 @@ +/* + * 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.turtle.items; + +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.common.IColouredItem; +import dan200.computercraft.shared.computer.items.IComputerItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public interface ITurtleItem extends IComputerItem, IColouredItem +{ + @Nullable + ITurtleUpgrade getUpgrade( @Nonnull ItemStack stack, @Nonnull TurtleSide side ); + + int getFuelLevel( @Nonnull ItemStack stack ); + + @Nullable + Identifier getOverlay( @Nonnull ItemStack stack ); +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/items/ItemTurtle.java b/src/main/java/dan200/computercraft/shared/turtle/items/ItemTurtle.java new file mode 100644 index 000000000..16b44ef99 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/items/ItemTurtle.java @@ -0,0 +1,163 @@ +/* + * 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.turtle.items; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.TurtleUpgrades; +import dan200.computercraft.shared.common.IColouredItem; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.items.ItemComputerBase; +import dan200.computercraft.shared.turtle.blocks.BlockTurtle; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Identifier; +import net.minecraft.util.collection.DefaultedList; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static dan200.computercraft.shared.turtle.core.TurtleBrain.*; + +public class ItemTurtle extends ItemComputerBase implements ITurtleItem +{ + public ItemTurtle( BlockTurtle block, Settings settings ) + { + super( block, settings ); + } + + public ItemStack create( int id, String label, int colour, ITurtleUpgrade leftUpgrade, ITurtleUpgrade rightUpgrade, int fuelLevel, Identifier overlay ) + { + // Build the stack + ItemStack stack = new ItemStack( this ); + if( label != null ) stack.setCustomName( new LiteralText( label ) ); + if( id >= 0 ) stack.getOrCreateTag().putInt( NBT_ID, id ); + IColouredItem.setColourBasic( stack, colour ); + if( fuelLevel > 0 ) stack.getOrCreateTag().putInt( NBT_FUEL, fuelLevel ); + if( overlay != null ) stack.getOrCreateTag().putString( NBT_OVERLAY, overlay.toString() ); + + if( leftUpgrade != null ) + { + stack.getOrCreateTag().putString( NBT_LEFT_UPGRADE, leftUpgrade.getUpgradeID().toString() ); + } + + if( rightUpgrade != null ) + { + stack.getOrCreateTag().putString( NBT_RIGHT_UPGRADE, rightUpgrade.getUpgradeID().toString() ); + } + + return stack; + } + + @Override + public void appendStacks( @Nonnull ItemGroup group, @Nonnull DefaultedList list ) + { + if( !isIn( group ) ) return; + + ComputerFamily family = getFamily(); + + list.add( create( -1, null, -1, null, null, 0, null ) ); + TurtleUpgrades.getVanillaUpgrades() + .filter( x -> TurtleUpgrades.suitableForFamily( family, x ) ) + .map( x -> create( -1, null, -1, null, x, 0, null ) ) + .forEach( list::add ); + } + + @Nonnull + @Override + public Text getName( @Nonnull ItemStack stack ) + { + String baseString = getTranslationKey( stack ); + ITurtleUpgrade left = getUpgrade( stack, TurtleSide.LEFT ); + ITurtleUpgrade right = getUpgrade( stack, TurtleSide.RIGHT ); + if( left != null && right != null ) + { + return new TranslatableText( baseString + ".upgraded_twice", + new TranslatableText( right.getUnlocalisedAdjective() ), + new TranslatableText( left.getUnlocalisedAdjective() ) + ); + } + else if( left != null ) + { + return new TranslatableText( baseString + ".upgraded", + new TranslatableText( left.getUnlocalisedAdjective() ) + ); + } + else if( right != null ) + { + return new TranslatableText( baseString + ".upgraded", + new TranslatableText( right.getUnlocalisedAdjective() ) + ); + } + else + { + return new TranslatableText( baseString ); + } + } + + @Nullable + @Override + public String getCreatorModId( ItemStack stack ) + { + // Determine our "creator mod" from the upgrades. We attempt to find the first non-vanilla/non-CC + // upgrade (starting from the left). + + ITurtleUpgrade left = getUpgrade( stack, TurtleSide.LEFT ); + if( left != null ) + { + String mod = TurtleUpgrades.getOwner( left ); + if( mod != null && !mod.equals( ComputerCraft.MOD_ID ) ) return mod; + } + + ITurtleUpgrade right = getUpgrade( stack, TurtleSide.RIGHT ); + if( right != null ) + { + String mod = TurtleUpgrades.getOwner( right ); + if( mod != null && !mod.equals( ComputerCraft.MOD_ID ) ) return mod; + } + + return super.getCreatorModId( stack ); + } + + @Override + public ItemStack withFamily( @Nonnull ItemStack stack, @Nonnull ComputerFamily family ) + { + return TurtleItemFactory.create( + getComputerID( stack ), getLabel( stack ), + getColour( stack ), family, + getUpgrade( stack, TurtleSide.LEFT ), getUpgrade( stack, TurtleSide.RIGHT ), + getFuelLevel( stack ), getOverlay( stack ) + ); + } + + @Override + public ITurtleUpgrade getUpgrade( @Nonnull ItemStack stack, @Nonnull TurtleSide side ) + { + CompoundTag tag = stack.getTag(); + if( tag == null ) return null; + + String key = side == TurtleSide.LEFT ? NBT_LEFT_UPGRADE : NBT_RIGHT_UPGRADE; + return tag.contains( key ) ? TurtleUpgrades.get( tag.getString( key ) ) : null; + } + + @Override + public Identifier getOverlay( @Nonnull ItemStack stack ) + { + CompoundTag tag = stack.getTag(); + return tag != null && tag.contains( NBT_OVERLAY ) ? new Identifier( tag.getString( NBT_OVERLAY ) ) : null; + } + + @Override + public int getFuelLevel( @Nonnull ItemStack stack ) + { + CompoundTag tag = stack.getTag(); + return tag != null && tag.contains( NBT_FUEL ) ? tag.getInt( NBT_FUEL ) : 0; + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItemFactory.java b/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItemFactory.java new file mode 100644 index 000000000..cc77819c4 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItemFactory.java @@ -0,0 +1,47 @@ +/* + * 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.turtle.items; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.turtle.blocks.ITurtleTile; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import javax.annotation.Nonnull; + +public final class TurtleItemFactory +{ + private TurtleItemFactory() {} + + @Nonnull + public static ItemStack create( ITurtleTile turtle ) + { + ITurtleAccess access = turtle.getAccess(); + + return create( + turtle.getComputerID(), turtle.getLabel(), turtle.getColour(), turtle.getFamily(), + access.getUpgrade( TurtleSide.LEFT ), access.getUpgrade( TurtleSide.RIGHT ), + access.getFuelLevel(), turtle.getOverlay() + ); + } + + @Nonnull + public static ItemStack create( int id, String label, int colour, ComputerFamily family, ITurtleUpgrade leftUpgrade, ITurtleUpgrade rightUpgrade, int fuelLevel, Identifier overlay ) + { + switch( family ) + { + case NORMAL: + return Registry.ModItems.TURTLE_NORMAL.get().create( id, label, colour, leftUpgrade, rightUpgrade, fuelLevel, overlay ); + case ADVANCED: + return Registry.ModItems.TURTLE_ADVANCED.get().create( id, label, colour, leftUpgrade, rightUpgrade, fuelLevel, overlay ); + default: + return ItemStack.EMPTY; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleRecipe.java b/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleRecipe.java new file mode 100644 index 000000000..1570afa93 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleRecipe.java @@ -0,0 +1,51 @@ +/* + * 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.turtle.recipes; + +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.items.IComputerItem; +import dan200.computercraft.shared.computer.recipe.ComputerFamilyRecipe; +import dan200.computercraft.shared.turtle.items.TurtleItemFactory; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.util.Identifier; +import net.minecraft.util.collection.DefaultedList; +import javax.annotation.Nonnull; + +public final class TurtleRecipe extends ComputerFamilyRecipe +{ + private TurtleRecipe( Identifier identifier, String group, int width, int height, DefaultedList ingredients, ItemStack result, ComputerFamily family ) + { + super( identifier, group, width, height, ingredients, result, family ); + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + @Nonnull + @Override + protected ItemStack convert( @Nonnull IComputerItem item, @Nonnull ItemStack stack ) + { + int computerID = item.getComputerID( stack ); + String label = item.getLabel( stack ); + + return TurtleItemFactory.create( computerID, label, -1, getFamily(), null, null, 0, null ); + } + + public static final RecipeSerializer SERIALIZER = new dan200.computercraft.shared.computer.recipe.ComputerFamilyRecipe.Serializer() + { + @Override + protected TurtleRecipe create( Identifier identifier, String group, int width, int height, DefaultedList ingredients, ItemStack result, ComputerFamily family ) + { + return new TurtleRecipe( identifier, group, width, height, ingredients, result, family ); + } + }; +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java b/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java new file mode 100644 index 000000000..bffb308a4 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java @@ -0,0 +1,189 @@ +/* + * 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.turtle.recipes; + +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.TurtleUpgrades; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.turtle.items.ITurtleItem; +import dan200.computercraft.shared.turtle.items.TurtleItemFactory; +import net.minecraft.inventory.CraftingInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.SpecialCraftingRecipe; +import net.minecraft.recipe.SpecialRecipeSerializer; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public final class TurtleUpgradeRecipe extends SpecialCraftingRecipe +{ + private TurtleUpgradeRecipe( Identifier id ) + { + super( id ); + } + + @Override + public boolean fits( int x, int y ) + { + return x >= 3 && y >= 1; + } + + @Nonnull + @Override + public ItemStack getOutput() + { + return TurtleItemFactory.create( -1, null, -1, ComputerFamily.NORMAL, null, null, 0, null ); + } + + @Override + public boolean matches( @Nonnull CraftingInventory inventory, @Nonnull World world ) + { + return !getCraftingResult( inventory ).isEmpty(); + } + + @Nonnull + @Override + public ItemStack getCraftingResult( @Nonnull CraftingInventory inventory ) + { + // Scan the grid for a row containing a turtle and 1 or 2 items + ItemStack leftItem = ItemStack.EMPTY; + ItemStack turtle = ItemStack.EMPTY; + ItemStack rightItem = ItemStack.EMPTY; + + for( int y = 0; y < inventory.getHeight(); y++ ) + { + if( turtle.isEmpty() ) + { + // Search this row for potential turtles + boolean finishedRow = false; + for( int x = 0; x < inventory.getWidth(); x++ ) + { + ItemStack item = inventory.getStack( x + y * inventory.getWidth() ); + if( !item.isEmpty() ) + { + if( finishedRow ) + { + return ItemStack.EMPTY; + } + + if( item.getItem() instanceof ITurtleItem ) + { + // Item is a turtle + if( turtle.isEmpty() ) + { + turtle = item; + } + else + { + return ItemStack.EMPTY; + } + } + else + { + // Item is not a turtle + if( turtle.isEmpty() && leftItem.isEmpty() ) + { + leftItem = item; + } + else if( !turtle.isEmpty() && rightItem.isEmpty() ) + { + rightItem = item; + } + else + { + return ItemStack.EMPTY; + } + } + } + else + { + // Item is empty + if( !leftItem.isEmpty() || !turtle.isEmpty() ) + { + finishedRow = true; + } + } + } + + // If we found anything, check we found a turtle too + if( turtle.isEmpty() && (!leftItem.isEmpty() || !rightItem.isEmpty()) ) + { + return ItemStack.EMPTY; + } + } + else + { + // Turtle is already found, just check this row is empty + for( int x = 0; x < inventory.getWidth(); x++ ) + { + ItemStack item = inventory.getStack( x + y * inventory.getWidth() ); + if( !item.isEmpty() ) + { + return ItemStack.EMPTY; + } + } + } + } + + // See if we found a turtle + one or more items + if( turtle.isEmpty() || leftItem.isEmpty() && rightItem.isEmpty() ) + { + return ItemStack.EMPTY; + } + + // At this point we have a turtle + 1 or 2 items + // Get the turtle we already have + ITurtleItem itemTurtle = (ITurtleItem) turtle.getItem(); + ComputerFamily family = itemTurtle.getFamily(); + ITurtleUpgrade[] upgrades = new ITurtleUpgrade[] { + itemTurtle.getUpgrade( turtle, TurtleSide.LEFT ), + itemTurtle.getUpgrade( turtle, TurtleSide.RIGHT ), + }; + + // Get the upgrades for the new items + ItemStack[] items = new ItemStack[] { rightItem, leftItem }; + for( int i = 0; i < 2; i++ ) + { + if( !items[i].isEmpty() ) + { + ITurtleUpgrade itemUpgrade = TurtleUpgrades.get( items[i] ); + if( itemUpgrade == null ) + { + return ItemStack.EMPTY; + } + if( upgrades[i] != null ) + { + return ItemStack.EMPTY; + } + if( !TurtleUpgrades.suitableForFamily( family, itemUpgrade ) ) + { + return ItemStack.EMPTY; + } + upgrades[i] = itemUpgrade; + } + } + + // Construct the new stack + int computerID = itemTurtle.getComputerID( turtle ); + String label = itemTurtle.getLabel( turtle ); + int fuelLevel = itemTurtle.getFuelLevel( turtle ); + int colour = itemTurtle.getColour( turtle ); + Identifier overlay = itemTurtle.getOverlay( turtle ); + return TurtleItemFactory.create( computerID, label, colour, family, upgrades[0], upgrades[1], fuelLevel, overlay ); + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + public static final RecipeSerializer SERIALIZER = new SpecialRecipeSerializer<>( TurtleUpgradeRecipe::new ); +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/CraftingTablePeripheral.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/CraftingTablePeripheral.java new file mode 100644 index 000000000..ebec3f562 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/CraftingTablePeripheral.java @@ -0,0 +1,61 @@ +/* + * 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.turtle.upgrades; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.shared.turtle.core.TurtleCraftCommand; + +import javax.annotation.Nonnull; +import java.util.Optional; + +/** + * The workbench peripheral allows you to craft items within the turtle's inventory. + * + * @cc.module workbench + * @hidden + * @cc.see turtle.craft This uses the {@link CraftingTablePeripheral} peripheral to craft items. + */ +public class CraftingTablePeripheral implements IPeripheral +{ + private final ITurtleAccess turtle; + + public CraftingTablePeripheral( ITurtleAccess turtle ) + { + this.turtle = turtle; + } + + @Nonnull + @Override + public String getType() + { + return "workbench"; + } + + @LuaFunction + public final MethodResult craft( Optional count ) throws LuaException + { + int limit = count.orElse( 64 ); + if( limit < 0 || limit > 64 ) throw new LuaException( "Crafting count " + limit + " out of range" ); + return turtle.executeCommand( new TurtleCraftCommand( limit ) ); + } + + @Override + public boolean equals( IPeripheral other ) + { + return other instanceof CraftingTablePeripheral; + } + + @Nonnull + @Override + public Object getTarget() + { + return turtle; + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleAxe.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleAxe.java new file mode 100644 index 000000000..44a680c4a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleAxe.java @@ -0,0 +1,34 @@ +/* + * 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.turtle.upgrades; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +public class TurtleAxe extends TurtleTool +{ + public TurtleAxe( Identifier id, String adjective, Item item ) + { + super( id, adjective, item ); + } + + public TurtleAxe( Identifier id, Item item ) + { + super( id, item ); + } + + public TurtleAxe( Identifier id, ItemStack craftItem, ItemStack toolItem ) + { + super( id, craftItem, toolItem ); + } + + @Override + protected float getDamageMultiplier() + { + return 6.0f; + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleCraftingTable.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleCraftingTable.java new file mode 100644 index 000000000..bbaef878a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleCraftingTable.java @@ -0,0 +1,61 @@ +/* + * 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.turtle.upgrades; + +import dan200.computercraft.api.client.TransformedModel; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.turtle.AbstractTurtleUpgrade; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.turtle.TurtleUpgradeType; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.block.Blocks; +import net.minecraft.client.util.ModelIdentifier; +import net.minecraft.util.Identifier; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; + +public class TurtleCraftingTable extends AbstractTurtleUpgrade +{ + @Environment(EnvType.CLIENT) + private ModelIdentifier m_leftModel; + + @Environment(EnvType.CLIENT) + private ModelIdentifier m_rightModel; + + public TurtleCraftingTable( Identifier id ) + { + super( id, TurtleUpgradeType.PERIPHERAL, Blocks.CRAFTING_TABLE ); + } + + @Override + public IPeripheral createPeripheral( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + return new CraftingTablePeripheral( turtle ); + } + + @Environment(EnvType.CLIENT) + private void loadModelLocations() + { + if( m_leftModel == null ) + { + m_leftModel = new ModelIdentifier( "computercraft:turtle_crafting_table_left", "inventory" ); + m_rightModel = new ModelIdentifier( "computercraft:turtle_crafting_table_right", "inventory" ); + } + } + + @Nonnull + @Override + @Environment(EnvType.CLIENT) + public TransformedModel getModel( ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + loadModelLocations(); + return TransformedModel.of( side == TurtleSide.LEFT ? m_leftModel : m_rightModel ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleHoe.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleHoe.java new file mode 100644 index 000000000..cb4622333 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleHoe.java @@ -0,0 +1,71 @@ +/* + * 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.turtle.upgrades; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.turtle.TurtleVerb; +import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand; +import dan200.computercraft.shared.turtle.core.TurtlePlayer; +import net.minecraft.block.BlockState; +import net.minecraft.block.Material; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public class TurtleHoe extends TurtleTool +{ + public TurtleHoe( Identifier id, String adjective, Item item ) + { + super( id, adjective, item ); + } + + public TurtleHoe( Identifier id, Item item ) + { + super( id, item ); + } + + public TurtleHoe( Identifier id, ItemStack craftItem, ItemStack toolItem ) + { + super( id, craftItem, toolItem ); + } + + @Override + protected boolean canBreakBlock( BlockState state, World world, BlockPos pos, TurtlePlayer player ) + { + if( !super.canBreakBlock( state, world, pos, player ) ) return false; + + Material material = state.getMaterial(); + return material == Material.PLANT || + material == Material.CACTUS || + material == Material.GOURD || + material == Material.LEAVES || + material == Material.UNDERWATER_PLANT || + material == Material.REPLACEABLE_PLANT; + } + + @Nonnull + @Override + public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull Direction direction ) + { + if( verb == TurtleVerb.DIG ) + { + ItemStack hoe = item.copy(); + ItemStack remainder = TurtlePlaceCommand.deploy( hoe, turtle, direction, null, null ); + if( remainder != hoe ) + { + return TurtleCommandResult.success(); + } + } + return super.useTool( turtle, side, verb, direction ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleInventoryCrafting.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleInventoryCrafting.java new file mode 100644 index 000000000..73da35331 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleInventoryCrafting.java @@ -0,0 +1,230 @@ +/* + * 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.turtle.upgrades; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.shared.turtle.blocks.TileTurtle; +import dan200.computercraft.shared.turtle.core.TurtlePlayer; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.inventory.CraftingInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.Recipe; +import net.minecraft.recipe.RecipeType; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; +import net.minecraftforge.common.ForgeHooks; +import net.minecraftforge.fml.hooks.BasicEventHooks; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class TurtleInventoryCrafting extends CraftingInventory +{ + private ITurtleAccess m_turtle; + private int m_xStart; + private int m_yStart; + + @SuppressWarnings( "ConstantConditions" ) + public TurtleInventoryCrafting( ITurtleAccess turtle ) + { + // Passing null in here is evil, but we don't have a container present. We override most methods in order to + // avoid throwing any NPEs. + super( null, 0, 0 ); + m_turtle = turtle; + m_xStart = 0; + m_yStart = 0; + } + + @Nullable + private Recipe tryCrafting( int xStart, int yStart ) + { + m_xStart = xStart; + m_yStart = yStart; + + // Check the non-relevant parts of the inventory are empty + for( int x = 0; x < TileTurtle.INVENTORY_WIDTH; x++ ) + { + for( int y = 0; y < TileTurtle.INVENTORY_HEIGHT; y++ ) + { + if( x < m_xStart || x >= m_xStart + 3 || + y < m_yStart || y >= m_yStart + 3 ) + { + if( !m_turtle.getInventory().getStack( x + y * TileTurtle.INVENTORY_WIDTH ).isEmpty() ) + { + return null; + } + } + } + } + + // Check the actual crafting + return m_turtle.getWorld().getRecipeManager().getFirstMatch( RecipeType.CRAFTING, this, m_turtle.getWorld() ).orElse( null ); + } + + @Nullable + public List doCrafting( World world, int maxCount ) + { + if( world.isClient || !(world instanceof ServerWorld) ) return null; + + // Find out what we can craft + Recipe recipe = tryCrafting( 0, 0 ); + if( recipe == null ) recipe = tryCrafting( 0, 1 ); + if( recipe == null ) recipe = tryCrafting( 1, 0 ); + if( recipe == null ) recipe = tryCrafting( 1, 1 ); + if( recipe == null ) return null; + + // Special case: craft(0) just returns an empty list if crafting was possible + if( maxCount == 0 ) return Collections.emptyList(); + + TurtlePlayer player = TurtlePlayer.get( m_turtle ); + + ArrayList results = new ArrayList<>(); + for( int i = 0; i < maxCount && recipe.matches( this, world ); i++ ) + { + ItemStack result = recipe.craft( this ); + if( result.isEmpty() ) break; + results.add( result ); + + result.onCraft( world, player, result.getCount() ); + BasicEventHooks.firePlayerCraftingEvent( player, result, this ); + + ForgeHooks.setCraftingPlayer( player ); + DefaultedList remainders = recipe.getRemainingStacks( this ); + ForgeHooks.setCraftingPlayer( null ); + + for( int slot = 0; slot < remainders.size(); slot++ ) + { + ItemStack existing = getStack( slot ); + ItemStack remainder = remainders.get( slot ); + + if( !existing.isEmpty() ) + { + removeStack( slot, 1 ); + existing = getStack( slot ); + } + + if( remainder.isEmpty() ) continue; + + // Either update the current stack or add it to the remainder list (to be inserted into the inventory + // afterwards). + if( existing.isEmpty() ) + { + setStack( slot, remainder ); + } + else if( ItemStack.areItemsEqualIgnoreDamage( existing, remainder ) && ItemStack.areTagsEqual( existing, remainder ) ) + { + remainder.increment( existing.getCount() ); + setStack( slot, remainder ); + } + else + { + results.add( remainder ); + } + } + } + + return results; + } + + @Override + public int getWidth() + { + return 3; + } + + @Override + public int getHeight() + { + return 3; + } + + private int modifyIndex( int index ) + { + int x = m_xStart + index % getWidth(); + int y = m_yStart + index / getHeight(); + return x >= 0 && x < TileTurtle.INVENTORY_WIDTH && y >= 0 && y < TileTurtle.INVENTORY_HEIGHT + ? x + y * TileTurtle.INVENTORY_WIDTH + : -1; + } + + // IInventory implementation + + @Override + public int size() + { + return getWidth() * getHeight(); + } + + @Nonnull + @Override + public ItemStack getStack( int i ) + { + i = modifyIndex( i ); + return m_turtle.getInventory().getStack( i ); + } + + @Nonnull + @Override + public ItemStack removeStack( int i ) + { + i = modifyIndex( i ); + return m_turtle.getInventory().removeStack( i ); + } + + @Nonnull + @Override + public ItemStack removeStack( int i, int size ) + { + i = modifyIndex( i ); + return m_turtle.getInventory().removeStack( i, size ); + } + + @Override + public void setStack( int i, @Nonnull ItemStack stack ) + { + i = modifyIndex( i ); + m_turtle.getInventory().setStack( i, stack ); + } + + @Override + public int getMaxCountPerStack() + { + return m_turtle.getInventory().getMaxCountPerStack(); + } + + @Override + public void markDirty() + { + m_turtle.getInventory().markDirty(); + } + + @Override + public boolean canPlayerUse( @Nonnull PlayerEntity player ) + { + return true; + } + + @Override + public boolean isValid( int i, @Nonnull ItemStack stack ) + { + i = modifyIndex( i ); + return m_turtle.getInventory().isValid( i, stack ); + } + + @Override + public void clear() + { + for( int i = 0; i < size(); i++ ) + { + int j = modifyIndex( i ); + m_turtle.getInventory().setStack( j, ItemStack.EMPTY ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java new file mode 100644 index 000000000..8e33891d5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java @@ -0,0 +1,163 @@ +/* + * 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.turtle.upgrades; + +import dan200.computercraft.api.client.TransformedModel; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.turtle.*; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.peripheral.modem.ModemState; +import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.util.ModelIdentifier; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; + +public class TurtleModem extends AbstractTurtleUpgrade +{ + private static class Peripheral extends WirelessModemPeripheral + { + private final ITurtleAccess turtle; + + Peripheral( ITurtleAccess turtle, boolean advanced ) + { + super( new ModemState(), advanced ); + this.turtle = turtle; + } + + @Nonnull + @Override + public World getWorld() + { + return turtle.getWorld(); + } + + @Nonnull + @Override + public Vec3d getPosition() + { + BlockPos turtlePos = turtle.getPosition(); + return new Vec3d( + turtlePos.getX(), + turtlePos.getY(), + turtlePos.getZ() + ); + } + + @Override + public boolean equals( IPeripheral other ) + { + return this == other || (other instanceof Peripheral && ((Peripheral) other).turtle == turtle); + } + } + + private final boolean advanced; + + @Environment(EnvType.CLIENT) + private ModelIdentifier m_leftOffModel; + + @Environment(EnvType.CLIENT) + private ModelIdentifier m_rightOffModel; + + @Environment(EnvType.CLIENT) + private ModelIdentifier m_leftOnModel; + + @Environment(EnvType.CLIENT) + private ModelIdentifier m_rightOnModel; + + public TurtleModem( boolean advanced, Identifier id ) + { + super( + id, TurtleUpgradeType.PERIPHERAL, + advanced + ? Registry.ModBlocks.WIRELESS_MODEM_ADVANCED + : Registry.ModBlocks.WIRELESS_MODEM_NORMAL + ); + this.advanced = advanced; + } + + @Override + public IPeripheral createPeripheral( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + return new Peripheral( turtle, advanced ); + } + + @Nonnull + @Override + public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull Direction dir ) + { + return TurtleCommandResult.failure(); + } + + @Environment(EnvType.CLIENT) + private void loadModelLocations() + { + if( m_leftOffModel == null ) + { + if( advanced ) + { + m_leftOffModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_off_left", "inventory" ); + m_rightOffModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_off_right", "inventory" ); + m_leftOnModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_on_left", "inventory" ); + m_rightOnModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_on_right", "inventory" ); + } + else + { + m_leftOffModel = new ModelIdentifier( "computercraft:turtle_modem_normal_off_left", "inventory" ); + m_rightOffModel = new ModelIdentifier( "computercraft:turtle_modem_normal_off_right", "inventory" ); + m_leftOnModel = new ModelIdentifier( "computercraft:turtle_modem_normal_on_left", "inventory" ); + m_rightOnModel = new ModelIdentifier( "computercraft:turtle_modem_normal_on_right", "inventory" ); + } + } + } + + @Nonnull + @Override + @Environment(EnvType.CLIENT) + public TransformedModel getModel( ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + loadModelLocations(); + + boolean active = false; + if( turtle != null ) + { + CompoundTag turtleNBT = turtle.getUpgradeNBTData( side ); + active = turtleNBT.contains( "active" ) && turtleNBT.getBoolean( "active" ); + } + + return side == TurtleSide.LEFT + ? TransformedModel.of( active ? m_leftOnModel : m_leftOffModel ) + : TransformedModel.of( active ? m_rightOnModel : m_rightOffModel ); + } + + @Override + public void update( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + // Advance the modem + if( !turtle.getWorld().isClient ) + { + IPeripheral peripheral = turtle.getPeripheral( side ); + if( peripheral instanceof Peripheral ) + { + ModemState state = ((Peripheral) peripheral).getModemState(); + if( state.pollChanged() ) + { + turtle.getUpgradeNBTData( side ).putBoolean( "active", state.isOpen() ); + turtle.updateUpgradeNBTData( side ); + } + } + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleShovel.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleShovel.java new file mode 100644 index 000000000..b9fb434ba --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleShovel.java @@ -0,0 +1,75 @@ +/* + * 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.turtle.upgrades; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.TurtleCommandResult; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.turtle.TurtleVerb; +import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand; +import dan200.computercraft.shared.turtle.core.TurtlePlayer; +import net.minecraft.block.BlockState; +import net.minecraft.block.Material; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public class TurtleShovel extends TurtleTool +{ + public TurtleShovel( Identifier id, String adjective, Item item ) + { + super( id, adjective, item ); + } + + public TurtleShovel( Identifier id, Item item ) + { + super( id, item ); + } + + public TurtleShovel( Identifier id, ItemStack craftItem, ItemStack toolItem ) + { + super( id, craftItem, toolItem ); + } + + @Override + protected boolean canBreakBlock( BlockState state, World world, BlockPos pos, TurtlePlayer player ) + { + if( !super.canBreakBlock( state, world, pos, player ) ) return false; + + Material material = state.getMaterial(); + return material == Material.SOIL || + material == Material.AGGREGATE || + material == Material.SNOW_LAYER || + material == Material.ORGANIC_PRODUCT || + material == Material.SNOW_BLOCK || + material == Material.PLANT || + material == Material.CACTUS || + material == Material.GOURD || + material == Material.LEAVES || + material == Material.REPLACEABLE_PLANT; + } + + @Nonnull + @Override + public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull Direction direction ) + { + if( verb == TurtleVerb.DIG ) + { + ItemStack shovel = item.copy(); + ItemStack remainder = TurtlePlaceCommand.deploy( shovel, turtle, direction, null, null ); + if( remainder != shovel ) + { + return TurtleCommandResult.success(); + } + } + return super.useTool( turtle, side, verb, direction ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java new file mode 100644 index 000000000..f2067b0e9 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java @@ -0,0 +1,106 @@ +/* + * 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.turtle.upgrades; + +import dan200.computercraft.api.client.TransformedModel; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.turtle.AbstractTurtleUpgrade; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.api.turtle.TurtleUpgradeType; +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.util.ModelIdentifier; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; + +public class TurtleSpeaker extends AbstractTurtleUpgrade +{ + private static class Peripheral extends SpeakerPeripheral + { + ITurtleAccess turtle; + + Peripheral( ITurtleAccess turtle ) + { + this.turtle = turtle; + } + + @Override + public World getWorld() + { + return turtle.getWorld(); + } + + @Override + public Vec3d getPosition() + { + BlockPos pos = turtle.getPosition(); + return new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ); + } + + @Override + public boolean equals( IPeripheral other ) + { + return this == other || (other instanceof Peripheral && turtle == ((Peripheral) other).turtle); + } + } + + @Environment(EnvType.CLIENT) + private ModelIdentifier m_leftModel; + + @Environment(EnvType.CLIENT) + private ModelIdentifier m_rightModel; + + public TurtleSpeaker( Identifier id ) + { + super( id, TurtleUpgradeType.PERIPHERAL, Registry.ModBlocks.SPEAKER ); + } + + @Override + public IPeripheral createPeripheral( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + return new TurtleSpeaker.Peripheral( turtle ); + } + + @Environment(EnvType.CLIENT) + private void loadModelLocations() + { + if( m_leftModel == null ) + { + m_leftModel = new ModelIdentifier( "computercraft:turtle_speaker_upgrade_left", "inventory" ); + m_rightModel = new ModelIdentifier( "computercraft:turtle_speaker_upgrade_right", "inventory" ); + } + } + + @Nonnull + @Override + @Environment(EnvType.CLIENT) + public TransformedModel getModel( ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + loadModelLocations(); + return TransformedModel.of( side == TurtleSide.LEFT ? m_leftModel : m_rightModel ); + } + + @Override + public void update( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide turtleSide ) + { + IPeripheral turtlePeripheral = turtle.getPeripheral( turtleSide ); + if( turtlePeripheral instanceof Peripheral ) + { + Peripheral peripheral = (Peripheral) turtlePeripheral; + peripheral.update(); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSword.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSword.java new file mode 100644 index 000000000..2fbdc8395 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSword.java @@ -0,0 +1,52 @@ +/* + * 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.turtle.upgrades; + +import dan200.computercraft.shared.turtle.core.TurtlePlayer; +import net.minecraft.block.BlockState; +import net.minecraft.block.Material; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +public class TurtleSword extends TurtleTool +{ + public TurtleSword( Identifier id, String adjective, Item item ) + { + super( id, adjective, item ); + } + + public TurtleSword( Identifier id, Item item ) + { + super( id, item ); + } + + public TurtleSword( Identifier id, ItemStack craftItem, ItemStack toolItem ) + { + super( id, craftItem, toolItem ); + } + + @Override + protected boolean canBreakBlock( BlockState state, World world, BlockPos pos, TurtlePlayer player ) + { + if( !super.canBreakBlock( state, world, pos, player ) ) return false; + + Material material = state.getMaterial(); + return material == Material.PLANT || + material == Material.LEAVES || + material == Material.REPLACEABLE_PLANT || + material == Material.WOOL || + material == Material.COBWEB; + } + + @Override + protected float getDamageMultiplier() + { + return 9.0f; + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java new file mode 100644 index 000000000..783aa8681 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java @@ -0,0 +1,277 @@ +/* + * 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.turtle.upgrades; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.client.TransformedModel; +import dan200.computercraft.api.turtle.*; +import dan200.computercraft.api.turtle.event.TurtleAttackEvent; +import dan200.computercraft.api.turtle.event.TurtleBlockEvent; +import dan200.computercraft.shared.TurtlePermissions; +import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand; +import dan200.computercraft.shared.turtle.core.TurtlePlayer; +import dan200.computercraft.shared.util.DropConsumer; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.WorldUtil; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.client.util.math.AffineTransformation; +import net.minecraft.entity.Entity; +import net.minecraft.entity.attribute.EntityAttributes; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.fluid.FluidState; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.AttackEntityEvent; +import net.minecraftforge.event.world.BlockEvent; +import org.apache.commons.lang3.tuple.Pair; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.function.Function; + +public class TurtleTool extends AbstractTurtleUpgrade +{ + protected final ItemStack item; + + public TurtleTool( Identifier id, String adjective, Item item ) + { + super( id, TurtleUpgradeType.TOOL, adjective, item ); + this.item = new ItemStack( item ); + } + + public TurtleTool( Identifier id, Item item ) + { + super( id, TurtleUpgradeType.TOOL, item ); + this.item = new ItemStack( item ); + } + + public TurtleTool( Identifier id, ItemStack craftItem, ItemStack toolItem ) + { + super( id, TurtleUpgradeType.TOOL, craftItem ); + this.item = toolItem; + } + + @Nonnull + @Override + @Environment(EnvType.CLIENT) + public TransformedModel getModel( ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + float xOffset = side == TurtleSide.LEFT ? -0.40625f : 0.40625f; + Matrix4f transform = new Matrix4f( new float[] { + 0.0f, 0.0f, -1.0f, 1.0f + xOffset, + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, -1.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 1.0f, + } ); + return TransformedModel.of( getCraftingItem(), new AffineTransformation( transform ) ); + } + + @Nonnull + @Override + public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull Direction direction ) + { + switch( verb ) + { + case ATTACK: + return attack( turtle, direction, side ); + case DIG: + return dig( turtle, direction, side ); + default: + return TurtleCommandResult.failure( "Unsupported action" ); + } + } + + protected boolean canBreakBlock( BlockState state, World world, BlockPos pos, TurtlePlayer player ) + { + Block block = state.getBlock(); + return !state.isAir( world, pos ) + && block != Blocks.BEDROCK + && state.calcBlockBreakingDelta( player, world, pos ) > 0 + && block.canEntityDestroy( state, world, pos, player ); + } + + protected float getDamageMultiplier() + { + return 3.0f; + } + + private TurtleCommandResult attack( final ITurtleAccess turtle, Direction direction, TurtleSide side ) + { + // Create a fake player, and orient it appropriately + final World world = turtle.getWorld(); + final BlockPos position = turtle.getPosition(); + final TurtlePlayer turtlePlayer = TurtlePlaceCommand.createPlayer( turtle, position, direction ); + + // See if there is an entity present + Vec3d turtlePos = turtlePlayer.getPos(); + Vec3d rayDir = turtlePlayer.getRotationVec( 1.0f ); + Pair hit = WorldUtil.rayTraceEntities( world, turtlePos, rayDir, 1.5 ); + if( hit != null ) + { + // Load up the turtle's inventory + ItemStack stackCopy = item.copy(); + turtlePlayer.loadInventory( stackCopy ); + + Entity hitEntity = hit.getKey(); + + // Fire several events to ensure we have permissions. + if( MinecraftForge.EVENT_BUS.post( new AttackEntityEvent( turtlePlayer, hitEntity ) ) || !hitEntity.isAttackable() ) + { + return TurtleCommandResult.failure( "Nothing to attack here" ); + } + + TurtleAttackEvent attackEvent = new TurtleAttackEvent( turtle, turtlePlayer, hitEntity, this, side ); + if( MinecraftForge.EVENT_BUS.post( attackEvent ) ) + { + return TurtleCommandResult.failure( attackEvent.getFailureMessage() ); + } + + // Start claiming entity drops + DropConsumer.set( hitEntity, turtleDropConsumer( turtle ) ); + + // Attack the entity + boolean attacked = false; + if( !hitEntity.handleAttack( turtlePlayer ) ) + { + float damage = (float) turtlePlayer.getAttributeValue( EntityAttributes.GENERIC_ATTACK_DAMAGE ); + damage *= getDamageMultiplier(); + if( damage > 0.0f ) + { + DamageSource source = DamageSource.player( turtlePlayer ); + if( hitEntity instanceof ArmorStandEntity ) + { + // Special case for armor stands: attack twice to guarantee destroy + hitEntity.damage( source, damage ); + if( hitEntity.isAlive() ) + { + hitEntity.damage( source, damage ); + } + attacked = true; + } + else + { + if( hitEntity.damage( source, damage ) ) + { + attacked = true; + } + } + } + } + + // Stop claiming drops + stopConsuming( turtle ); + + // Put everything we collected into the turtles inventory, then return + if( attacked ) + { + turtlePlayer.unloadInventory( turtle ); + return TurtleCommandResult.success(); + } + } + + return TurtleCommandResult.failure( "Nothing to attack here" ); + } + + private TurtleCommandResult dig( ITurtleAccess turtle, Direction direction, TurtleSide side ) + { + // Get ready to dig + World world = turtle.getWorld(); + BlockPos turtlePosition = turtle.getPosition(); + BlockPos blockPosition = turtlePosition.offset( direction ); + + if( world.isAir( blockPosition ) || WorldUtil.isLiquidBlock( world, blockPosition ) ) + { + return TurtleCommandResult.failure( "Nothing to dig here" ); + } + + BlockState state = world.getBlockState( blockPosition ); + FluidState fluidState = world.getFluidState( blockPosition ); + + TurtlePlayer turtlePlayer = TurtlePlaceCommand.createPlayer( turtle, turtlePosition, direction ); + turtlePlayer.loadInventory( item.copy() ); + + if( ComputerCraft.turtlesObeyBlockProtection ) + { + // Check spawn protection + if( MinecraftForge.EVENT_BUS.post( new BlockEvent.BreakEvent( world, blockPosition, state, turtlePlayer ) ) ) + { + return TurtleCommandResult.failure( "Cannot break protected block" ); + } + + if( !TurtlePermissions.isBlockEditable( world, blockPosition, turtlePlayer ) ) + { + return TurtleCommandResult.failure( "Cannot break protected block" ); + } + } + + // Check if we can break the block + if( !canBreakBlock( state, world, blockPosition, turtlePlayer ) ) + { + return TurtleCommandResult.failure( "Unbreakable block detected" ); + } + + // Fire the dig event, checking whether it was cancelled. + TurtleBlockEvent.Dig digEvent = new TurtleBlockEvent.Dig( turtle, turtlePlayer, world, blockPosition, state, this, side ); + if( MinecraftForge.EVENT_BUS.post( digEvent ) ) + { + return TurtleCommandResult.failure( digEvent.getFailureMessage() ); + } + + // Consume the items the block drops + DropConsumer.set( world, blockPosition, turtleDropConsumer( turtle ) ); + + BlockEntity tile = world.getBlockEntity( blockPosition ); + + // Much of this logic comes from PlayerInteractionManager#tryHarvestBlock, so it's a good idea + // to consult there before making any changes. + + // Play the destruction sound and particles + world.syncWorldEvent( 2001, blockPosition, Block.getRawIdFromState( state ) ); + + // Destroy the block + boolean canHarvest = state.canHarvestBlock( world, blockPosition, turtlePlayer ); + boolean canBreak = state.removedByPlayer( world, blockPosition, turtlePlayer, canHarvest, fluidState ); + if( canBreak ) state.getBlock().onBroken( world, blockPosition, state ); + if( canHarvest && canBreak ) + { + state.getBlock().afterBreak( world, turtlePlayer, blockPosition, state, tile, turtlePlayer.getMainHandStack() ); + } + + stopConsuming( turtle ); + + return TurtleCommandResult.success(); + + } + + private static Function turtleDropConsumer( ITurtleAccess turtle ) + { + return drop -> InventoryUtil.storeItems( drop, turtle.getItemHandler(), turtle.getSelectedSlot() ); + } + + private static void stopConsuming( ITurtleAccess turtle ) + { + List extra = DropConsumer.clear(); + for( ItemStack remainder : extra ) + { + WorldUtil.dropItemStack( remainder, turtle.getWorld(), turtle.getPosition(), turtle.getDirection().getOpposite() ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/BasicRecipeSerializer.java b/src/main/java/dan200/computercraft/shared/util/BasicRecipeSerializer.java new file mode 100644 index 000000000..2dd0f7423 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/BasicRecipeSerializer.java @@ -0,0 +1,19 @@ +/* + * 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.util; + +import net.minecraft.recipe.Recipe; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraftforge.registries.ForgeRegistryEntry; + +/** + * A {@link IRecipeSerializer} which implements all the Forge registry entries. + * + * @param The reciep serializer + */ +public abstract class BasicRecipeSerializer> extends ForgeRegistryEntry> implements RecipeSerializer +{ +} diff --git a/src/main/java/dan200/computercraft/shared/util/CapabilityUtil.java b/src/main/java/dan200/computercraft/shared/util/CapabilityUtil.java new file mode 100644 index 000000000..7643eeb2c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/CapabilityUtil.java @@ -0,0 +1,47 @@ +/* + * 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.util; + +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.common.util.NonNullConsumer; + +import javax.annotation.Nullable; + +public final class CapabilityUtil +{ + private CapabilityUtil() + { + } + + @Nullable + public static LazyOptional invalidate( @Nullable LazyOptional cap ) + { + if( cap != null ) cap.invalidate(); + return null; + } + + public static void invalidate( @Nullable LazyOptional[] caps ) + { + if( caps == null ) return; + + for( int i = 0; i < caps.length; i++ ) + { + LazyOptional cap = caps[i]; + if( cap != null ) cap.invalidate(); + caps[i] = null; + } + } + + @Nullable + public static T unwrap( LazyOptional p, NonNullConsumer> invalidate ) + { + if( !p.isPresent() ) return null; + + p.addListener( invalidate ); + return p.orElseThrow( NullPointerException::new ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/Colour.java b/src/main/java/dan200/computercraft/shared/util/Colour.java new file mode 100644 index 000000000..5c9e6b86d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/Colour.java @@ -0,0 +1,91 @@ +/* + * 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.util; + +public enum Colour +{ + BLACK( 0x111111 ), + RED( 0xcc4c4c ), + GREEN( 0x57A64E ), + BROWN( 0x7f664c ), + BLUE( 0x3366cc ), + PURPLE( 0xb266e5 ), + CYAN( 0x4c99b2 ), + LIGHT_GREY( 0x999999 ), + GREY( 0x4c4c4c ), + PINK( 0xf2b2cc ), + LIME( 0x7fcc19 ), + YELLOW( 0xdede6c ), + LIGHT_BLUE( 0x99b2f2 ), + MAGENTA( 0xe57fd8 ), + ORANGE( 0xf2b233 ), + WHITE( 0xf0f0f0 ); + + public static final Colour[] VALUES = values(); + + public static Colour fromInt( int colour ) + { + return colour >= 0 && colour < 16 ? Colour.VALUES[colour] : null; + } + + public static Colour fromHex( int colour ) + { + for( Colour entry : VALUES ) + { + if( entry.getHex() == colour ) return entry; + } + + return null; + } + + private final int hex; + private final float[] rgb; + + Colour( int hex ) + { + this.hex = hex; + rgb = new float[] { + ((hex >> 16) & 0xFF) / 255.0f, + ((hex >> 8) & 0xFF) / 255.0f, + (hex & 0xFF) / 255.0f, + }; + } + + public Colour getNext() + { + return VALUES[(ordinal() + 1) % 16]; + } + + public Colour getPrevious() + { + return VALUES[(ordinal() + 15) % 16]; + } + + public int getHex() + { + return hex; + } + + public float[] getRGB() + { + return rgb; + } + + public float getR() + { + return rgb[0]; + } + + public float getG() + { + return rgb[1]; + } + + public float getB() + { + return rgb[2]; + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/ColourTracker.java b/src/main/java/dan200/computercraft/shared/util/ColourTracker.java new file mode 100644 index 000000000..b7118ff7d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/ColourTracker.java @@ -0,0 +1,53 @@ +/* + * 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.util; + +/** + * A reimplementation of the colour system in {@link ArmorDyeRecipe}, but + * bundled together as an object. + */ +public class ColourTracker +{ + private int total; + private int totalR; + private int totalG; + private int totalB; + private int count; + + public void addColour( int r, int g, int b ) + { + total += Math.max( r, Math.max( g, b ) ); + totalR += r; + totalG += g; + totalB += b; + count++; + } + + public void addColour( float r, float g, float b ) + { + addColour( (int) (r * 255), (int) (g * 255), (int) (b * 255) ); + } + + public boolean hasColour() + { + return count > 0; + } + + public int getColour() + { + int avgR = totalR / count; + int avgG = totalG / count; + int avgB = totalB / count; + + float avgTotal = (float) total / count; + float avgMax = Math.max( avgR, Math.max( avgG, avgB ) ); + avgR = (int) (avgR * avgTotal / avgMax); + avgG = (int) (avgG * avgTotal / avgMax); + avgB = (int) (avgB * avgTotal / avgMax); + + return (avgR << 16) | (avgG << 8) | avgB; + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/ColourUtils.java b/src/main/java/dan200/computercraft/shared/util/ColourUtils.java new file mode 100644 index 000000000..41eb93f6a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/ColourUtils.java @@ -0,0 +1,51 @@ +/* + * 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.util; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.tag.Tag; +import net.minecraft.util.DyeColor; +import net.minecraftforge.common.Tags; + +import javax.annotation.Nullable; + +public final class ColourUtils +{ + @SuppressWarnings( { "unchecked", "rawtypes" } ) + private static final Tag[] DYES = new Tag[] { + Tags.Items.DYES_WHITE, + Tags.Items.DYES_ORANGE, + Tags.Items.DYES_MAGENTA, + Tags.Items.DYES_LIGHT_BLUE, + Tags.Items.DYES_YELLOW, + Tags.Items.DYES_LIME, + Tags.Items.DYES_PINK, + Tags.Items.DYES_GRAY, + Tags.Items.DYES_LIGHT_GRAY, + Tags.Items.DYES_CYAN, + Tags.Items.DYES_PURPLE, + Tags.Items.DYES_BLUE, + Tags.Items.DYES_BROWN, + Tags.Items.DYES_GREEN, + Tags.Items.DYES_RED, + Tags.Items.DYES_BLACK, + }; + + @Nullable + private ColourUtils() {} + + public static DyeColor getStackColour( ItemStack stack ) + { + for( int i = 0; i < DYES.length; i++ ) + { + Tag dye = DYES[i]; + if( dye.contains( stack.getItem() ) ) return DyeColor.byId( i ); + } + + return null; + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/CreativeTabMain.java b/src/main/java/dan200/computercraft/shared/util/CreativeTabMain.java new file mode 100644 index 000000000..0f0fa999c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/CreativeTabMain.java @@ -0,0 +1,33 @@ +/* + * 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.util; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.Registry; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; + +public class CreativeTabMain extends ItemGroup +{ + public CreativeTabMain() + { + super( ComputerCraft.MOD_ID ); + } + + @Nonnull + @Override + @Environment(EnvType.CLIENT) + public ItemStack createIcon() + { + return new ItemStack( Registry.ModBlocks.COMPUTER_NORMAL.get() ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/DefaultInventory.java b/src/main/java/dan200/computercraft/shared/util/DefaultInventory.java new file mode 100644 index 000000000..83fbb9492 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/DefaultInventory.java @@ -0,0 +1,37 @@ +/* + * 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.util; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; + +public interface DefaultInventory extends Inventory +{ + @Override + default int getMaxCountPerStack() + { + return 64; + } + + @Override + default void onOpen( @Nonnull PlayerEntity player ) + { + } + + @Override + default void onClose( @Nonnull PlayerEntity player ) + { + } + + @Override + default boolean isValid( int slot, @Nonnull ItemStack stack ) + { + return true; + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/DefaultSidedInventory.java b/src/main/java/dan200/computercraft/shared/util/DefaultSidedInventory.java new file mode 100644 index 000000000..9dd444793 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/DefaultSidedInventory.java @@ -0,0 +1,27 @@ +/* + * 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.util; + +import net.minecraft.inventory.SidedInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.Direction; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public interface DefaultSidedInventory extends DefaultInventory, SidedInventory +{ + @Override + default boolean canInsert( int slot, @Nonnull ItemStack stack, @Nullable Direction side ) + { + return isValid( slot, stack ); + } + + @Override + default boolean canExtract( int slot, @Nonnull ItemStack stack, @Nonnull Direction side ) + { + return true; + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java b/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java new file mode 100644 index 000000000..4d6170be5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java @@ -0,0 +1,41 @@ +/* + * 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.util; + +import dan200.computercraft.core.computer.ComputerSide; +import net.minecraft.util.math.Direction; + +public final class DirectionUtil +{ + private DirectionUtil() {} + + public static final Direction[] FACINGS = Direction.values(); + + public static ComputerSide toLocal( Direction front, Direction dir ) + { + if( front.getAxis() == Direction.Axis.Y ) front = Direction.NORTH; + + if( dir == front ) return ComputerSide.FRONT; + if( dir == front.getOpposite() ) return ComputerSide.BACK; + if( dir == front.rotateYCounterclockwise() ) return ComputerSide.LEFT; + if( dir == front.rotateYClockwise() ) return ComputerSide.RIGHT; + if( dir == Direction.UP ) return ComputerSide.TOP; + return ComputerSide.BOTTOM; + } + + public static float toPitchAngle( Direction dir ) + { + switch( dir ) + { + case DOWN: + return 90.0f; + case UP: + return 270.0f; + default: + return 0.0f; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/DropConsumer.java b/src/main/java/dan200/computercraft/shared/util/DropConsumer.java new file mode 100644 index 000000000..96cc72936 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/DropConsumer.java @@ -0,0 +1,97 @@ +/* + * 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.util; + +import dan200.computercraft.ComputerCraft; +import net.minecraft.entity.Entity; +import net.minecraft.entity.ItemEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.world.World; +import net.minecraftforge.event.entity.EntityJoinWorldEvent; +import net.minecraftforge.event.entity.living.LivingDropsEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID ) +public final class DropConsumer +{ + private DropConsumer() + { + } + + private static Function dropConsumer; + private static List remainingDrops; + private static World dropWorld; + private static Box dropBounds; + private static Entity dropEntity; + + public static void set( Entity entity, Function consumer ) + { + dropConsumer = consumer; + remainingDrops = new ArrayList<>(); + dropEntity = entity; + dropWorld = entity.world; + dropBounds = new Box( entity.getBlockPos() ).expand( 2, 2, 2 ); + + entity.captureDrops( new ArrayList<>() ); + } + + public static void set( World world, BlockPos pos, Function consumer ) + { + dropConsumer = consumer; + remainingDrops = new ArrayList<>( 2 ); + dropEntity = null; + dropWorld = world; + dropBounds = new Box( pos ).expand( 2, 2, 2 ); + } + + public static List clear() + { + List remainingStacks = remainingDrops; + + dropConsumer = null; + remainingDrops = null; + dropEntity = null; + dropWorld = null; + dropBounds = null; + + return remainingStacks; + } + + private static void handleDrops( ItemStack stack ) + { + ItemStack remaining = dropConsumer.apply( stack ); + if( !remaining.isEmpty() ) remainingDrops.add( remaining ); + } + + @SubscribeEvent( priority = EventPriority.HIGHEST ) + public static void onEntitySpawn( EntityJoinWorldEvent event ) + { + // Capture any nearby item spawns + if( dropWorld == event.getWorld() && event.getEntity() instanceof ItemEntity + && dropBounds.contains( event.getEntity().getPos() ) ) + { + handleDrops( ((ItemEntity) event.getEntity()).getStack() ); + event.setCanceled( true ); + } + } + + @SubscribeEvent + public static void onLivingDrops( LivingDropsEvent drops ) + { + if( dropEntity == null || drops.getEntity() != dropEntity ) return; + + for( ItemEntity drop : drops.getDrops() ) handleDrops( drop.getStack() ); + drops.setCanceled( true ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java b/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java new file mode 100644 index 000000000..76285f9a1 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java @@ -0,0 +1,397 @@ +/* + * 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.util; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import net.minecraft.network.*; +import net.minecraft.network.listener.PacketListener; +import net.minecraft.network.packet.c2s.play.AdvancementTabC2SPacket; +import net.minecraft.network.packet.c2s.play.BoatPaddleStateC2SPacket; +import net.minecraft.network.packet.c2s.play.BookUpdateC2SPacket; +import net.minecraft.network.packet.c2s.play.ButtonClickC2SPacket; +import net.minecraft.network.packet.c2s.play.ChatMessageC2SPacket; +import net.minecraft.network.packet.c2s.play.ClickWindowC2SPacket; +import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket; +import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket; +import net.minecraft.network.packet.c2s.play.ClientStatusC2SPacket; +import net.minecraft.network.packet.c2s.play.ConfirmGuiActionC2SPacket; +import net.minecraft.network.packet.c2s.play.CraftRequestC2SPacket; +import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket; +import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket; +import net.minecraft.network.packet.c2s.play.GuiCloseC2SPacket; +import net.minecraft.network.packet.c2s.play.HandSwingC2SPacket; +import net.minecraft.network.packet.c2s.play.KeepAliveC2SPacket; +import net.minecraft.network.packet.c2s.play.PickFromInventoryC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerInputC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerInteractBlockC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerInteractEntityC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerInteractItemC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; +import net.minecraft.network.packet.c2s.play.QueryBlockNbtC2SPacket; +import net.minecraft.network.packet.c2s.play.QueryEntityNbtC2SPacket; +import net.minecraft.network.packet.c2s.play.RenameItemC2SPacket; +import net.minecraft.network.packet.c2s.play.RequestCommandCompletionsC2SPacket; +import net.minecraft.network.packet.c2s.play.ResourcePackStatusC2SPacket; +import net.minecraft.network.packet.c2s.play.SelectVillagerTradeC2SPacket; +import net.minecraft.network.packet.c2s.play.SpectatorTeleportC2SPacket; +import net.minecraft.network.packet.c2s.play.TeleportConfirmC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateBeaconC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateCommandBlockC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateCommandBlockMinecartC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateDifficultyC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateDifficultyLockC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateJigsawC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdatePlayerAbilitiesC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateSelectedSlotC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateSignC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateStructureBlockC2SPacket; +import net.minecraft.network.packet.c2s.play.VehicleMoveC2SPacket; +import net.minecraft.network.play.client.*; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.text.Text; +import net.minecraftforge.common.util.FakePlayer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.crypto.SecretKey; + +public class FakeNetHandler extends ServerPlayNetworkHandler +{ + public FakeNetHandler( @Nonnull FakePlayer player ) + { + super( player.getServerWorld().getServer(), new FakeNetworkManager(), player ); + } + + @Override + public void tick() + { + } + + @Override + public void disconnect( @Nonnull Text reason ) + { + } + + @Override + public void onDisconnected( @Nonnull Text reason ) + { + } + + @Override + public void sendPacket( @Nonnull Packet packet ) + { + } + + @Override + public void sendPacket( @Nonnull Packet packet, @Nullable GenericFutureListener> whenSent ) + { + } + + @Override + public void onPlayerInput( @Nonnull PlayerInputC2SPacket packet ) + { + } + + @Override + public void onVehicleMove( @Nonnull VehicleMoveC2SPacket packet ) + { + } + + @Override + public void onTeleportConfirm( @Nonnull TeleportConfirmC2SPacket packet ) + { + } + + @Override + public void onAdvancementTab( @Nonnull AdvancementTabC2SPacket packet ) + { + } + + @Override + public void onRequestCommandCompletions( @Nonnull RequestCommandCompletionsC2SPacket packet ) + { + } + + @Override + public void onUpdateCommandBlock( @Nonnull UpdateCommandBlockC2SPacket packet ) + { + } + + @Override + public void onUpdateCommandBlockMinecart( @Nonnull UpdateCommandBlockMinecartC2SPacket packet ) + { + } + + @Override + public void onPickFromInventory( @Nonnull PickFromInventoryC2SPacket packet ) + { + } + + @Override + public void onRenameItem( @Nonnull RenameItemC2SPacket packet ) + { + } + + @Override + public void onUpdateBeacon( @Nonnull UpdateBeaconC2SPacket packet ) + { + } + + @Override + public void onStructureBlockUpdate( @Nonnull UpdateStructureBlockC2SPacket packet ) + { + } + + @Override + public void onJigsawUpdate( @Nonnull UpdateJigsawC2SPacket packet ) + { + } + + @Override + public void onVillagerTradeSelect( @Nonnull SelectVillagerTradeC2SPacket packet ) + { + } + + @Override + public void onBookUpdate( @Nonnull BookUpdateC2SPacket packet ) + { + } + + @Override + public void onQueryEntityNbt( @Nonnull QueryEntityNbtC2SPacket packet ) + { + } + + @Override + public void onQueryBlockNbt( @Nonnull QueryBlockNbtC2SPacket packet ) + { + } + + @Override + public void onPlayerMove( @Nonnull PlayerMoveC2SPacket packet ) + { + } + + @Override + public void onPlayerAction( @Nonnull PlayerActionC2SPacket packet ) + { + } + + @Override + public void onPlayerInteractBlock( @Nonnull PlayerInteractBlockC2SPacket packet ) + { + } + + @Override + public void onPlayerInteractItem( @Nonnull PlayerInteractItemC2SPacket packet ) + { + } + + @Override + public void onSpectatorTeleport( @Nonnull SpectatorTeleportC2SPacket packet ) + { + } + + @Override + public void onResourcePackStatus( @Nonnull ResourcePackStatusC2SPacket packet ) + { + } + + @Override + public void onBoatPaddleState( @Nonnull BoatPaddleStateC2SPacket packet ) + { + } + + @Override + public void onUpdateSelectedSlot( @Nonnull UpdateSelectedSlotC2SPacket packet ) + { + } + + @Override + public void onGameMessage( @Nonnull ChatMessageC2SPacket packet ) + { + } + + @Override + public void onHandSwing( @Nonnull HandSwingC2SPacket packet ) + { + } + + @Override + public void onClientCommand( @Nonnull ClientCommandC2SPacket packet ) + { + } + + @Override + public void onPlayerInteractEntity( @Nonnull PlayerInteractEntityC2SPacket packet ) + { + } + + @Override + public void onClientStatus( @Nonnull ClientStatusC2SPacket packet ) + { + } + + @Override + public void onGuiClose( @Nonnull GuiCloseC2SPacket packet ) + { + } + + @Override + public void onClickWindow( @Nonnull ClickWindowC2SPacket packet ) + { + } + + @Override + public void onCraftRequest( @Nonnull CraftRequestC2SPacket packet ) + { + } + + @Override + public void onButtonClick( @Nonnull ButtonClickC2SPacket packet ) + { + } + + @Override + public void onCreativeInventoryAction( @Nonnull CreativeInventoryActionC2SPacket packet ) + { + } + + @Override + public void onConfirmTransaction( @Nonnull ConfirmGuiActionC2SPacket packet ) + { + } + + @Override + public void onSignUpdate( @Nonnull UpdateSignC2SPacket packet ) + { + } + + @Override + public void onKeepAlive( @Nonnull KeepAliveC2SPacket packet ) + { + } + + @Override + public void onPlayerAbilities( @Nonnull UpdatePlayerAbilitiesC2SPacket packet ) + { + } + + @Override + public void onClientSettings( @Nonnull ClientSettingsC2SPacket packet ) + { + } + + @Override + public void onCustomPayload( @Nonnull CustomPayloadC2SPacket packet ) + { + } + + @Override + public void onUpdateDifficulty( @Nonnull UpdateDifficultyC2SPacket packet ) + { + } + + @Override + public void onUpdateDifficultyLock( @Nonnull UpdateDifficultyLockC2SPacket packet ) + { + } + + private static class FakeNetworkManager extends ClientConnection + { + private PacketListener handler; + private Text closeReason; + + FakeNetworkManager() + { + super( NetworkSide.CLIENTBOUND ); + } + + @Override + public void channelActive( @Nonnull ChannelHandlerContext context ) + { + } + + @Override + public void setState( @Nonnull NetworkState state ) + { + } + + @Override + public void channelInactive( @Nonnull ChannelHandlerContext context ) + { + } + + @Override + public void exceptionCaught( @Nonnull ChannelHandlerContext context, @Nonnull Throwable err ) + { + } + + @Override + protected void channelRead0( @Nonnull ChannelHandlerContext context, @Nonnull Packet packet ) + { + } + + @Override + public void setPacketListener( @Nonnull PacketListener handler ) + { + this.handler = handler; + } + + @Override + public void send( @Nonnull Packet packet ) + { + } + + @Override + public void send( @Nonnull Packet packet, @Nullable GenericFutureListener> whenSent ) + { + } + + @Override + public void tick() + { + } + + @Override + public void disconnect( @Nonnull Text message ) + { + this.closeReason = message; + } + + @Override + public void setupEncryption( @Nonnull SecretKey key ) + { + } + + @Nonnull + @Override + public PacketListener getPacketListener() + { + return handler; + } + + @Nullable + @Override + public Text getDisconnectReason() + { + return closeReason; + } + + @Override + public void disableAutoRead() + { + } + + @Override + public void setCompressionThreshold( int threshold ) + { + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/FixedPointTileEntityType.java b/src/main/java/dan200/computercraft/shared/util/FixedPointTileEntityType.java new file mode 100644 index 000000000..4fe7f61ec --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/FixedPointTileEntityType.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.util; + +import net.minecraft.block.Block; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A {@link TileEntityType} whose supplier uses itself as an argument. + * + * @param The type of the produced tile entity. + */ +public final class FixedPointTileEntityType extends BlockEntityType +{ + private final Supplier block; + + private FixedPointTileEntityType( Supplier block, Supplier builder ) + { + super( builder, Collections.emptySet(), null ); + this.block = block; + } + + public static FixedPointTileEntityType create( Supplier block, Function, T> builder ) + { + return new FixedPointSupplier<>( block, builder ).factory; + } + + @Override + public boolean supports( @Nonnull Block block ) + { + return block == this.block.get(); + } + + private static final class FixedPointSupplier implements Supplier + { + final FixedPointTileEntityType factory; + private final Function, T> builder; + + private FixedPointSupplier( Supplier block, Function, T> builder ) + { + factory = new FixedPointTileEntityType<>( block, this ); + this.builder = builder; + } + + @Override + public T get() + { + return builder.apply( factory ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/Holiday.java b/src/main/java/dan200/computercraft/shared/util/Holiday.java new file mode 100644 index 000000000..b4728d46c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/Holiday.java @@ -0,0 +1,15 @@ +/* + * 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.util; + +public enum Holiday +{ + NONE, + VALENTINES, + APRIL_FOOLS_DAY, + HALLOWEEN, + CHRISTMAS, +} diff --git a/src/main/java/dan200/computercraft/shared/util/HolidayUtil.java b/src/main/java/dan200/computercraft/shared/util/HolidayUtil.java new file mode 100644 index 000000000..ea9e5dd74 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/HolidayUtil.java @@ -0,0 +1,29 @@ +/* + * 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.util; + +import java.util.Calendar; + +public final class HolidayUtil +{ + private HolidayUtil() {} + + public static Holiday getCurrentHoliday() + { + return getHoliday( Calendar.getInstance() ); + } + + private static Holiday getHoliday( Calendar calendar ) + { + int month = calendar.get( Calendar.MONTH ); + int day = calendar.get( Calendar.DAY_OF_MONTH ); + if( month == Calendar.FEBRUARY && day == 14 ) return Holiday.VALENTINES; + if( month == Calendar.APRIL && day == 1 ) return Holiday.APRIL_FOOLS_DAY; + if( month == Calendar.OCTOBER && day == 31 ) return Holiday.HALLOWEEN; + if( month == Calendar.DECEMBER && day >= 24 && day <= 30 ) return Holiday.CHRISTMAS; + return Holiday.NONE; + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/IDAssigner.java b/src/main/java/dan200/computercraft/shared/util/IDAssigner.java new file mode 100644 index 000000000..9ab33553c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/IDAssigner.java @@ -0,0 +1,106 @@ +/* + * 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.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import dan200.computercraft.ComputerCraft; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.WorldSavePath; +import net.minecraftforge.fml.server.ServerLifecycleHooks; + +import java.io.File; +import java.io.Reader; +import java.io.Writer; +import java.lang.ref.WeakReference; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +public final class IDAssigner +{ + private static final WorldSavePath FOLDER = new WorldSavePath( ComputerCraft.MOD_ID ); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Type ID_TOKEN = new TypeToken>() + { + }.getType(); + + private IDAssigner() + { + } + + private static Map ids; + private static WeakReference server; + private static Path idFile; + + public static File getDir() + { + return ServerLifecycleHooks.getCurrentServer().getSavePath( FOLDER ).toFile(); + } + + private static MinecraftServer getCachedServer() + { + if( server == null ) return null; + + MinecraftServer currentServer = server.get(); + if( currentServer == null ) return null; + + if( currentServer != ServerLifecycleHooks.getCurrentServer() ) return null; + return currentServer; + } + + public static synchronized int getNextId( String kind ) + { + MinecraftServer currentServer = getCachedServer(); + if( currentServer == null ) + { + // The server has changed, refetch our ID map + server = new WeakReference<>( ServerLifecycleHooks.getCurrentServer() ); + + File dir = getDir(); + dir.mkdirs(); + + // Load our ID file from disk + idFile = new File( dir, "ids.json" ).toPath(); + if( Files.isRegularFile( idFile ) ) + { + try( Reader reader = Files.newBufferedReader( idFile, StandardCharsets.UTF_8 ) ) + { + ids = GSON.fromJson( reader, ID_TOKEN ); + } + catch( Exception e ) + { + ComputerCraft.log.error( "Cannot load id file '" + idFile + "'", e ); + ids = new HashMap<>(); + } + } + else + { + ids = new HashMap<>(); + } + } + + Integer existing = ids.get( kind ); + int next = existing == null ? 0 : existing + 1; + ids.put( kind, next ); + + // We've changed the ID file, so save it back again. + try( Writer writer = Files.newBufferedWriter( idFile, StandardCharsets.UTF_8 ) ) + { + GSON.toJson( ids, writer ); + } + catch( Exception e ) + { + ComputerCraft.log.error( "Cannot update ID file '" + idFile + "'", e ); + } + + return next; + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/ImpostorRecipe.java b/src/main/java/dan200/computercraft/shared/util/ImpostorRecipe.java new file mode 100644 index 000000000..3a4862af0 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/ImpostorRecipe.java @@ -0,0 +1,93 @@ +/* + * 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.util; + +import com.google.gson.JsonObject; +import net.minecraft.inventory.CraftingInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.ShapedRecipe; +import net.minecraft.util.Identifier; +import net.minecraft.util.JsonHelper; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; +import net.minecraftforge.common.crafting.CraftingHelper; + +import javax.annotation.Nonnull; + +public final class ImpostorRecipe extends ShapedRecipe +{ + private final String group; + + private ImpostorRecipe( @Nonnull Identifier id, @Nonnull String group, int width, int height, DefaultedList ingredients, @Nonnull ItemStack result ) + { + super( id, group, width, height, ingredients, result ); + this.group = group; + } + + @Nonnull + @Override + public String getGroup() + { + return group; + } + + @Override + public boolean matches( @Nonnull CraftingInventory inv, @Nonnull World world ) + { + return false; + } + + @Nonnull + @Override + public ItemStack craft( @Nonnull CraftingInventory inventory ) + { + return ItemStack.EMPTY; + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + public static final RecipeSerializer SERIALIZER = new BasicRecipeSerializer() + { + @Override + public ImpostorRecipe read( @Nonnull Identifier identifier, @Nonnull JsonObject json ) + { + String group = JsonHelper.getString( json, "group", "" ); + ShapedRecipe recipe = RecipeSerializer.SHAPED.read( identifier, json ); + ItemStack result = CraftingHelper.getItemStack( JsonHelper.getObject( json, "result" ), true ); + return new ImpostorRecipe( identifier, group, recipe.getWidth(), recipe.getHeight(), recipe.getPreviewInputs(), result ); + } + + @Override + public ImpostorRecipe read( @Nonnull Identifier identifier, @Nonnull PacketByteBuf buf ) + { + int width = buf.readVarInt(); + int height = buf.readVarInt(); + String group = buf.readString( Short.MAX_VALUE ); + DefaultedList items = DefaultedList.ofSize( width * height, Ingredient.EMPTY ); + for( int k = 0; k < items.size(); ++k ) items.set( k, Ingredient.fromPacket( buf ) ); + ItemStack result = buf.readItemStack(); + return new ImpostorRecipe( identifier, group, width, height, items, result ); + } + + @Override + public void write( @Nonnull PacketByteBuf buf, @Nonnull ImpostorRecipe recipe ) + { + buf.writeVarInt( recipe.getRecipeWidth() ); + buf.writeVarInt( recipe.getRecipeHeight() ); + buf.writeString( recipe.getGroup() ); + for( Ingredient ingredient : recipe.getPreviewInputs() ) ingredient.write( buf ); + buf.writeItemStack( recipe.getOutput() ); + } + }; +} diff --git a/src/main/java/dan200/computercraft/shared/util/ImpostorShapelessRecipe.java b/src/main/java/dan200/computercraft/shared/util/ImpostorShapelessRecipe.java new file mode 100644 index 000000000..ca34188df --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/ImpostorShapelessRecipe.java @@ -0,0 +1,115 @@ +/* + * 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.util; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import net.minecraft.inventory.CraftingInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.ShapelessRecipe; +import net.minecraft.util.Identifier; +import net.minecraft.util.JsonHelper; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; +import net.minecraftforge.common.crafting.CraftingHelper; + +import javax.annotation.Nonnull; + +public final class ImpostorShapelessRecipe extends ShapelessRecipe +{ + private final String group; + + private ImpostorShapelessRecipe( @Nonnull Identifier id, @Nonnull String group, @Nonnull ItemStack result, DefaultedList ingredients ) + { + super( id, group, result, ingredients ); + this.group = group; + } + + @Nonnull + @Override + public String getGroup() + { + return group; + } + + @Override + public boolean matches( @Nonnull CraftingInventory inv, @Nonnull World world ) + { + return false; + } + + @Nonnull + @Override + public ItemStack craft( @Nonnull CraftingInventory inventory ) + { + return ItemStack.EMPTY; + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + public static final RecipeSerializer SERIALIZER = new BasicRecipeSerializer() + { + @Override + public ImpostorShapelessRecipe read( @Nonnull Identifier id, @Nonnull JsonObject json ) + { + String s = JsonHelper.getString( json, "group", "" ); + DefaultedList ingredients = readIngredients( JsonHelper.getArray( json, "ingredients" ) ); + + if( ingredients.isEmpty() ) throw new JsonParseException( "No ingredients for shapeless recipe" ); + if( ingredients.size() > 9 ) + { + throw new JsonParseException( "Too many ingredients for shapeless recipe the max is 9" ); + } + + ItemStack itemstack = CraftingHelper.getItemStack( JsonHelper.getObject( json, "result" ), true ); + return new ImpostorShapelessRecipe( id, s, itemstack, ingredients ); + } + + private DefaultedList readIngredients( JsonArray arrays ) + { + DefaultedList items = DefaultedList.of(); + for( int i = 0; i < arrays.size(); ++i ) + { + Ingredient ingredient = Ingredient.fromJson( arrays.get( i ) ); + if( !ingredient.isEmpty() ) items.add( ingredient ); + } + + return items; + } + + @Override + public ImpostorShapelessRecipe read( @Nonnull Identifier id, PacketByteBuf buffer ) + { + String s = buffer.readString( 32767 ); + int i = buffer.readVarInt(); + DefaultedList items = DefaultedList.ofSize( i, Ingredient.EMPTY ); + + for( int j = 0; j < items.size(); j++ ) items.set( j, Ingredient.fromPacket( buffer ) ); + ItemStack result = buffer.readItemStack(); + + return new ImpostorShapelessRecipe( id, s, result, items ); + } + + @Override + public void write( @Nonnull PacketByteBuf buffer, @Nonnull ImpostorShapelessRecipe recipe ) + { + buffer.writeString( recipe.getGroup() ); + buffer.writeVarInt( recipe.getPreviewInputs().size() ); + + for( Ingredient ingredient : recipe.getPreviewInputs() ) ingredient.write( buffer ); + buffer.writeItemStack( recipe.getOutput() ); + } + }; +} diff --git a/src/main/java/dan200/computercraft/shared/util/InventoryDelegate.java b/src/main/java/dan200/computercraft/shared/util/InventoryDelegate.java new file mode 100644 index 000000000..b100b6870 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/InventoryDelegate.java @@ -0,0 +1,119 @@ +/* + * 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.util; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; +import java.util.Set; + +/** + * Provides a delegate over inventories. + * + * This may be used both on {@link net.minecraft.tileentity.TileEntity}s to redirect the inventory to another tile, + * and by other interfaces to have inventories which change their backing store. + */ +@FunctionalInterface +public interface InventoryDelegate extends Inventory +{ + Inventory getInventory(); + + @Override + default int size() + { + return getInventory().size(); + } + + @Override + default boolean isEmpty() + { + return getInventory().isEmpty(); + } + + @Nonnull + @Override + default ItemStack getStack( int slot ) + { + return getInventory().getStack( slot ); + } + + @Nonnull + @Override + default ItemStack removeStack( int slot, int count ) + { + return getInventory().removeStack( slot, count ); + } + + @Nonnull + @Override + default ItemStack removeStack( int slot ) + { + return getInventory().removeStack( slot ); + } + + @Override + default void setStack( int slot, @Nonnull ItemStack stack ) + { + getInventory().setStack( slot, stack ); + } + + @Override + default int getMaxCountPerStack() + { + return getInventory().getMaxCountPerStack(); + } + + @Override + default void markDirty() + { + getInventory().markDirty(); + } + + @Override + default boolean canPlayerUse( @Nonnull PlayerEntity player ) + { + return getInventory().canPlayerUse( player ); + } + + @Override + default void onOpen( @Nonnull PlayerEntity player ) + { + getInventory().onOpen( player ); + } + + @Override + default void onClose( @Nonnull PlayerEntity player ) + { + getInventory().onClose( player ); + } + + @Override + default boolean isValid( int slot, @Nonnull ItemStack stack ) + { + return getInventory().isValid( slot, stack ); + } + + @Override + default void clear() + { + getInventory().clear(); + } + + @Override + default int count( @Nonnull Item stack ) + { + return getInventory().count( stack ); + } + + @Override + default boolean containsAny( @Nonnull Set set ) + { + return getInventory().containsAny( set ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java b/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java new file mode 100644 index 000000000..bb0b36761 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java @@ -0,0 +1,199 @@ +/* + * 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.util; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SidedInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +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.items.wrapper.SidedInvWrapper; +import org.apache.commons.lang3.tuple.Pair; + +import javax.annotation.Nonnull; + +public final class InventoryUtil +{ + private InventoryUtil() {} + // Methods for comparing things: + + public static boolean areItemsEqual( @Nonnull ItemStack a, @Nonnull ItemStack b ) + { + return a == b || ItemStack.areEqual( a, b ); + } + + public static boolean areItemsStackable( @Nonnull ItemStack a, @Nonnull ItemStack b ) + { + return a == b || ItemHandlerHelper.canItemStacksStack( a, b ); + } + + /** + * Determines if two items are "mostly" equivalent. Namely, they have the same item and damage, and identical + * share stacks. + * + * This is largely based on {@link net.minecraftforge.common.crafting.IngredientNBT#test(ItemStack)}. It is + * sufficient to ensure basic information (such as enchantments) are the same, while not having to worry about + * capabilities. + * + * @param a The first stack to check + * @param b The second stack to check + * @return If these items are largely the same. + */ + public static boolean areItemsSimilar( @Nonnull ItemStack a, @Nonnull ItemStack b ) + { + if( a == b ) return true; + if( a.isEmpty() ) return !b.isEmpty(); + + if( a.getItem() != b.getItem() ) return false; + + // A more expanded form of ItemStack.areShareTagsEqual, but allowing an empty tag to be equal to a + // null one. + CompoundTag shareTagA = a.getItem().getShareTag( a ); + CompoundTag shareTagB = b.getItem().getShareTag( b ); + if( shareTagA == shareTagB ) return true; + if( shareTagA == null ) return shareTagB.isEmpty(); + if( shareTagB == null ) return shareTagA.isEmpty(); + return shareTagA.equals( shareTagB ); + } + + // Methods for finding inventories: + + public static IItemHandler getInventory( World world, BlockPos pos, Direction side ) + { + // Look for tile with inventory + BlockEntity tileEntity = world.getBlockEntity( pos ); + if( tileEntity != null ) + { + LazyOptional itemHandler = tileEntity.getCapability( CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, side ); + if( itemHandler.isPresent() ) + { + return itemHandler.orElseThrow( NullPointerException::new ); + } + else if( side != null && tileEntity instanceof SidedInventory ) + { + return new SidedInvWrapper( (SidedInventory) tileEntity, side ); + } + else if( tileEntity instanceof Inventory ) + { + return new InvWrapper( (Inventory) tileEntity ); + } + } + + // Look for entity with inventory + Vec3d vecStart = new Vec3d( + pos.getX() + 0.5 + 0.6 * side.getOffsetX(), + pos.getY() + 0.5 + 0.6 * side.getOffsetY(), + pos.getZ() + 0.5 + 0.6 * side.getOffsetZ() + ); + Direction dir = side.getOpposite(); + Vec3d vecDir = new Vec3d( + dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ() + ); + Pair hit = WorldUtil.rayTraceEntities( world, vecStart, vecDir, 1.1 ); + if( hit != null ) + { + Entity entity = hit.getKey(); + if( entity instanceof Inventory ) + { + return new InvWrapper( (Inventory) entity ); + } + } + return null; + } + + // Methods for placing into inventories: + + @Nonnull + public static ItemStack storeItems( @Nonnull ItemStack itemstack, IItemHandler inventory, int begin ) + { + return storeItems( itemstack, inventory, 0, inventory.getSlots(), begin ); + } + + @Nonnull + public static ItemStack storeItems( @Nonnull ItemStack itemstack, IItemHandler inventory ) + { + return storeItems( itemstack, inventory, 0, inventory.getSlots(), 0 ); + } + + @Nonnull + public static ItemStack storeItems( @Nonnull ItemStack stack, IItemHandler inventory, int start, int range, int begin ) + { + if( stack.isEmpty() ) return ItemStack.EMPTY; + + // Inspect the slots in order and try to find empty or stackable slots + ItemStack remainder = stack.copy(); + for( int i = 0; i < range; i++ ) + { + int slot = start + (i + begin - start) % range; + if( remainder.isEmpty() ) break; + remainder = inventory.insertItem( slot, remainder, false ); + } + return areItemsEqual( stack, remainder ) ? stack : remainder; + } + + // Methods for taking out of inventories + + @Nonnull + public static ItemStack takeItems( int count, IItemHandler inventory, int begin ) + { + return takeItems( count, inventory, 0, inventory.getSlots(), begin ); + } + + @Nonnull + public static ItemStack takeItems( int count, IItemHandler inventory ) + { + return takeItems( count, inventory, 0, inventory.getSlots(), 0 ); + } + + @Nonnull + public static ItemStack takeItems( int count, IItemHandler inventory, int start, int range, int begin ) + { + // Combine multiple stacks from inventory into one if necessary + ItemStack partialStack = ItemStack.EMPTY; + for( int i = 0; i < range; i++ ) + { + int slot = start + (i + begin - start) % range; + + // If we've extracted all items, return + if( count <= 0 ) break; + + // If this doesn't slot, abort. + ItemStack stack = inventory.getStackInSlot( slot ); + if( !stack.isEmpty() && (partialStack.isEmpty() || areItemsStackable( stack, partialStack )) ) + { + ItemStack extracted = inventory.extractItem( slot, count, false ); + if( !extracted.isEmpty() ) + { + if( partialStack.isEmpty() ) + { + // If we've extracted for this first time, then limit the count to the maximum stack size. + partialStack = extracted; + count = Math.min( count, extracted.getMaxCount() ); + } + else + { + partialStack.increment( extracted.getCount() ); + } + + count -= extracted.getCount(); + } + } + + } + + return partialStack; + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/IoUtil.java b/src/main/java/dan200/computercraft/shared/util/IoUtil.java new file mode 100644 index 000000000..910a716e2 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/IoUtil.java @@ -0,0 +1,26 @@ +/* + * 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.util; + +import javax.annotation.Nullable; +import java.io.Closeable; +import java.io.IOException; + +public final class IoUtil +{ + private IoUtil() {} + + public static void closeQuietly( @Nullable Closeable closeable ) + { + try + { + if( closeable != null ) closeable.close(); + } + catch( IOException ignored ) + { + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/NBTUtil.java b/src/main/java/dan200/computercraft/shared/util/NBTUtil.java new file mode 100644 index 000000000..6c5d0ef6c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/NBTUtil.java @@ -0,0 +1,214 @@ +/* + * 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.util; + +import dan200.computercraft.ComputerCraft; +import net.minecraft.nbt.*; +import net.minecraftforge.common.util.Constants; +import org.apache.commons.codec.binary.Hex; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import static net.minecraftforge.common.util.Constants.NBT.*; + +public final class NBTUtil +{ + private NBTUtil() {} + + private static Tag toNBTTag( Object object ) + { + if( object == null ) return null; + if( object instanceof Boolean ) return ByteTag.of( (byte) ((boolean) (Boolean) object ? 1 : 0) ); + if( object instanceof Number ) return DoubleTag.of( ((Number) object).doubleValue() ); + if( object instanceof String ) return StringTag.of( object.toString() ); + if( object instanceof Map ) + { + Map m = (Map) object; + CompoundTag nbt = new CompoundTag(); + int i = 0; + for( Map.Entry entry : m.entrySet() ) + { + Tag key = toNBTTag( entry.getKey() ); + Tag value = toNBTTag( entry.getKey() ); + if( key != null && value != null ) + { + nbt.put( "k" + i, key ); + nbt.put( "v" + i, value ); + i++; + } + } + nbt.putInt( "len", m.size() ); + return nbt; + } + + return null; + } + + public static CompoundTag encodeObjects( Object[] objects ) + { + if( objects == null || objects.length <= 0 ) return null; + + CompoundTag nbt = new CompoundTag(); + nbt.putInt( "len", objects.length ); + for( int i = 0; i < objects.length; i++ ) + { + Tag child = toNBTTag( objects[i] ); + if( child != null ) nbt.put( Integer.toString( i ), child ); + } + return nbt; + } + + private static Object fromNBTTag( Tag tag ) + { + if( tag == null ) return null; + switch( tag.getType() ) + { + case TAG_BYTE: + return ((ByteTag) tag).getByte() > 0; + case TAG_DOUBLE: + return ((DoubleTag) tag).getDouble(); + default: + case TAG_STRING: + return tag.asString(); + case TAG_COMPOUND: + { + CompoundTag c = (CompoundTag) tag; + int len = c.getInt( "len" ); + Map map = new HashMap<>( len ); + for( int i = 0; i < len; i++ ) + { + Object key = fromNBTTag( c.get( "k" + i ) ); + Object value = fromNBTTag( c.get( "v" + i ) ); + if( key != null && value != null ) map.put( key, value ); + } + return map; + } + } + } + + public static Object toLua( Tag tag ) + { + if( tag == null ) return null; + + byte typeID = tag.getType(); + switch( typeID ) + { + case Constants.NBT.TAG_BYTE: + case Constants.NBT.TAG_SHORT: + case Constants.NBT.TAG_INT: + case Constants.NBT.TAG_LONG: + return ((AbstractNumberTag) tag).getLong(); + case Constants.NBT.TAG_FLOAT: + case Constants.NBT.TAG_DOUBLE: + return ((AbstractNumberTag) tag).getDouble(); + case Constants.NBT.TAG_STRING: // String + return tag.asString(); + case Constants.NBT.TAG_COMPOUND: // Compound + { + CompoundTag compound = (CompoundTag) tag; + Map map = new HashMap<>( compound.getSize() ); + for( String key : compound.getKeys() ) + { + Object value = toLua( compound.get( key ) ); + if( value != null ) map.put( key, value ); + } + return map; + } + case Constants.NBT.TAG_LIST: + { + ListTag list = (ListTag) tag; + Map map = new HashMap<>( list.size() ); + for( int i = 0; i < list.size(); i++ ) map.put( i, toLua( list.get( i ) ) ); + return map; + } + case Constants.NBT.TAG_BYTE_ARRAY: + { + byte[] array = ((ByteArrayTag) tag).getByteArray(); + Map map = new HashMap<>( array.length ); + for( int i = 0; i < array.length; i++ ) map.put( i + 1, array[i] ); + return map; + } + case Constants.NBT.TAG_INT_ARRAY: + { + int[] array = ((IntArrayTag) tag).getIntArray(); + Map map = new HashMap<>( array.length ); + for( int i = 0; i < array.length; i++ ) map.put( i + 1, array[i] ); + return map; + } + + default: + return null; + } + } + + public static Object[] decodeObjects( CompoundTag tag ) + { + int len = tag.getInt( "len" ); + if( len <= 0 ) return null; + + Object[] objects = new Object[len]; + for( int i = 0; i < len; i++ ) + { + String key = Integer.toString( i ); + if( tag.contains( key ) ) + { + objects[i] = fromNBTTag( tag.get( key ) ); + } + } + return objects; + } + + @Nullable + public static String getNBTHash( @Nullable CompoundTag tag ) + { + if( tag == null ) return null; + + try + { + MessageDigest digest = MessageDigest.getInstance( "MD5" ); + DataOutput output = new DataOutputStream( new DigestOutputStream( digest ) ); + NbtIo.write( tag, output ); + byte[] hash = digest.digest(); + return new String( Hex.encodeHex( hash ) ); + } + catch( NoSuchAlgorithmException | IOException e ) + { + ComputerCraft.log.error( "Cannot hash NBT", e ); + return null; + } + } + + private static final class DigestOutputStream extends OutputStream + { + private final MessageDigest digest; + + DigestOutputStream( MessageDigest digest ) + { + this.digest = digest; + } + + @Override + public void write( @Nonnull byte[] b, int off, int len ) + { + digest.update( b, off, len ); + } + + @Override + public void write( int b ) + { + digest.update( (byte) b ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/NullStorage.java b/src/main/java/dan200/computercraft/shared/util/NullStorage.java new file mode 100644 index 000000000..737fc0986 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/NullStorage.java @@ -0,0 +1,25 @@ +/* + * 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.util; + +import net.minecraft.nbt.Tag; +import net.minecraft.util.math.Direction; +import net.minecraftforge.common.capabilities.Capability; + +public class NullStorage implements Capability.IStorage +{ + @Override + public Tag writeNBT( Capability capability, T instance, Direction side ) + { + return null; + } + + @Override + public void readNBT( Capability capability, T instance, Direction side, Tag base ) + { + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/Palette.java b/src/main/java/dan200/computercraft/shared/util/Palette.java new file mode 100644 index 000000000..1332d9827 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/Palette.java @@ -0,0 +1,123 @@ +/* + * 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.util; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.PacketByteBuf; + +public class Palette +{ + private static final int PALETTE_SIZE = 16; + private final double[][] colours = new double[PALETTE_SIZE][3]; + + public static final Palette DEFAULT = new Palette(); + + public Palette() + { + // Get the default palette + resetColours(); + } + + public void setColour( int i, double r, double g, double b ) + { + if( i >= 0 && i < colours.length ) + { + colours[i][0] = r; + colours[i][1] = g; + colours[i][2] = b; + } + } + + public void setColour( int i, Colour colour ) + { + setColour( i, colour.getR(), colour.getG(), colour.getB() ); + } + + public double[] getColour( int i ) + { + if( i >= 0 && i < colours.length ) + { + return colours[i]; + } + return null; + } + + public void resetColour( int i ) + { + if( i >= 0 && i < colours.length ) + { + setColour( i, Colour.VALUES[i] ); + } + } + + public void resetColours() + { + for( int i = 0; i < Colour.VALUES.length; i++ ) + { + resetColour( i ); + } + } + + public static int encodeRGB8( double[] rgb ) + { + int r = (int) (rgb[0] * 255) & 0xFF; + int g = (int) (rgb[1] * 255) & 0xFF; + int b = (int) (rgb[2] * 255) & 0xFF; + + return (r << 16) | (g << 8) | b; + } + + public static double[] decodeRGB8( int rgb ) + { + return new double[] { + ((rgb >> 16) & 0xFF) / 255.0f, + ((rgb >> 8) & 0xFF) / 255.0f, + (rgb & 0xFF) / 255.0f, + }; + } + + public void write( PacketByteBuf buffer ) + { + for( double[] colour : colours ) + { + for( double channel : colour ) buffer.writeByte( (int) (channel * 0xFF) & 0xFF ); + } + } + + public void read( PacketByteBuf buffer ) + { + for( double[] colour : colours ) + { + for( int i = 0; i < colour.length; i++ ) colour[i] = (buffer.readByte() & 0xFF) / 255.0; + } + } + + public CompoundTag writeToNBT( CompoundTag nbt ) + { + int[] rgb8 = new int[colours.length]; + + for( int i = 0; i < colours.length; i++ ) + { + rgb8[i] = encodeRGB8( colours[i] ); + } + + nbt.putIntArray( "term_palette", rgb8 ); + return nbt; + } + + public void readFromNBT( CompoundTag nbt ) + { + if( !nbt.contains( "term_palette" ) ) return; + int[] rgb8 = nbt.getIntArray( "term_palette" ); + + if( rgb8.length != colours.length ) return; + + for( int i = 0; i < colours.length; i++ ) + { + colours[i] = decodeRGB8( rgb8[i] ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/RecipeUtil.java b/src/main/java/dan200/computercraft/shared/util/RecipeUtil.java new file mode 100644 index 000000000..e685be2f6 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/RecipeUtil.java @@ -0,0 +1,118 @@ +/* + * 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.util; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import java.util.Map; +import java.util.Set; +import net.minecraft.recipe.Ingredient; +import net.minecraft.util.JsonHelper; +import net.minecraft.util.collection.DefaultedList; + +// TODO: Replace some things with Forge?? + +public final class RecipeUtil +{ + private RecipeUtil() {} + + public static class ShapedTemplate + { + public final int width; + public final int height; + public final DefaultedList ingredients; + + public ShapedTemplate( int width, int height, DefaultedList ingredients ) + { + this.width = width; + this.height = height; + this.ingredients = ingredients; + } + } + + public static ShapedTemplate getTemplate( JsonObject json ) + { + Map ingMap = Maps.newHashMap(); + for( Map.Entry entry : JsonHelper.getObject( json, "key" ).entrySet() ) + { + if( entry.getKey().length() != 1 ) + { + throw new JsonSyntaxException( "Invalid key entry: '" + entry.getKey() + "' is an invalid symbol (must be 1 character only)." ); + } + if( " ".equals( entry.getKey() ) ) + { + throw new JsonSyntaxException( "Invalid key entry: ' ' is a reserved symbol." ); + } + + ingMap.put( entry.getKey().charAt( 0 ), Ingredient.fromJson( entry.getValue() ) ); + } + + ingMap.put( ' ', Ingredient.EMPTY ); + + JsonArray patternJ = JsonHelper.getArray( json, "pattern" ); + + if( patternJ.size() == 0 ) + { + throw new JsonSyntaxException( "Invalid pattern: empty pattern not allowed" ); + } + + String[] pattern = new String[patternJ.size()]; + for( int x = 0; x < pattern.length; x++ ) + { + String line = JsonHelper.asString( patternJ.get( x ), "pattern[" + x + "]" ); + if( x > 0 && pattern[0].length() != line.length() ) + { + throw new JsonSyntaxException( "Invalid pattern: each row must be the same width" ); + } + pattern[x] = line; + } + + int width = pattern[0].length(); + int height = pattern.length; + DefaultedList ingredients = DefaultedList.ofSize( width * height, Ingredient.EMPTY ); + + Set missingKeys = Sets.newHashSet( ingMap.keySet() ); + missingKeys.remove( ' ' ); + + int i = 0; + for( String line : pattern ) + { + for( char chr : line.toCharArray() ) + { + Ingredient ing = ingMap.get( chr ); + if( ing == null ) + { + throw new JsonSyntaxException( "Pattern references symbol '" + chr + "' but it's not defined in the key" ); + } + ingredients.set( i++, ing ); + missingKeys.remove( chr ); + } + } + + if( !missingKeys.isEmpty() ) + { + throw new JsonSyntaxException( "Key defines symbols that aren't used in pattern: " + missingKeys ); + } + + return new ShapedTemplate( width, height, ingredients ); + } + + public static ComputerFamily getFamily( JsonObject json, String name ) + { + String familyName = JsonHelper.getString( json, name ); + for( ComputerFamily family : ComputerFamily.values() ) + { + if( family.name().equalsIgnoreCase( familyName ) ) return family; + } + + throw new JsonSyntaxException( "Unknown computer family '" + familyName + "' for field " + name ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/RecordUtil.java b/src/main/java/dan200/computercraft/shared/util/RecordUtil.java new file mode 100644 index 000000000..116c45c4b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/RecordUtil.java @@ -0,0 +1,25 @@ +/* + * 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.util; + +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.NetworkMessage; +import dan200.computercraft.shared.network.client.PlayRecordClientMessage; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +public final class RecordUtil +{ + private RecordUtil() {} + + public static void playRecord( SoundEvent record, String recordInfo, World world, BlockPos pos ) + { + NetworkMessage packet = record != null ? new PlayRecordClientMessage( pos, record, recordInfo ) : new PlayRecordClientMessage( pos ); + NetworkHandler.sendToAllAround( packet, world, Vec3d.ofCenter( pos ), 64 ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/RedstoneUtil.java b/src/main/java/dan200/computercraft/shared/util/RedstoneUtil.java new file mode 100644 index 000000000..26258c43d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/RedstoneUtil.java @@ -0,0 +1,28 @@ +/* + * 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.util; + +import net.minecraft.block.BlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraftforge.event.ForgeEventFactory; + +import java.util.EnumSet; + +public final class RedstoneUtil +{ + public static void propagateRedstoneOutput( World world, BlockPos pos, Direction side ) + { + // Propagate ordinary output. See BlockRedstoneDiode.notifyNeighbors + BlockState block = world.getBlockState( pos ); + if( ForgeEventFactory.onNeighborNotify( world, pos, block, EnumSet.of( side ), false ).isCanceled() ) return; + + BlockPos neighbourPos = pos.offset( side ); + world.updateNeighbor( neighbourPos, block.getBlock(), pos ); + world.updateNeighborsExcept( neighbourPos, block.getBlock(), side.getOpposite() ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/ServiceUtil.java b/src/main/java/dan200/computercraft/shared/util/ServiceUtil.java new file mode 100644 index 000000000..6ff2d898e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/ServiceUtil.java @@ -0,0 +1,63 @@ +/* + * 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.util; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +import net.minecraftforge.fml.ModList; +import org.objectweb.asm.Type; + +import java.util.List; +import java.util.ServiceLoader; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public final class ServiceUtil +{ + private static final Type AUTO_SERVICE = Type.getType( "Lcom/google/auto/service/AutoService;" ); + + private ServiceUtil() + { + } + + public static Stream loadServices( Class target ) + { + return StreamSupport.stream( ServiceLoader.load( target, ServiceUtil.class.getClassLoader() ).spliterator(), false ); + } + + public static Stream loadServicesForge( Class target ) + { + Type type = Type.getType( target ); + ClassLoader loader = ComputerCraftAPI.class.getClassLoader(); + return ModList.get().getAllScanData().stream() + .flatMap( x -> x.getAnnotations().stream() ) + .filter( x -> x.getAnnotationType().equals( AUTO_SERVICE ) ) + .filter( x -> { + Object value = x.getAnnotationData().get( "value" ); + return value instanceof List && ((List) value).contains( type ); + } ) + .flatMap( x -> { + try + { + Class klass = loader.loadClass( x.getClassType().getClassName() ); + if( !target.isAssignableFrom( klass ) ) + { + ComputerCraft.log.error( "{} is not a subtype of {}", x.getClassType().getClassName(), target.getName() ); + return Stream.empty(); + } + + Class casted = klass.asSubclass( target ); + return Stream.of( casted.newInstance() ); + } + catch( ReflectiveOperationException e ) + { + ComputerCraft.log.error( "Cannot load {}", x.getClassType(), e ); + return Stream.empty(); + } + } ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/SidedCaps.java b/src/main/java/dan200/computercraft/shared/util/SidedCaps.java new file mode 100644 index 000000000..25875c0cd --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/SidedCaps.java @@ -0,0 +1,68 @@ +/* + * 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.util; + +import net.minecraft.util.math.Direction; +import net.minecraftforge.common.util.LazyOptional; + +import javax.annotation.Nullable; +import java.util.function.Function; + +/** + * Provides a constant (but invalidate-able) capability for each side. + * + * @param The type of the produced capability. + */ +public final class SidedCaps +{ + private final Function factory; + private final boolean allowNull; + private T[] values; + private LazyOptional[] caps; + + private SidedCaps( Function factory, boolean allowNull ) + { + this.factory = factory; + this.allowNull = allowNull; + } + + public static SidedCaps ofNonNull( Function factory ) + { + return new SidedCaps<>( factory, false ); + } + + public static SidedCaps ofNullable( Function factory ) + { + return new SidedCaps<>( factory, true ); + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + public LazyOptional get( @Nullable Direction direction ) + { + if( direction == null && !allowNull ) return LazyOptional.empty(); + int index = direction == null ? 6 : direction.ordinal(); + + LazyOptional[] caps = this.caps; + if( caps == null ) + { + caps = this.caps = new LazyOptional[allowNull ? 7 : 6]; + values = (T[]) new Object[caps.length]; + } + + LazyOptional cap = caps[index]; + return cap == null ? caps[index] = LazyOptional.of( () -> { + T[] values = this.values; + T value = values[index]; + return value == null ? values[index] = factory.apply( direction ) : value; + } ) : cap; + } + + public void invalidate() + { + if( caps != null ) CapabilityUtil.invalidate( caps ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/SingleIntArray.java b/src/main/java/dan200/computercraft/shared/util/SingleIntArray.java new file mode 100644 index 000000000..fe0098272 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/SingleIntArray.java @@ -0,0 +1,31 @@ +/* + * 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.util; + +import net.minecraft.screen.PropertyDelegate; + +@FunctionalInterface +public interface SingleIntArray extends PropertyDelegate +{ + int get(); + + @Override + default int get( int property ) + { + return property == 0 ? get() : 0; + } + + @Override + default void set( int property, int value ) + { + } + + @Override + default int size() + { + return 1; + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/StringUtil.java b/src/main/java/dan200/computercraft/shared/util/StringUtil.java new file mode 100644 index 000000000..64a7f1e2d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/StringUtil.java @@ -0,0 +1,40 @@ +/* + * 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.util; + +import javax.annotation.Nullable; + +public final class StringUtil +{ + private StringUtil() {} + + public static String normaliseLabel( String label ) + { + if( label == null ) return null; + + int length = Math.min( 32, label.length() ); + StringBuilder builder = new StringBuilder( length ); + for( int i = 0; i < length; i++ ) + { + char c = label.charAt( i ); + if( (c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255) ) + { + builder.append( c ); + } + else + { + builder.append( '?' ); + } + } + + return builder.toString(); + } + + public static String toString( @Nullable Object value ) + { + return value == null ? "" : value.toString(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/ThreadUtils.java b/src/main/java/dan200/computercraft/shared/util/ThreadUtils.java new file mode 100644 index 000000000..f03bb4d8e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/ThreadUtils.java @@ -0,0 +1,79 @@ +/* + * 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.util; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import dan200.computercraft.ComputerCraft; + +import java.util.concurrent.ThreadFactory; + +/** + * Provides some utilities to create thread groups. + */ +public final class ThreadUtils +{ + private static final ThreadGroup baseGroup = new ThreadGroup( "ComputerCraft" ); + + private ThreadUtils() + { + } + + /** + * Get the base thread group, that all off-thread ComputerCraft activities are run on. + * + * @return The ComputerCraft group. + */ + public static ThreadGroup group() + { + return baseGroup; + } + + /** + * Construct a group under ComputerCraft's shared group. + * + * @param name The group's name. This will be prefixed with "ComputerCraft-". + * @return The constructed thread group. + */ + public static ThreadGroup group( String name ) + { + return new ThreadGroup( baseGroup, baseGroup.getName() + "-" + name ); + } + + /** + * Create a new {@link ThreadFactoryBuilder}, which constructs threads under a group of the given {@code name}. + * + * Each thread will be of the format {@code ComputerCraft--}, and belong to a group + * called {@code ComputerCraft-} (which in turn will be a child group of the main {@code ComputerCraft} group. + * + * @param name The name for the thread group and child threads. + * @return The constructed thread factory builder, which may be extended with other properties. + * @see #factory(String) + */ + public static ThreadFactoryBuilder builder( String name ) + { + ThreadGroup group = group( name ); + return new ThreadFactoryBuilder() + .setDaemon( true ) + .setNameFormat( group.getName().replace( "%", "%%" ) + "-%d" ) + .setUncaughtExceptionHandler( ( t, e ) -> ComputerCraft.log.error( "Exception in thread " + t.getName(), e ) ) + .setThreadFactory( x -> new Thread( group, x ) ); + } + + /** + * Create a new {@link ThreadFactory}, which constructs threads under a group of the given {@code name}. + * + * Each thread will be of the format {@code ComputerCraft--}, and belong to a group + * called {@code ComputerCraft-} (which in turn will be a child group of the main {@code ComputerCraft} group. + * + * @param name The name for the thread group and child threads. + * @return The constructed thread factory. + * @see #builder(String) + */ + public static ThreadFactory factory( String name ) + { + return builder( name ).build(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/TickScheduler.java b/src/main/java/dan200/computercraft/shared/util/TickScheduler.java new file mode 100644 index 000000000..27ae25e47 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/TickScheduler.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.util; + +import com.google.common.collect.MapMaker; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.common.TileGeneric; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; + +/** + * A thread-safe version of {@link ITickList#scheduleTick(BlockPos, Object, int)}. + * + * We use this when modems and other peripherals change a block in a different thread. + */ +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID ) +public final class TickScheduler +{ + private TickScheduler() + { + } + + private static final Set toTick = Collections.newSetFromMap( + new MapMaker() + .weakKeys() + .makeMap() + ); + + public static void schedule( TileGeneric tile ) + { + World world = tile.getWorld(); + if( world != null && !world.isClient ) toTick.add( tile ); + } + + @SubscribeEvent + public static void tick( TickEvent.ServerTickEvent event ) + { + if( event.phase != TickEvent.Phase.START ) return; + + Iterator iterator = toTick.iterator(); + while( iterator.hasNext() ) + { + BlockEntity tile = iterator.next(); + iterator.remove(); + + World world = tile.getWorld(); + BlockPos pos = tile.getPos(); + + if( world != null && pos != null && world.isAreaLoaded( pos, 0 ) && world.getBlockEntity( pos ) == tile ) + { + world.getBlockTickScheduler().schedule( pos, tile.getCachedState().getBlock(), 0 ); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/ValidatingSlot.java b/src/main/java/dan200/computercraft/shared/util/ValidatingSlot.java new file mode 100644 index 000000000..470a8f17d --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/ValidatingSlot.java @@ -0,0 +1,25 @@ +/* + * 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.util; + +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import javax.annotation.Nonnull; + +public class ValidatingSlot extends Slot +{ + public ValidatingSlot( Inventory inventoryIn, int index, int xPosition, int yPosition ) + { + super( inventoryIn, index, xPosition, yPosition ); + } + + @Override + public boolean canInsert( @Nonnull ItemStack stack ) + { + return true; // inventory.isItemValidForSlot( slotNumber, stack ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/WaterloggableHelpers.java b/src/main/java/dan200/computercraft/shared/util/WaterloggableHelpers.java new file mode 100644 index 000000000..c4a99ac80 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/WaterloggableHelpers.java @@ -0,0 +1,60 @@ +/* + * 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.util; + +import net.minecraft.block.BlockState; +import net.minecraft.fluid.FluidState; +import net.minecraft.fluid.Fluids; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.state.property.BooleanProperty; +import net.minecraft.state.property.Properties; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldAccess; + +/** + * Represents a block which can be filled with water + * + * I'm fairly sure this exists on 1.14, but it's a useful convenience wrapper to have on 1.13. + */ +public final class WaterloggableHelpers +{ + public static final BooleanProperty WATERLOGGED = Properties.WATERLOGGED; + + private WaterloggableHelpers() + { + } + + /** + * Call from {@link net.minecraft.block.Block#getFluidState(BlockState)}. + * + * @param state The current state + * @return This waterlogged block's current fluid + */ + public static FluidState getWaterloggedFluidState( BlockState state ) + { + return state.get( WATERLOGGED ) ? Fluids.WATER.getStill( false ) : Fluids.EMPTY.getDefaultState(); + } + + /** + * Call from {@link net.minecraft.block.Block#updatePostPlacement(BlockState, Direction, BlockState, IWorld, BlockPos, BlockPos)}. + * + * @param state The current state + * @param world The position of this block + * @param pos The world this block exists in + */ + public static void updateWaterloggedPostPlacement( BlockState state, WorldAccess world, BlockPos pos ) + { + if( state.get( WATERLOGGED ) ) + { + world.getFluidTickScheduler().schedule( pos, Fluids.WATER, Fluids.WATER.getTickRate( world ) ); + } + } + + public static boolean getWaterloggedStateForPlacement( ItemPlacementContext context ) + { + return context.getWorld().getFluidState( context.getBlockPos() ).getFluid() == Fluids.WATER; + } +} diff --git a/src/main/java/dan200/computercraft/shared/util/WorldUtil.java b/src/main/java/dan200/computercraft/shared/util/WorldUtil.java new file mode 100644 index 000000000..d26b9ec4b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/util/WorldUtil.java @@ -0,0 +1,196 @@ +/* + * 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.util; + +import com.google.common.base.Predicate; +import com.google.common.collect.MapMaker; +import net.minecraft.entity.*; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.RaycastContext; +import net.minecraft.world.World; +import net.minecraftforge.common.ForgeMod; +import org.apache.commons.lang3.tuple.Pair; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Map; + +public final class WorldUtil +{ + @SuppressWarnings( "Guava" ) + private static final Predicate CAN_COLLIDE = x -> x != null && x.isAlive() && x.collides(); + + private static final Map entityCache = new MapMaker().weakKeys().weakValues().makeMap(); + + private static synchronized Entity getEntity( World world ) + { + // TODO: It'd be nice if we could avoid this. Maybe always use the turtle player (if it's available). + Entity entity = entityCache.get( world ); + if( entity != null ) return entity; + + entity = new ItemEntity( EntityType.ITEM, world ) + { + @Nonnull + @Override + public EntityDimensions getDimensions( @Nonnull EntityPose pose ) + { + return EntityDimensions.fixed( 0, 0 ); + } + }; + + entity.noClip = true; + entity.calculateDimensions(); + entityCache.put( world, entity ); + return entity; + } + + public static boolean isLiquidBlock( World world, BlockPos pos ) + { + if( !World.method_24794( pos ) ) return false; + return world.getBlockState( pos ).getMaterial().isLiquid(); + } + + public static boolean isVecInside( VoxelShape shape, Vec3d vec ) + { + if( shape.isEmpty() ) return false; + // AxisAlignedBB.contains, but without strict inequalities. + Box bb = shape.getBoundingBox(); + return vec.x >= bb.minX && vec.x <= bb.maxX && vec.y >= bb.minY && vec.y <= bb.maxY && vec.z >= bb.minZ && vec.z <= bb.maxZ; + } + + public static Pair rayTraceEntities( World world, Vec3d vecStart, Vec3d vecDir, double distance ) + { + Vec3d vecEnd = vecStart.add( vecDir.x * distance, vecDir.y * distance, vecDir.z * distance ); + + // Raycast for blocks + Entity collisionEntity = getEntity( world ); + collisionEntity.updatePosition( vecStart.x, vecStart.y, vecStart.z ); + RaycastContext context = new RaycastContext( vecStart, vecEnd, RaycastContext.ShapeType.COLLIDER, RaycastContext.FluidHandling.NONE, collisionEntity ); + HitResult result = world.raycast( context ); + if( result != null && result.getType() == HitResult.Type.BLOCK ) + { + distance = vecStart.distanceTo( result.getPos() ); + vecEnd = vecStart.add( vecDir.x * distance, vecDir.y * distance, vecDir.z * distance ); + } + + // Check for entities + float xStretch = Math.abs( vecDir.x ) > 0.25f ? 0.0f : 1.0f; + float yStretch = Math.abs( vecDir.y ) > 0.25f ? 0.0f : 1.0f; + float zStretch = Math.abs( vecDir.z ) > 0.25f ? 0.0f : 1.0f; + Box bigBox = new Box( + Math.min( vecStart.x, vecEnd.x ) - 0.375f * xStretch, + Math.min( vecStart.y, vecEnd.y ) - 0.375f * yStretch, + Math.min( vecStart.z, vecEnd.z ) - 0.375f * zStretch, + Math.max( vecStart.x, vecEnd.x ) + 0.375f * xStretch, + Math.max( vecStart.y, vecEnd.y ) + 0.375f * yStretch, + Math.max( vecStart.z, vecEnd.z ) + 0.375f * zStretch + ); + + Entity closest = null; + double closestDist = 99.0; + List list = world.getEntitiesByClass( Entity.class, bigBox, CAN_COLLIDE ); + for( Entity entity : list ) + { + Box littleBox = entity.getBoundingBox(); + if( littleBox.contains( vecStart ) ) + { + closest = entity; + closestDist = 0.0f; + continue; + } + + Vec3d littleBoxResult = littleBox.raycast( vecStart, vecEnd ).orElse( null ); + if( littleBoxResult != null ) + { + double dist = vecStart.distanceTo( littleBoxResult ); + if( closest == null || dist <= closestDist ) + { + closest = entity; + closestDist = dist; + } + } + else if( littleBox.intersects( bigBox ) ) + { + if( closest == null ) + { + closest = entity; + closestDist = distance; + } + } + } + if( closest != null && closestDist <= distance ) + { + Vec3d closestPos = vecStart.add( vecDir.x * closestDist, vecDir.y * closestDist, vecDir.z * closestDist ); + return Pair.of( closest, closestPos ); + } + return null; + } + + public static Vec3d getRayStart( LivingEntity entity ) + { + return entity.getCameraPosVec( 1 ); + } + + public static Vec3d getRayEnd( PlayerEntity player ) + { + double reach = player.getAttributeInstance( ForgeMod.REACH_DISTANCE.get() ).getValue(); + Vec3d look = player.getRotationVector(); + return getRayStart( player ).add( look.x * reach, look.y * reach, look.z * reach ); + } + + public static void dropItemStack( @Nonnull ItemStack stack, World world, BlockPos pos ) + { + dropItemStack( stack, world, pos, null ); + } + + public static void dropItemStack( @Nonnull ItemStack stack, World world, BlockPos pos, Direction direction ) + { + double xDir; + double yDir; + double zDir; + if( direction != null ) + { + xDir = direction.getOffsetX(); + yDir = direction.getOffsetY(); + zDir = direction.getOffsetZ(); + } + else + { + xDir = 0.0; + yDir = 0.0; + zDir = 0.0; + } + + double xPos = pos.getX() + 0.5 + xDir * 0.4; + double yPos = pos.getY() + 0.5 + yDir * 0.4; + double zPos = pos.getZ() + 0.5 + zDir * 0.4; + dropItemStack( stack, world, new Vec3d( xPos, yPos, zPos ), xDir, yDir, zDir ); + } + + public static void dropItemStack( @Nonnull ItemStack stack, World world, Vec3d pos ) + { + dropItemStack( stack, world, pos, 0.0, 0.0, 0.0 ); + } + + public static void dropItemStack( @Nonnull ItemStack stack, World world, Vec3d pos, double xDir, double yDir, double zDir ) + { + ItemEntity item = new ItemEntity( world, pos.x, pos.y, pos.z, stack.copy() ); + item.setVelocity( + xDir * 0.7 + world.getRandom().nextFloat() * 0.2 - 0.1, + yDir * 0.7 + world.getRandom().nextFloat() * 0.2 - 0.1, + zDir * 0.7 + world.getRandom().nextFloat() * 0.2 - 0.1 + ); + item.setToDefaultPickupDelay(); + world.spawnEntity( item ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/wired/InvariantChecker.java b/src/main/java/dan200/computercraft/shared/wired/InvariantChecker.java new file mode 100644 index 000000000..40084252f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/wired/InvariantChecker.java @@ -0,0 +1,53 @@ +/* + * 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.wired; + +import dan200.computercraft.ComputerCraft; + +/** + * Verifies certain elements of a network are "well formed". + * + * This adds substantial overhead to network modification, and so should only be enabled + * in a development environment. + */ +public final class InvariantChecker +{ + private static final boolean ENABLED = false; + + private InvariantChecker() {} + + public static void checkNode( WiredNode node ) + { + if( !ENABLED ) return; + + WiredNetwork network = node.network; + if( network == null ) + { + ComputerCraft.log.error( "Node's network is null", new Exception() ); + return; + } + + if( network.nodes == null || !network.nodes.contains( node ) ) + { + ComputerCraft.log.error( "Node's network does not contain node", new Exception() ); + } + + for( WiredNode neighbour : node.neighbours ) + { + if( !neighbour.neighbours.contains( node ) ) + { + ComputerCraft.log.error( "Neighbour is missing node", new Exception() ); + } + } + } + + public static void checkNetwork( WiredNetwork network ) + { + if( !ENABLED ) return; + + for( WiredNode node : network.nodes ) checkNode( node ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/wired/WiredNetwork.java b/src/main/java/dan200/computercraft/shared/wired/WiredNetwork.java new file mode 100644 index 000000000..c66c7e19b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/wired/WiredNetwork.java @@ -0,0 +1,464 @@ +/* + * 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.wired; + +import com.google.common.collect.ImmutableMap; +import dan200.computercraft.api.network.Packet; +import dan200.computercraft.api.network.wired.IWiredNetwork; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import java.util.*; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public final class WiredNetwork implements IWiredNetwork +{ + final ReadWriteLock lock = new ReentrantReadWriteLock(); + HashSet nodes; + private HashMap peripherals = new HashMap<>(); + + WiredNetwork( WiredNode node ) + { + nodes = new HashSet<>( 1 ); + nodes.add( node ); + } + + private WiredNetwork( HashSet nodes ) + { + this.nodes = nodes; + } + + @Override + public boolean connect( @Nonnull IWiredNode nodeU, @Nonnull IWiredNode nodeV ) + { + WiredNode wiredU = checkNode( nodeU ); + WiredNode wiredV = checkNode( nodeV ); + if( nodeU == nodeV ) throw new IllegalArgumentException( "Cannot add a connection to oneself." ); + + lock.writeLock().lock(); + try + { + if( nodes == null ) throw new IllegalStateException( "Cannot add a connection to an empty network." ); + + boolean hasU = wiredU.network == this; + boolean hasV = wiredV.network == this; + if( !hasU && !hasV ) throw new IllegalArgumentException( "Neither node is in the network." ); + + // We're going to assimilate a node. Copy across all edges and vertices. + if( !hasU || !hasV ) + { + WiredNetwork other = hasU ? wiredV.network : wiredU.network; + other.lock.writeLock().lock(); + try + { + // Cache several properties for iterating over later + Map otherPeripherals = other.peripherals; + Map thisPeripherals = otherPeripherals.isEmpty() ? peripherals : new HashMap<>( peripherals ); + + Collection thisNodes = otherPeripherals.isEmpty() ? nodes : new ArrayList<>( nodes ); + Collection otherNodes = other.nodes; + + // Move all nodes across into this network, destroying the original nodes. + nodes.addAll( otherNodes ); + for( WiredNode node : otherNodes ) node.network = this; + other.nodes = null; + + // Move all peripherals across, + other.peripherals = null; + peripherals.putAll( otherPeripherals ); + + if( !thisPeripherals.isEmpty() ) + { + WiredNetworkChange.added( thisPeripherals ).broadcast( otherNodes ); + } + + if( !otherPeripherals.isEmpty() ) + { + WiredNetworkChange.added( otherPeripherals ).broadcast( thisNodes ); + } + } + finally + { + other.lock.writeLock().unlock(); + } + } + + boolean added = wiredU.neighbours.add( wiredV ); + if( added ) wiredV.neighbours.add( wiredU ); + + InvariantChecker.checkNetwork( this ); + InvariantChecker.checkNode( wiredU ); + InvariantChecker.checkNode( wiredV ); + + return added; + } + finally + { + lock.writeLock().unlock(); + } + } + + @Override + public boolean disconnect( @Nonnull IWiredNode nodeU, @Nonnull IWiredNode nodeV ) + { + WiredNode wiredU = checkNode( nodeU ); + WiredNode wiredV = checkNode( nodeV ); + if( nodeU == nodeV ) throw new IllegalArgumentException( "Cannot remove a connection to oneself." ); + + lock.writeLock().lock(); + try + { + boolean hasU = wiredU.network == this; + boolean hasV = wiredV.network == this; + if( !hasU || !hasV ) throw new IllegalArgumentException( "One node is not in the network." ); + + // If there was no connection to remove then split. + if( !wiredU.neighbours.remove( wiredV ) ) return false; + wiredV.neighbours.remove( wiredU ); + + // Determine if there is still some connection from u to v. + // Note this is an inlining of reachableNodes which short-circuits + // if all nodes are reachable. + Queue enqueued = new ArrayDeque<>(); + HashSet reachableU = new HashSet<>(); + + reachableU.add( wiredU ); + enqueued.add( wiredU ); + + while( !enqueued.isEmpty() ) + { + WiredNode node = enqueued.remove(); + for( WiredNode neighbour : node.neighbours ) + { + // If we can reach wiredV from wiredU then abort. + if( neighbour == wiredV ) return true; + + // Otherwise attempt to enqueue this neighbour as well. + if( reachableU.add( neighbour ) ) enqueued.add( neighbour ); + } + } + + // Create a new network with all U-reachable nodes/edges and remove them + // from the existing graph. + WiredNetwork networkU = new WiredNetwork( reachableU ); + networkU.lock.writeLock().lock(); + try + { + // Remove nodes from this network + nodes.removeAll( reachableU ); + + // Set network and transfer peripherals + for( WiredNode node : reachableU ) + { + node.network = networkU; + networkU.peripherals.putAll( node.peripherals ); + peripherals.keySet().removeAll( node.peripherals.keySet() ); + } + + // Broadcast changes + if( !peripherals.isEmpty() ) WiredNetworkChange.removed( peripherals ).broadcast( networkU.nodes ); + if( !networkU.peripherals.isEmpty() ) + { + WiredNetworkChange.removed( networkU.peripherals ).broadcast( nodes ); + } + + InvariantChecker.checkNetwork( this ); + InvariantChecker.checkNetwork( networkU ); + InvariantChecker.checkNode( wiredU ); + InvariantChecker.checkNode( wiredV ); + + return true; + } + finally + { + networkU.lock.writeLock().unlock(); + } + } + finally + { + lock.writeLock().unlock(); + } + } + + @Override + public boolean remove( @Nonnull IWiredNode node ) + { + WiredNode wired = checkNode( node ); + + lock.writeLock().lock(); + try + { + // If we're the empty graph then just abort: nodes must have _some_ network. + if( nodes == null ) return false; + if( nodes.size() <= 1 ) return false; + if( wired.network != this ) return false; + + HashSet neighbours = wired.neighbours; + + // Remove this node and move into a separate network. + nodes.remove( wired ); + for( WiredNode neighbour : neighbours ) neighbour.neighbours.remove( wired ); + + WiredNetwork wiredNetwork = new WiredNetwork( wired ); + + // If we're a leaf node in the graph (only one neighbour) then we don't need to + // check for network splitting + if( neighbours.size() == 1 ) + { + // Broadcast our simple peripheral changes + removeSingleNode( wired, wiredNetwork ); + InvariantChecker.checkNode( wired ); + InvariantChecker.checkNetwork( wiredNetwork ); + return true; + } + + HashSet reachable = reachableNodes( neighbours.iterator().next() ); + + // If all nodes are reachable then exit. + if( reachable.size() == nodes.size() ) + { + // Broadcast our simple peripheral changes + removeSingleNode( wired, wiredNetwork ); + InvariantChecker.checkNode( wired ); + InvariantChecker.checkNetwork( wiredNetwork ); + return true; + } + + // A split may cause 2..neighbours.size() separate networks, so we + // iterate through our neighbour list, generating child networks. + neighbours.removeAll( reachable ); + ArrayList maximals = new ArrayList<>( neighbours.size() + 1 ); + maximals.add( wiredNetwork ); + maximals.add( new WiredNetwork( reachable ) ); + + while( !neighbours.isEmpty() ) + { + reachable = reachableNodes( neighbours.iterator().next() ); + neighbours.removeAll( reachable ); + maximals.add( new WiredNetwork( reachable ) ); + } + + for( WiredNetwork network : maximals ) network.lock.writeLock().lock(); + + try + { + // We special case the original node: detaching all peripherals when needed. + wired.network = wiredNetwork; + wired.peripherals = Collections.emptyMap(); + + // Ensure every network is finalised + for( WiredNetwork network : maximals ) + { + for( WiredNode child : network.nodes ) + { + child.network = network; + network.peripherals.putAll( child.peripherals ); + } + } + + for( WiredNetwork network : maximals ) InvariantChecker.checkNetwork( network ); + InvariantChecker.checkNode( wired ); + + // Then broadcast network changes once all nodes are finalised + for( WiredNetwork network : maximals ) + { + WiredNetworkChange.changeOf( peripherals, network.peripherals ).broadcast( network.nodes ); + } + } + finally + { + for( WiredNetwork network : maximals ) network.lock.writeLock().unlock(); + } + + nodes.clear(); + peripherals.clear(); + + return true; + } + finally + { + lock.writeLock().unlock(); + } + } + + @Override + public void updatePeripherals( @Nonnull IWiredNode node, @Nonnull Map newPeripherals ) + { + WiredNode wired = checkNode( node ); + Objects.requireNonNull( peripherals, "peripherals cannot be null" ); + + lock.writeLock().lock(); + try + { + if( wired.network != this ) throw new IllegalStateException( "Node is not on this network" ); + + Map oldPeripherals = wired.peripherals; + WiredNetworkChange change = WiredNetworkChange.changeOf( oldPeripherals, newPeripherals ); + if( change.isEmpty() ) return; + + wired.peripherals = ImmutableMap.copyOf( newPeripherals ); + + // Detach the old peripherals then remove them. + peripherals.keySet().removeAll( change.peripheralsRemoved().keySet() ); + + // Add the new peripherals and attach them + peripherals.putAll( change.peripheralsAdded() ); + + change.broadcast( nodes ); + } + finally + { + lock.writeLock().unlock(); + } + } + + static void transmitPacket( WiredNode start, Packet packet, double range, boolean interdimensional ) + { + Map points = new HashMap<>(); + TreeSet transmitTo = new TreeSet<>(); + + { + TransmitPoint startEntry = start.element.getWorld() != packet.getSender().getWorld() + ? new TransmitPoint( start, Double.POSITIVE_INFINITY, true ) + : new TransmitPoint( start, start.element.getPosition().distanceTo( packet.getSender().getPosition() ), false ); + points.put( start, startEntry ); + transmitTo.add( startEntry ); + } + + { + TransmitPoint point; + while( (point = transmitTo.pollFirst()) != null ) + { + World world = point.node.element.getWorld(); + Vec3d position = point.node.element.getPosition(); + for( WiredNode neighbour : point.node.neighbours ) + { + TransmitPoint neighbourPoint = points.get( neighbour ); + + boolean newInterdimensional; + double newDistance; + if( world != neighbour.element.getWorld() ) + { + newInterdimensional = true; + newDistance = Double.POSITIVE_INFINITY; + } + else + { + newInterdimensional = false; + newDistance = point.distance + position.distanceTo( neighbour.element.getPosition() ); + } + + if( neighbourPoint == null ) + { + TransmitPoint nextPoint = new TransmitPoint( neighbour, newDistance, newInterdimensional ); + points.put( neighbour, nextPoint ); + transmitTo.add( nextPoint ); + } + else if( newDistance < neighbourPoint.distance ) + { + transmitTo.remove( neighbourPoint ); + neighbourPoint.distance = newDistance; + neighbourPoint.interdimensional = newInterdimensional; + transmitTo.add( neighbourPoint ); + } + } + } + } + + for( TransmitPoint point : points.values() ) + { + point.node.tryTransmit( packet, point.distance, point.interdimensional, range, interdimensional ); + } + } + + private void removeSingleNode( WiredNode wired, WiredNetwork wiredNetwork ) + { + wiredNetwork.lock.writeLock().lock(); + try + { + // Cache all the old nodes. + Map wiredPeripherals = new HashMap<>( wired.peripherals ); + + // Setup the new node's network + // Detach the old peripherals then remove them from the old network + wired.network = wiredNetwork; + wired.neighbours.clear(); + wired.peripherals = Collections.emptyMap(); + + // Broadcast the change + if( !peripherals.isEmpty() ) WiredNetworkChange.removed( peripherals ).broadcast( wired ); + + // Now remove all peripherals from this network and broadcast the change. + peripherals.keySet().removeAll( wiredPeripherals.keySet() ); + if( !wiredPeripherals.isEmpty() ) WiredNetworkChange.removed( wiredPeripherals ).broadcast( nodes ); + + } + finally + { + wiredNetwork.lock.writeLock().unlock(); + } + } + + private static class TransmitPoint implements Comparable + { + final WiredNode node; + double distance; + boolean interdimensional; + + TransmitPoint( WiredNode node, double distance, boolean interdimensional ) + { + this.node = node; + this.distance = distance; + this.interdimensional = interdimensional; + } + + @Override + public int compareTo( @Nonnull TransmitPoint o ) + { + // Objects with the same distance are not the same object, so we must add an additional layer of ordering. + return distance == o.distance + ? Integer.compare( node.hashCode(), o.node.hashCode() ) + : Double.compare( distance, o.distance ); + } + } + + private static WiredNode checkNode( IWiredNode node ) + { + if( node instanceof WiredNode ) + { + return (WiredNode) node; + } + else + { + throw new IllegalArgumentException( "Unknown implementation of IWiredNode: " + node ); + } + } + + private static HashSet reachableNodes( WiredNode start ) + { + Queue enqueued = new ArrayDeque<>(); + HashSet reachable = new HashSet<>(); + + reachable.add( start ); + enqueued.add( start ); + + WiredNode node; + while( (node = enqueued.poll()) != null ) + { + for( WiredNode neighbour : node.neighbours ) + { + // Otherwise attempt to enqueue this neighbour as well. + if( reachable.add( neighbour ) ) enqueued.add( neighbour ); + } + } + + return reachable; + } +} diff --git a/src/main/java/dan200/computercraft/shared/wired/WiredNetworkChange.java b/src/main/java/dan200/computercraft/shared/wired/WiredNetworkChange.java new file mode 100644 index 000000000..144fe6a76 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/wired/WiredNetworkChange.java @@ -0,0 +1,122 @@ +/* + * 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.wired; + +import dan200.computercraft.api.network.wired.IWiredNetworkChange; +import dan200.computercraft.api.peripheral.IPeripheral; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class WiredNetworkChange implements IWiredNetworkChange +{ + private static final WiredNetworkChange EMPTY = new WiredNetworkChange( Collections.emptyMap(), Collections.emptyMap() ); + + private final Map removed; + private final Map added; + + private WiredNetworkChange( Map removed, Map added ) + { + this.removed = removed; + this.added = added; + } + + public static WiredNetworkChange changed( Map removed, Map added ) + { + return new WiredNetworkChange( Collections.unmodifiableMap( removed ), Collections.unmodifiableMap( added ) ); + } + + public static WiredNetworkChange added( Map added ) + { + return added.isEmpty() ? EMPTY : new WiredNetworkChange( Collections.emptyMap(), Collections.unmodifiableMap( added ) ); + } + + public static WiredNetworkChange removed( Map removed ) + { + return removed.isEmpty() ? EMPTY : new WiredNetworkChange( Collections.unmodifiableMap( removed ), Collections.emptyMap() ); + } + + public static WiredNetworkChange changeOf( Map oldPeripherals, Map newPeripherals ) + { + // Handle the trivial cases, where all peripherals have been added or removed. + if( oldPeripherals.isEmpty() && newPeripherals.isEmpty() ) + { + return EMPTY; + } + else if( oldPeripherals.isEmpty() ) + { + return new WiredNetworkChange( Collections.emptyMap(), newPeripherals ); + } + else if( newPeripherals.isEmpty() ) + { + return new WiredNetworkChange( oldPeripherals, Collections.emptyMap() ); + } + + Map added = new HashMap<>( newPeripherals ); + Map removed = new HashMap<>(); + + for( Map.Entry entry : oldPeripherals.entrySet() ) + { + String oldKey = entry.getKey(); + IPeripheral oldValue = entry.getValue(); + if( newPeripherals.containsKey( oldKey ) ) + { + IPeripheral rightValue = added.get( oldKey ); + if( oldValue.equals( rightValue ) ) + { + added.remove( oldKey ); + } + else + { + removed.put( oldKey, oldValue ); + } + } + else + { + removed.put( oldKey, oldValue ); + } + } + + return changed( removed, added ); + } + + @Nonnull + @Override + public Map peripheralsAdded() + { + return added; + } + + @Nonnull + @Override + public Map peripheralsRemoved() + { + return removed; + } + + public boolean isEmpty() + { + return added.isEmpty() && removed.isEmpty(); + } + + void broadcast( Iterable nodes ) + { + if( !isEmpty() ) + { + for( WiredNode node : nodes ) node.element.networkChanged( this ); + } + } + + void broadcast( WiredNode node ) + { + if( !isEmpty() ) + { + node.element.networkChanged( this ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/wired/WiredNode.java b/src/main/java/dan200/computercraft/shared/wired/WiredNode.java new file mode 100644 index 000000000..4ccd80ebc --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/wired/WiredNode.java @@ -0,0 +1,152 @@ +/* + * 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.wired; + +import dan200.computercraft.api.network.IPacketReceiver; +import dan200.computercraft.api.network.Packet; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNetwork; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.network.wired.IWiredSender; +import dan200.computercraft.api.peripheral.IPeripheral; + +import javax.annotation.Nonnull; +import java.util.*; +import java.util.concurrent.locks.Lock; + +public final class WiredNode implements IWiredNode +{ + private Set receivers; + + final IWiredElement element; + Map peripherals = Collections.emptyMap(); + + final HashSet neighbours = new HashSet<>(); + volatile WiredNetwork network; + + public WiredNode( IWiredElement element ) + { + this.element = element; + network = new WiredNetwork( this ); + } + + @Override + public synchronized void addReceiver( @Nonnull IPacketReceiver receiver ) + { + if( receivers == null ) receivers = new HashSet<>(); + receivers.add( receiver ); + } + + @Override + public synchronized void removeReceiver( @Nonnull IPacketReceiver receiver ) + { + if( receivers != null ) receivers.remove( receiver ); + } + + synchronized void tryTransmit( Packet packet, double packetDistance, boolean packetInterdimensional, double range, boolean interdimensional ) + { + if( receivers == null ) return; + + for( IPacketReceiver receiver : receivers ) + { + if( !packetInterdimensional ) + { + double receiveRange = Math.max( range, receiver.getRange() ); // Ensure range is symmetrical + if( interdimensional || receiver.isInterdimensional() || packetDistance < receiveRange ) + { + receiver.receiveSameDimension( packet, packetDistance + element.getPosition().distanceTo( receiver.getPosition() ) ); + } + } + else + { + if( interdimensional || receiver.isInterdimensional() ) + { + receiver.receiveDifferentDimension( packet ); + } + } + } + } + + @Override + public boolean isWireless() + { + return false; + } + + @Override + public void transmitSameDimension( @Nonnull Packet packet, double range ) + { + Objects.requireNonNull( packet, "packet cannot be null" ); + if( !(packet.getSender() instanceof IWiredSender) || ((IWiredSender) packet.getSender()).getNode() != this ) + { + throw new IllegalArgumentException( "Sender is not in the network" ); + } + + acquireReadLock(); + try + { + WiredNetwork.transmitPacket( this, packet, range, false ); + } + finally + { + network.lock.readLock().unlock(); + } + } + + @Override + public void transmitInterdimensional( @Nonnull Packet packet ) + { + Objects.requireNonNull( packet, "packet cannot be null" ); + if( !(packet.getSender() instanceof IWiredSender) || ((IWiredSender) packet.getSender()).getNode() != this ) + { + throw new IllegalArgumentException( "Sender is not in the network" ); + } + + acquireReadLock(); + try + { + WiredNetwork.transmitPacket( this, packet, 0, true ); + } + finally + { + network.lock.readLock().unlock(); + } + } + + @Nonnull + @Override + public IWiredElement getElement() + { + return element; + } + + @Nonnull + @Override + public IWiredNetwork getNetwork() + { + return network; + } + + @Override + public String toString() + { + return "WiredNode{@" + element.getPosition() + " (" + element.getClass().getSimpleName() + ")}"; + } + + private void acquireReadLock() + { + WiredNetwork currentNetwork = network; + while( true ) + { + Lock lock = currentNetwork.lock.readLock(); + lock.lock(); + if( currentNetwork == network ) return; + + + lock.unlock(); + } + } +} diff --git a/src/main/resources/assets/computercraft/lang/da_dk.json b/src/main/resources/assets/computercraft/lang/da_dk.json new file mode 100644 index 000000000..1b72f4622 --- /dev/null +++ b/src/main/resources/assets/computercraft/lang/da_dk.json @@ -0,0 +1,43 @@ +{ + "block.computercraft.computer_normal": "Computer", + "block.computercraft.computer_advanced": "Avanceret Computer", + "block.computercraft.computer_command": "Kommandocomputer", + "block.computercraft.disk_drive": "Diskdrev", + "block.computercraft.printer": "Printer", + "block.computercraft.speaker": "Højttaler", + "block.computercraft.monitor_normal": "Skærm", + "block.computercraft.monitor_advanced": "Avanceret Skærm", + "block.computercraft.wireless_modem_normal": "Trådløst Modem", + "block.computercraft.wireless_modem_advanced": "Endermodem", + "block.computercraft.wired_modem": "Kablet Modem", + "block.computercraft.cable": "Netværkskabel", + "block.computercraft.turtle_normal": "Turtle", + "block.computercraft.turtle_normal.upgraded": "%s Turtle", + "block.computercraft.turtle_normal.upgraded_twice": "%s %s Turtle", + "block.computercraft.turtle_advanced": "Avanceret Turtle", + "block.computercraft.turtle_advanced.upgraded": "Avanceret %s Turtle", + "block.computercraft.turtle_advanced.upgraded_twice": "Avanceret %s %s Turtle", + "item.computercraft.disk": "Floppydisk", + "item.computercraft.treasure_disk": "Floppydisk", + "item.computercraft.printed_page": "Printet Side", + "item.computercraft.printed_pages": "Printede Sider", + "item.computercraft.printed_book": "Printet Bog", + "item.computercraft.pocket_computer_normal": "Lommecomputer", + "item.computercraft.pocket_computer_normal.upgraded": "%s Lommecomputer", + "item.computercraft.pocket_computer_advanced": "Avanceret Lommecomputer", + "item.computercraft.pocket_computer_advanced.upgraded": "Avanceret %s Lommecomputer", + "upgrade.minecraft.diamond_sword.adjective": "Kæmpende", + "upgrade.minecraft.diamond_shovel.adjective": "Gravende", + "upgrade.minecraft.diamond_pickaxe.adjective": "Brydende", + "upgrade.minecraft.diamond_axe.adjective": "Fældende", + "upgrade.minecraft.diamond_hoe.adjective": "Dyrkende", + "upgrade.minecraft.crafting_table.adjective": "Fremstillende", + "upgrade.computercraft.wireless_modem_normal.adjective": "Trådløs", + "upgrade.computercraft.wireless_modem_advanced.adjective": "Endertrådløs", + "upgrade.computercraft.speaker.adjective": "Larmende", + "chat.computercraft.wired_modem.peripheral_connected": "Perifer enhed \"%s\" koblet til netværk", + "chat.computercraft.wired_modem.peripheral_disconnected": "Perifer enhed \"%s\" koblet fra netværk", + "gui.computercraft.tooltip.copy": "Kopier til udklipsholder", + "gui.computercraft.tooltip.computer_id": "Computer-ID: %s", + "gui.computercraft.tooltip.disk_id": "Disk-ID: %s" +} diff --git a/src/main/resources/assets/computercraft/lang/ko_kr.json b/src/main/resources/assets/computercraft/lang/ko_kr.json new file mode 100644 index 000000000..919d7e56d --- /dev/null +++ b/src/main/resources/assets/computercraft/lang/ko_kr.json @@ -0,0 +1,108 @@ +{ + "itemGroup.computercraft": "컴퓨터크래프트", + "block.computercraft.computer_normal": "컴퓨터", + "block.computercraft.computer_advanced": "고급 컴퓨터", + "block.computercraft.computer_command": "명령 컴퓨터", + "block.computercraft.disk_drive": "디스크 드라이브", + "block.computercraft.printer": "프린터", + "block.computercraft.speaker": "스피커", + "block.computercraft.monitor_normal": "모니터", + "block.computercraft.monitor_advanced": "고급 모니터", + "block.computercraft.wireless_modem_normal": "무선 모뎀", + "block.computercraft.wireless_modem_advanced": "엔더 모뎀", + "block.computercraft.wired_modem": "유선 모뎀", + "block.computercraft.cable": "네트워크 케이블", + "block.computercraft.turtle_normal": "터틀", + "block.computercraft.turtle_normal.upgraded": "%s 터틀", + "block.computercraft.turtle_normal.upgraded_twice": "%s %s 터틀", + "block.computercraft.turtle_advanced": "고급 터틀", + "block.computercraft.turtle_advanced.upgraded": "고급 %s 터틀", + "block.computercraft.turtle_advanced.upgraded_twice": "고급 %s %s 터틀", + "item.computercraft.disk": "플로피 디스크", + "item.computercraft.treasure_disk": "플로피 디스크", + "item.computercraft.printed_page": "인쇄된 페이지", + "item.computercraft.printed_pages": "인쇄된 페이지 모음", + "item.computercraft.printed_book": "인쇄된 책", + "item.computercraft.pocket_computer_normal": "포켓 컴퓨터", + "item.computercraft.pocket_computer_normal.upgraded": "%s 포켓 컴퓨터", + "item.computercraft.pocket_computer_advanced": "고급 포켓 컴퓨터", + "item.computercraft.pocket_computer_advanced.upgraded": "고급 %s 포켓 컴퓨터", + "upgrade.minecraft.diamond_sword.adjective": "난투", + "upgrade.minecraft.diamond_shovel.adjective": "굴착", + "upgrade.minecraft.diamond_pickaxe.adjective": "채굴", + "upgrade.minecraft.diamond_axe.adjective": "벌목", + "upgrade.minecraft.diamond_hoe.adjective": "농업", + "upgrade.minecraft.crafting_table.adjective": "조합", + "upgrade.computercraft.wireless_modem_normal.adjective": "무선", + "upgrade.computercraft.wireless_modem_advanced.adjective": "엔더", + "upgrade.computercraft.speaker.adjective": "소음", + "chat.computercraft.wired_modem.peripheral_connected": "주변 \"%s\"이 네트워크에 연결되었습니다.", + "chat.computercraft.wired_modem.peripheral_disconnected": "주변 \"%s\"이 네트워크로부터 분리되었습니다.", + "commands.computercraft.synopsis": "컴퓨터를 제어하기 위한 다양한 명령어", + "commands.computercraft.desc": "/computercraft 명령어는 컴퓨터를 제어하고 상호작용하기 위한 다양한 디버깅 및 관리자 도구를 제공합니다.", + "commands.computercraft.help.synopsis": "특정 명령어에 대한 도움말을 제공하기", + "commands.computercraft.help.no_children": "%s에는 하위 명령어가 없습니다.", + "commands.computercraft.help.no_command": "'%s'라는 명령어가 없습니다.", + "commands.computercraft.dump.synopsis": "컴퓨터의 상태를 보여주기", + "commands.computercraft.dump.desc": "모든 시스템의 상태 또는 한 시스템에 대한 특정 정보를 표시합니다. 컴퓨터의 인스턴스 ID(예: 123)나 컴퓨터 ID(예: #123) 또는 라벨(예: \"@My Computer\")을 지정할 수 있습니다.", + "commands.computercraft.dump.action": "이 컴퓨터에 대한 추가 정보를 봅니다.", + "commands.computercraft.shutdown.synopsis": "시스템을 원격으로 종료하기", + "commands.computercraft.shutdown.desc": "나열된 시스템 또는 지정된 시스템이 없는 경우 모두 종료합니다. 컴퓨터의 인스턴스 ID(예: 123)나 컴퓨터 ID(예: #123) 또는 라벨(예: \"@My Computer\")을 지정할 수 있습니다.", + "commands.computercraft.shutdown.done": "%s/%s 컴퓨터 시스템 종료", + "commands.computercraft.turn_on.synopsis": "시스템을 원격으로 실행하기", + "commands.computercraft.turn_on.desc": "나열된 컴퓨터를 실행합니다. 컴퓨터의 인스턴스 ID(예: 123)나 컴퓨터 ID(예: #123) 또는 라벨(예: \"@My Computer\")을 지정할 수 있습니다.", + "commands.computercraft.turn_on.done": "%s/%s 컴퓨터 시스템 실행", + "commands.computercraft.tp.synopsis": "특정 컴퓨터로 순간이동하기", + "commands.computercraft.tp.desc": "컴퓨터의 위치로 순간이동합니다. 컴퓨터의 인스턴스 ID(예: 123) 또는 컴퓨터 ID(예: #123)를 지정할 수 있습니다.", + "commands.computercraft.tp.action": "이 컴퓨터로 순간이동하기", + "commands.computercraft.tp.not_there": "월드에서 컴퓨터를 위치시킬 수 없습니다.", + "commands.computercraft.view.synopsis": "컴퓨터의 터미널을 보기", + "commands.computercraft.view.desc": "컴퓨터의 원격 제어를 허용하는 컴퓨터의 터미널을 엽니다. 이것은 터틀의 인벤토리에 대한 접근을 제공하지 않습니다. 컴퓨터의 인스턴스 ID(예: 123) 또는 컴퓨터 ID(예: #123)를 지정할 수 있습니다.", + "commands.computercraft.view.action": "이 컴퓨터를 봅니다.", + "commands.computercraft.view.not_player": "비플레이어한테 터미널을 열 수 없습니다.", + "commands.computercraft.track.synopsis": "컴퓨터의 실행 시간을 추적하기", + "commands.computercraft.track.desc": "컴퓨터가 실행되는 기간과 처리되는 이벤트 수를 추적합니다. 이는 /forge 트랙과 유사한 방법으로 정보를 제공하며 지연 로그에 유용할 수 있습니다.", + "commands.computercraft.track.start.synopsis": "모든 컴퓨터의 추적을 시작하기", + "commands.computercraft.track.start.desc": "모든 컴퓨터의 이벤트 및 실행 시간 추적을 시작합니다. 이는 이전 실행의 결과를 폐기할 것입니다.", + "commands.computercraft.track.start.stop": "%s을(를) 실행하여 추적을 중지하고 결과를 확인합니다.", + "commands.computercraft.track.stop.synopsis": "모든 컴퓨터의 추적을 중지하기", + "commands.computercraft.track.stop.desc": "모든 컴퓨터의 이벤트 및 실행 시간 추적을 중지합니다.", + "commands.computercraft.track.stop.action": "추적을 중지하려면 클릭하세요.", + "commands.computercraft.track.stop.not_enabled": "현재 추적하는 컴퓨터가 없습니다.", + "commands.computercraft.track.dump.synopsis": "최신 추적 결과를 덤프하기", + "commands.computercraft.track.dump.desc": "최신 컴퓨터 추적의 결과를 덤프합니다.", + "commands.computercraft.track.dump.no_timings": "사용가능한 시간이 없습니다.", + "commands.computercraft.track.dump.computer": "컴퓨터", + "commands.computercraft.reload.synopsis": "컴퓨터크래프트 구성파일을 리로드하기", + "commands.computercraft.reload.desc": "컴퓨터크래프트 구성파일을 리로드합니다.", + "commands.computercraft.reload.done": "리로드된 구성", + "commands.computercraft.queue.synopsis": "computer_command 이벤트를 명령 컴퓨터에 보내기", + "commands.computercraft.queue.desc": "computer_command 이벤트를 명령 컴퓨터로 전송하여 추가 인수를 전달합니다. 이는 대부분 지도 제작자를 위해 설계되었으며, 보다 컴퓨터 친화적인 버전의 /trigger 역할을 합니다. 어떤 플레이어든 명령을 실행할 수 있으며, 이는 텍스트 구성 요소의 클릭 이벤트를 통해 수행될 가능성이 가장 높습니다.", + "commands.computercraft.generic.no_position": "", + "commands.computercraft.generic.position": "%s, %s, %s", + "commands.computercraft.generic.yes": "Y", + "commands.computercraft.generic.no": "N", + "commands.computercraft.generic.exception": "처리되지 않은 예외 (%s)", + "commands.computercraft.generic.additional_rows": "%d개의 추가 행…", + "argument.computercraft.computer.no_matching": "'%s'와 일치하는 컴퓨터가 없습니다.", + "argument.computercraft.computer.many_matching": "'%s'와 일치하는 여러 컴퓨터 (인스턴스 %s)", + "tracking_field.computercraft.tasks.name": "작업", + "tracking_field.computercraft.total.name": "전체 시간", + "tracking_field.computercraft.average.name": "평균 시간", + "tracking_field.computercraft.max.name": "최대 시간", + "tracking_field.computercraft.server_count.name": "서버 작업 수", + "tracking_field.computercraft.server_time.name": "서버 작업 시간", + "tracking_field.computercraft.peripheral.name": "주변 호출", + "tracking_field.computercraft.fs.name": "파일시스템 작업", + "tracking_field.computercraft.turtle.name": "터틀 작업", + "tracking_field.computercraft.http.name": "HTTP 요청", + "tracking_field.computercraft.http_upload.name": "HTTP 업로드", + "tracking_field.computercraft.http_download.name": "HTTP 다운로드", + "tracking_field.computercraft.websocket_incoming.name": "웹소켓 수신", + "tracking_field.computercraft.websocket_outgoing.name": "웹소켓 송신", + "tracking_field.computercraft.coroutines_created.name": "코루틴 생성됨", + "tracking_field.computercraft.coroutines_dead.name": "코루틴 처리됨", + "gui.computercraft.tooltip.copy": "클립보드에 복사", + "gui.computercraft.tooltip.computer_id": "컴퓨터 ID: %s", + "gui.computercraft.tooltip.disk_id": "디스크 ID: %s" +} diff --git a/src/main/resources/assets/computercraft/lang/nl_nl.json b/src/main/resources/assets/computercraft/lang/nl_nl.json new file mode 100644 index 000000000..98a174583 --- /dev/null +++ b/src/main/resources/assets/computercraft/lang/nl_nl.json @@ -0,0 +1,113 @@ +{ + "itemGroup.computercraft": "ComputerCraft", + "block.computercraft.computer_normal": "Computer", + "block.computercraft.computer_advanced": "Geavanceerde Computer", + "block.computercraft.computer_command": "Commandocomputer", + "block.computercraft.disk_drive": "Diskettestation", + "block.computercraft.printer": "Printer", + "block.computercraft.speaker": "Luidspreker", + "block.computercraft.monitor_normal": "Beeldscherm", + "block.computercraft.monitor_advanced": "Geavanceerd Beeldscherm", + "block.computercraft.wireless_modem_normal": "Draadloze Modem", + "block.computercraft.wireless_modem_advanced": "Ender Modem", + "block.computercraft.wired_modem": "Bedrade Modem", + "block.computercraft.cable": "Netwerkkabel", + "block.computercraft.wired_modem_full": "Bedrade Modem", + "block.computercraft.turtle_normal": "Turtle", + "block.computercraft.turtle_normal.upgraded": "%s Turtle", + "block.computercraft.turtle_normal.upgraded_twice": "%s %s Turtle", + "block.computercraft.turtle_advanced": "Geavanceerde Turtle", + "block.computercraft.turtle_advanced.upgraded": "Geavanceerde %s Turtle", + "block.computercraft.turtle_advanced.upgraded_twice": "Geavanceerde %s %s Turtle", + "item.computercraft.disk": "Diskette", + "item.computercraft.treasure_disk": "Diskette", + "item.computercraft.printed_page": "Geprinte Pagina", + "item.computercraft.printed_pages": "Geprinte Pagina's", + "item.computercraft.printed_book": "Geprint Boek", + "item.computercraft.pocket_computer_normal": "Zakcomputer", + "item.computercraft.pocket_computer_normal.upgraded": "%s Zakcomputer", + "item.computercraft.pocket_computer_advanced": "Geavanceerde Zakcomputer", + "item.computercraft.pocket_computer_advanced.upgraded": "Geavanceerde %s Zakcomputer", + "upgrade.minecraft.diamond_sword.adjective": "Vechtende", + "upgrade.minecraft.diamond_shovel.adjective": "Gravende", + "upgrade.minecraft.diamond_pickaxe.adjective": "Mijnbouw", + "upgrade.minecraft.diamond_axe.adjective": "Kappende", + "upgrade.minecraft.diamond_hoe.adjective": "Landbouw", + "upgrade.minecraft.crafting_table.adjective": "Craftende", + "upgrade.computercraft.wireless_modem_normal.adjective": "Draadloos", + "upgrade.computercraft.wireless_modem_advanced.adjective": "Ender", + "upgrade.computercraft.speaker.adjective": "Lawaaierige", + "chat.computercraft.wired_modem.peripheral_connected": "Randapperaat \"%s\" gekoppeld met netwerk", + "chat.computercraft.wired_modem.peripheral_disconnected": "Randapperaat \"%s\" ontkoppeld van netwerk", + "commands.computercraft.synopsis": "Verschillende commando's voor het beheren van computers.", + "commands.computercraft.desc": "Het /computercraft commando biedt verschillende debug- en administrator-tools voor het beheren van en werken met Computers.", + "commands.computercraft.help.synopsis": "Biedt hulp voor een specifiek commando", + "commands.computercraft.help.desc": "Geeft dit hulpbericht weer", + "commands.computercraft.help.no_children": "%s heeft geen sub-commando's", + "commands.computercraft.help.no_command": "Er bestaat geen commando als '%s'", + "commands.computercraft.dump.synopsis": "Geef de status van computers weer.", + "commands.computercraft.dump.desc": "Geef de status van alle computers of specifieke informatie over \\u00e9\\u00e9n computer weer. Je kunt een instance-id (bijv. 123), computer-id (bijv. #123) of computer-label (bijv. \\\"@Mijn Computer\\\") opgeven.", + "commands.computercraft.dump.action": "Geef meer informatie over deze computer weer", + "commands.computercraft.shutdown.synopsis": "Sluit computers af op afstand.", + "commands.computercraft.shutdown.desc": "Sluit alle genoemde computers af, of geen enkele wanneer niet gespecificeerd. Je kunt een instance-id (bijv. 123), computer-id (bijv. #123) of computer-label (bijv. \\\"@Mijn Computer\\\") opgeven.", + "commands.computercraft.shutdown.done": "%s/%s computers afgesloten", + "commands.computercraft.track.stop.not_enabled": "Er worden op dit moment geen computers bijgehouden", + "commands.computercraft.track.dump.desc": "Dump de laatste resultaten van het bijhouden van computers.", + "commands.computercraft.queue.desc": "Verzend een computer_commando event naar een commandocomputer. Additionele argumenten worden doorgegeven. Dit is vooral bedoeld voor mapmakers. Het doet dienst als een computer-vriendelijke versie van /trigger. Elke speler kan het commando uitvoeren, wat meestal gedaan zal worden door een text-component klik-event.", + "commands.computercraft.turn_on.synopsis": "Zet computers aan op afstand.", + "commands.computercraft.turn_on.desc": "Zet de genoemde computers aan op afstand. Je kunt een instance-id (bijv. 123), computer-id (bijv. #123) of computer-label (bijv. \"@Mijn Computer\") opgeven.", + "commands.computercraft.turn_on.done": "%s/%s computers aangezet", + "commands.computercraft.tp.synopsis": "Teleporteer naar een specifieke computer.", + "commands.computercraft.tp.desc": "Teleporteer naar de locatie van een specefieke computer. Je kunt een instance-id (bijv. 123) of computer-id (bijv. #123) opgeven.", + "commands.computercraft.tp.action": "Teleporteer naar deze computer", + "commands.computercraft.tp.not_player": "Kan geen terminal openen voor non-speler", + "commands.computercraft.tp.not_there": "Kan de computer niet lokaliseren", + "commands.computercraft.view.synopsis": "De terminal van een computer weergeven.", + "commands.computercraft.view.desc": "De terminal van een computer weergeven, voor controle op afstand. Dit biedt geen toegang tot turtle's inventarissen. Je kunt een instance-id (bijv. 123) of computer-id (bijv. #123) opgeven.", + "commands.computercraft.view.action": "Geef deze computer weer", + "commands.computercraft.view.not_player": "Kan geen terminal openen voor non-speler", + "commands.computercraft.track.synopsis": "Houd uitvoertijd van computers bij.", + "commands.computercraft.track.desc": "Houd uitvoertijd en het aantal behandelde events van computers bij. Dit biedt informatie op een gelijke manier als /forge track en kan nuttig zijn in het opsloren van lag.", + "commands.computercraft.track.start.synopsis": "Start met het bijhouden van alle computers", + "commands.computercraft.track.start.desc": "Start met het bijhouden van uitvoertijden en aantal behandelde events van alle computers. Dit gooit de resultaten van eerdere runs weg.", + "commands.computercraft.track.start.stop": "Voer %s uit om het bijhouden te stoppen en de resultaten te tonen", + "commands.computercraft.track.stop.synopsis": "Stop het bijhouden van alle computers", + "commands.computercraft.track.stop.desc": "Stop het bijhouden van uitvoertijd en aantal behandelde events van alle computers", + "commands.computercraft.track.stop.action": "Klik om bijhouden te stoppen", + "commands.computercraft.track.dump.no_timings": "Geen tijden beschikbaar", + "commands.computercraft.track.dump.computer": "Computer", + "commands.computercraft.reload.synopsis": "Herlaad het ComputerCraft configuratiebestand", + "commands.computercraft.reload.desc": "Herlaad het ComputerCraft configuratiebestand", + "commands.computercraft.reload.done": "Configuratie herladen", + "commands.computercraft.queue.synopsis": "Verzend een computer_command event naar een commandocomputer", + "commands.computercraft.generic.position": "%s, %s, %s", + "commands.computercraft.generic.yes": "J", + "commands.computercraft.generic.no": "N", + "commands.computercraft.generic.exception": "Niet-afgehandelde exception (%s)", + "commands.computercraft.generic.additional_rows": "%d additionele rijen…", + "argument.computercraft.computer.no_matching": "Geen computer matcht '%s'", + "argument.computercraft.computer.many_matching": "Meerdere computers matchen '%s' (instanties %s)", + "argument.computercraft.argument_expected": "Argument verwacht", + "tracking_field.computercraft.tasks.name": "Taken", + "tracking_field.computercraft.total.name": "Totale tijd", + "tracking_field.computercraft.average.name": "Gemiddelde tijd", + "tracking_field.computercraft.max.name": "Maximale tijd", + "tracking_field.computercraft.server_count.name": "Aantal server-taken", + "tracking_field.computercraft.server_time.name": "Server-taak tijd", + "tracking_field.computercraft.peripheral.name": "Randapparatuur aanroepen", + "tracking_field.computercraft.turtle.name": "Turtle operaties", + "tracking_field.computercraft.http.name": "HTTP verzoeken", + "tracking_field.computercraft.http_upload.name": "HTTP upload", + "tracking_field.computercraft.websocket_incoming.name": "Websocket inkomend", + "tracking_field.computercraft.websocket_outgoing.name": "Websocket uitgaand", + "tracking_field.computercraft.coroutines_created.name": "Coroutines gecreëerd", + "gui.computercraft.tooltip.copy": "Kopiëren naar klembord", + "gui.computercraft.tooltip.computer_id": "Computer ID: %s", + "gui.computercraft.tooltip.disk_id": "Diskette ID: %s", + "tracking_field.computercraft.http_download.name": "HTTP download", + "commands.computercraft.generic.no_position": "", + "commands.computercraft.track.dump.synopsis": "Dump de laatste resultaten van het bijhouden van computers", + "tracking_field.computercraft.fs.name": "Restandssysteem operaties", + "tracking_field.computercraft.coroutines_dead.name": "Coroutines verwijderd", + "argument.computercraft.tracking_field.no_field": "Onbekend veld '%s'" +} diff --git a/src/main/resources/assets/computercraft/lang/pl_pl.json b/src/main/resources/assets/computercraft/lang/pl_pl.json new file mode 100644 index 000000000..027fc006e --- /dev/null +++ b/src/main/resources/assets/computercraft/lang/pl_pl.json @@ -0,0 +1,43 @@ +{ + "itemGroup.computercraft": "ComputerCraft", + "block.computercraft.computer_normal": "Komputer", + "block.computercraft.computer_advanced": "Zaawansowany Komputer", + "block.computercraft.computer_command": "Komputer Komendowy", + "block.computercraft.disk_drive": "Stacja Dyskietek", + "block.computercraft.printer": "Drukarka", + "block.computercraft.speaker": "Głośnik", + "block.computercraft.monitor_normal": "Monitor", + "block.computercraft.monitor_advanced": "Zaawansowany Monitor", + "block.computercraft.wireless_modem_normal": "Bezprzewodowy Adapter Sieciowy", + "block.computercraft.wired_modem": "Adapter Sieciowy", + "block.computercraft.cable": "Kabel Sieciowy", + "block.computercraft.wired_modem_full": "Adapter Sieciowy", + "item.computercraft.disk": "Dyskietka", + "item.computercraft.treasure_disk": "Dyskietka", + "item.computercraft.printed_page": "Wydrukowana Strona", + "item.computercraft.printed_pages": "Wydrukowane Strony", + "item.computercraft.printed_book": "Wydrukowana Książka", + "item.computercraft.pocket_computer_normal": "Komputer Przenośny", + "item.computercraft.pocket_computer_normal.upgraded": "%s Komputer Przenośny", + "item.computercraft.pocket_computer_advanced": "Zaawansowany Komputer Przenośny", + "chat.computercraft.wired_modem.peripheral_connected": "Urządzenie \"%s\" zostało podłączone do sieci", + "chat.computercraft.wired_modem.peripheral_disconnected": "Urządzenie \"%s\" zostało odłączone od sieci", + "commands.computercraft.synopsis": "Różne komendy do kontrolowania komputerami.", + "commands.computercraft.desc": "Komenda /computercraft dostarcza wiele narzędzi do zarządzania, kontrolowania i administrowania komputerami.", + "commands.computercraft.help.synopsis": "Uzyskaj pomoc do konkretnej komendy", + "commands.computercraft.help.no_children": "%s nie ma pod-komend", + "commands.computercraft.help.no_command": "Nie odnaleziono komendy '%s'", + "commands.computercraft.dump.synopsis": "Wyświetl stan komputerów.", + "commands.computercraft.dump.desc": "Wyświetla status wszystkich komputerów lub informacje o jednym komputerze. Możesz wybrać numer sesji komputera (np. 123), ID komputera (np. #123) lub jego etykietę (np. \"@Mój Komputer\").", + "commands.computercraft.dump.action": "Wyświetl więcej informacji o tym komputerze", + "commands.computercraft.shutdown.synopsis": "Zdalnie wyłącz komputery.", + "commands.computercraft.shutdown.desc": "Wyłącz wszystkie, lub tylko wylistowane komputery. Możesz wybrać numer sesji komputera (np. 123), ID komputera (np. #123) lub jego etykietę (np. \"@Mój Komputer\").", + "commands.computercraft.shutdown.done": "Wyłączono %s z %s komputerów", + "commands.computercraft.turn_on.synopsis": "Zdalnie włącz komputery.", + "commands.computercraft.turn_on.desc": "Włącz podane komputery. Możesz wybrać numer sesji komputera (np. 123), ID komputera (np. #123) lub jego etykietę (np. \"@Mój Komputer\").", + "commands.computercraft.turn_on.done": "Włączono %s z %s komputerów", + "commands.computercraft.tp.synopsis": "Przeteleportuj się do podanego komputera.", + "commands.computercraft.tp.desc": "Przeteleportuj się do lokalizacji komputera. Możesz wybrać numer sesji komputera (np. 123) lub ID komputera (np. #123).", + "commands.computercraft.tp.action": "Przeteleportuj się do podanego komputera", + "commands.computercraft.view.synopsis": "Wyświetl ekran komputera." +} diff --git a/src/main/resources/assets/computercraft/lang/ru.json b/src/main/resources/assets/computercraft/lang/ru.json new file mode 100644 index 000000000..210b77091 --- /dev/null +++ b/src/main/resources/assets/computercraft/lang/ru.json @@ -0,0 +1,113 @@ +{ + "block.computercraft.computer_normal": "Компьютер", + "block.computercraft.computer_advanced": "Улучшенный компьютер", + "block.computercraft.computer_command": "Командный компьютер", + "block.computercraft.disk_drive": "Дисковод", + "block.computercraft.printer": "Принтер", + "block.computercraft.speaker": "Колонка", + "block.computercraft.monitor_normal": "Монитор", + "block.computercraft.monitor_advanced": "Улучшенный монитор", + "block.computercraft.wireless_modem_normal": "Беспроводной модем", + "block.computercraft.wireless_modem_advanced": "Эндер модем", + "block.computercraft.wired_modem": "Проводной модем", + "block.computercraft.cable": "Сетевой кабель", + "block.computercraft.turtle_normal.upgraded": "%s черепашка", + "block.computercraft.turtle_advanced": "Улучшенная черепашка", + "block.computercraft.turtle_advanced.upgraded": "Улучшенная %s черепашка", + "block.computercraft.turtle_advanced.upgraded_twice": "Улучшенная %s %s черепашка", + "item.computercraft.treasure_disk": "Дискета", + "item.computercraft.printed_page": "Напечатанная страница", + "item.computercraft.printed_book": "Напечатанная книга", + "item.computercraft.pocket_computer_normal": "Карманный компьютер", + "item.computercraft.pocket_computer_normal.upgraded": "%s карманный компьютер", + "item.computercraft.pocket_computer_advanced.upgraded": "Улучшенный %s карманный компьютер", + "upgrade.minecraft.diamond_sword.adjective": "Боевая", + "upgrade.minecraft.diamond_shovel.adjective": "Копающая", + "upgrade.minecraft.diamond_pickaxe.adjective": "Добывающая", + "upgrade.minecraft.diamond_axe.adjective": "Рубящая", + "upgrade.minecraft.diamond_hoe.adjective": "Возделывающая", + "upgrade.minecraft.crafting_table.adjective": "Крафтящая", + "upgrade.computercraft.wireless_modem_advanced.adjective": "Эндер", + "upgrade.computercraft.speaker.adjective": "Шумящий", + "chat.computercraft.wired_modem.peripheral_connected": "Устройство \"%s\" подключено к сети", + "commands.computercraft.synopsis": "Команды для управления компьютерами.", + "commands.computercraft.help.synopsis": "Получить подсказку по одной из команд", + "commands.computercraft.help.desc": "Показывает этот текст", + "commands.computercraft.help.no_children": "%s не имеет подкоманд", + "commands.computercraft.help.no_command": "Нет команды '%s'", + "commands.computercraft.dump.synopsis": "Показать статус компьютеров.", + "commands.computercraft.dump.action": "Показать больше информации об этом компьютере", + "commands.computercraft.shutdown.synopsis": "Удалённо выключить компьютер.", + "commands.computercraft.shutdown.done": "%s/%s компьютеров выключено", + "commands.computercraft.turn_on.synopsis": "Удалённо включить компьютеры.", + "commands.computercraft.turn_on.done": "%s/%s компьютеров включено", + "commands.computercraft.tp.synopsis": "Телепортироваться к компьютеру.", + "commands.computercraft.tp.desc": "Телепортироваться к месту установки компьютера. Можно указать номер инстанса (например 123) или идентификатор компьютера (#123).", + "commands.computercraft.tp.action": "Телепортироваться к этому компьютеру", + "commands.computercraft.tp.not_there": "Невозможно найти компьютер в мире", + "commands.computercraft.view.synopsis": "Показать терминал компьютера.", + "commands.computercraft.view.not_player": "Нельзя открыть терминал для не-игрока", + "commands.computercraft.view.action": "Открыть этот компьютер", + "commands.computercraft.track.synopsis": "Отслеживание метрик компьютеров.", + "commands.computercraft.track.start.synopsis": "Начать собирать метрики со всех компьютеров", + "commands.computercraft.track.start.desc": "Начать собирать метрики со всех компьютеров: сколько времени выполняется код и сколько событий обрабатывается. Это сотрёт результаты предыдущего сбора.", + "commands.computercraft.track.start.stop": "Запустите %s чтобы остановить сбор метрик и посмотреть результаты", + "commands.computercraft.track.stop.action": "Остановить сбор метрик", + "commands.computercraft.track.stop.not_enabled": "Сбор метрик не был запущен", + "commands.computercraft.track.dump.synopsis": "Вывести результаты сбора метрик", + "commands.computercraft.track.dump.desc": "Вывести результаты последнего сбора метрик.", + "commands.computercraft.track.dump.no_timings": "Нет доступных результатов сбора метрик", + "commands.computercraft.track.dump.computer": "Компьютер", + "commands.computercraft.reload.synopsis": "Перезагрузить конфигурацию ComputerCraft", + "commands.computercraft.reload.done": "Конфигурация перезагружена", + "commands.computercraft.queue.desc": "Отправить событие computer_command командному компьютеру. Это нужно для создателей карт, более дружественная версия /trigger. Любой игрок сможет запустить команду, которая скорее всего будет выполнена с помощью click event текстового компонента.", + "commands.computercraft.generic.no_position": "<нет позиции>", + "commands.computercraft.generic.position": "%s, %s, %s", + "commands.computercraft.generic.yes": "Д", + "commands.computercraft.generic.no": "Н", + "commands.computercraft.generic.exception": "Необработанное исключение (%s)", + "commands.computercraft.generic.additional_rows": "%d дополнительных строк…", + "argument.computercraft.computer.no_matching": "Нет компьютеров совпадающих с '%s'", + "argument.computercraft.tracking_field.no_field": "Неизвестное поле '%s'", + "argument.computercraft.argument_expected": "Ожидается аргумент", + "tracking_field.computercraft.tasks.name": "Задачи", + "tracking_field.computercraft.total.name": "Общее время", + "tracking_field.computercraft.average.name": "Среднее время", + "tracking_field.computercraft.max.name": "Максимальное время", + "tracking_field.computercraft.server_count.name": "Число серверных задач", + "tracking_field.computercraft.server_time.name": "Время серверных задач", + "tracking_field.computercraft.fs.name": "Операций с файлами", + "tracking_field.computercraft.turtle.name": "Действий черепашек", + "tracking_field.computercraft.http.name": "HTTP запросы", + "tracking_field.computercraft.http_upload.name": "HTTP загрузки", + "tracking_field.computercraft.http_download.name": "HTTP скачивания", + "tracking_field.computercraft.websocket_outgoing.name": "Исходящие вебсокеты", + "tracking_field.computercraft.coroutines_created.name": "Корутин создано", + "tracking_field.computercraft.coroutines_dead.name": "Корутин удалено", + "gui.computercraft.tooltip.copy": "Скопировать в буфер обмена", + "gui.computercraft.tooltip.disk_id": "ID дискеты: %s", + "itemGroup.computercraft": "ComputerCraft", + "block.computercraft.wired_modem_full": "Проводной модем", + "block.computercraft.turtle_normal": "Черепашка", + "block.computercraft.turtle_normal.upgraded_twice": "%s %s черепашка", + "item.computercraft.disk": "Дискета", + "item.computercraft.printed_pages": "Напечатанные страницы", + "item.computercraft.pocket_computer_advanced": "Улучшенный карманный компьютер", + "upgrade.computercraft.wireless_modem_normal.adjective": "Беспроводный", + "chat.computercraft.wired_modem.peripheral_disconnected": "Устройство \"%s\" отключено от сети", + "commands.computercraft.desc": "Команда /computercraft предоставляет отладочные и административные утилиты для управления компьютерами.", + "commands.computercraft.dump.desc": "Показать статус всех компьютеров или информацию об одном из компьютеров. Можно указать номер инстанса (например 123), идентификатор компьютера (#123) или метку (\"@My Computer\").", + "commands.computercraft.shutdown.desc": "Выключить указанные компьютеры или все. Можно указать номер инстанса (например 123), идентификатор компьютера (#123) или метку (\"@My Computer\").", + "commands.computercraft.turn_on.desc": "Включить указанные компьютеры. Можно указать номер инстанса (например 123), идентификатор компьютера (#123) или метку (\"@My Computer\").", + "commands.computercraft.tp.not_player": "Нельзя открыть терминал для не-игрока", + "commands.computercraft.view.desc": "Открыть терминал и получить контроль над компьютером. Это не показывает инвентарь черепашек. Можно указать номер инстанса (например 123) или идентификатор компьютера (#123).", + "commands.computercraft.track.desc": "Отслеживание как долго компьютеры выполняют код, как много событий они обрабатывают. Это показывает информацию аналогично /forge track и может быть полезно в поиске причин лагов.", + "commands.computercraft.track.stop.synopsis": "Остановить сбор метрик со всех компьютеров", + "commands.computercraft.track.stop.desc": "Остановить сбор метрик со всех компьютеров: сколько времени выполняется код и сколько событий обрабатывается", + "commands.computercraft.reload.desc": "Перезагрузить конфигурацию ComputerCraft", + "commands.computercraft.queue.synopsis": "Отправить событие computer_command командному компьютеру", + "argument.computercraft.computer.many_matching": "Несколько компьютеров совпадают с '%s' (инстансы %s)", + "tracking_field.computercraft.peripheral.name": "Вызовы периферийных устройств", + "tracking_field.computercraft.websocket_incoming.name": "Входящие вебсокеты", + "gui.computercraft.tooltip.computer_id": "ID компьютера: %s" +} diff --git a/src/main/resources/assets/computercraft/models/block/turtle_advanced_base.json b/src/main/resources/assets/computercraft/models/block/turtle_advanced_base.json new file mode 100644 index 000000000..c232c67d3 --- /dev/null +++ b/src/main/resources/assets/computercraft/models/block/turtle_advanced_base.json @@ -0,0 +1,6 @@ +{ + "parent": "computercraft:block/turtle_base", + "textures": { + "texture": "computercraft:block/turtle_advanced" + } +} diff --git a/src/main/resources/assets/computercraft/models/block/turtle_normal_base.json b/src/main/resources/assets/computercraft/models/block/turtle_normal_base.json new file mode 100644 index 000000000..840517b05 --- /dev/null +++ b/src/main/resources/assets/computercraft/models/block/turtle_normal_base.json @@ -0,0 +1,6 @@ +{ + "parent": "computercraft:block/turtle_base", + "textures": { + "texture": "computercraft:block/turtle_normal" + } +} diff --git a/src/main/resources/assets/computercraft/models/block/turtle_overlay.json b/src/main/resources/assets/computercraft/models/block/turtle_overlay.json new file mode 100644 index 000000000..906b68a82 --- /dev/null +++ b/src/main/resources/assets/computercraft/models/block/turtle_overlay.json @@ -0,0 +1,43 @@ +{ + "parent": "block/block", + "textures": { + "particle": "#texture" + }, + "elements": [ + { + "from": [ 2, 2, 2 ], + "to": [ 14, 14, 13 ], + "faces": { + "down": { "uv": [ 2.75, 0, 5.75, 2.75 ], "texture": "#texture" }, + "up": { "uv": [ 5.75, 0, 8.75, 2.75 ], "texture": "#texture" }, + "north": { "uv": [ 8.5, 5.75, 11.5, 2.75 ], "texture": "#texture" }, + "south": { "uv": [ 2.75, 5.75, 5.75, 2.75 ], "texture": "#texture" }, + "west": { "uv": [ 0, 5.75, 2.75, 2.75 ], "texture": "#texture" }, + "east": { "uv": [ 5.75, 5.75, 8.5, 2.75 ], "texture": "#texture" } + } + }, + { + "from": [ 3, 6, 13 ], + "to": [ 13, 13, 15 ], + "faces": { + "down": { "uv": [ 9.25, 0, 11.75, 0.5 ], "texture": "#texture" }, + "up": { "uv": [ 11.75, 0, 14.25, 0.5 ], "texture": "#texture" }, + "south": { "uv": [ 9.25, 2.25, 11.75, 0.5 ], "texture": "#texture" }, + "west": { "uv": [ 8.75, 2.25, 9.25, 0.5 ], "texture": "#texture" }, + "east": { "uv": [ 11.75, 2.25, 12.25, 0.5 ], "texture": "#texture" } + } + }, + { + "from": [ 1.5, 1.5, 1.5 ], + "to": [ 14.5, 14.5, 13.5 ], + "faces": { + "down": { "uv": [ 2.75, 8, 5.75, 10.75 ], "texture": "#texture" }, + "up": { "uv": [ 5.75, 8, 8.75, 10.75 ], "texture": "#texture" }, + "north": { "uv": [ 8.5, 13.75, 11.5, 10.75 ], "texture": "#texture" }, + "south": { "uv": [ 2.75, 13.75, 5.75, 10.75 ], "texture": "#texture" }, + "west": { "uv": [ 0, 13.75, 2.75, 10.75 ], "texture": "#texture" }, + "east": { "uv": [ 5.75, 13.75, 8.5, 10.75 ], "texture": "#texture" } + } + } + ] +} diff --git a/src/main/resources/assets/computercraft/shaders/monitor.frag b/src/main/resources/assets/computercraft/shaders/monitor.frag new file mode 100644 index 000000000..b0b7b49ed --- /dev/null +++ b/src/main/resources/assets/computercraft/shaders/monitor.frag @@ -0,0 +1,40 @@ +#version 140 + +#define FONT_WIDTH 6.0 +#define FONT_HEIGHT 9.0 + +uniform sampler2D u_font; +uniform int u_width; +uniform int u_height; +uniform samplerBuffer u_tbo; +uniform vec3 u_palette[16]; + +in vec2 f_pos; + +out vec4 colour; + +vec2 texture_corner(int index) { + float x = 1.0 + float(index % 16) * (FONT_WIDTH + 2.0); + float y = 1.0 + float(index / 16) * (FONT_HEIGHT + 2.0); + return vec2(x, y); +} + +void main() { + vec2 term_pos = vec2(f_pos.x / FONT_WIDTH, f_pos.y / FONT_HEIGHT); + vec2 corner = floor(term_pos); + + ivec2 cell = ivec2(corner); + int index = 3 * (clamp(cell.x, 0, u_width - 1) + clamp(cell.y, 0, u_height - 1) * u_width); + + // 1 if 0 <= x, y < width, height, 0 otherwise + vec2 outside = step(vec2(0.0, 0.0), vec2(cell)) * step(vec2(cell), vec2(float(u_width) - 1.0, float(u_height) - 1.0)); + float mult = outside.x * outside.y; + + int character = int(texelFetch(u_tbo, index).r * 255.0); + int fg = int(texelFetch(u_tbo, index + 1).r * 255.0); + int bg = int(texelFetch(u_tbo, index + 2).r * 255.0); + + vec2 pos = (term_pos - corner) * vec2(FONT_WIDTH, FONT_HEIGHT); + vec4 img = texture(u_font, (texture_corner(character) + pos) / 256.0); + colour = vec4(mix(u_palette[bg], img.rgb * u_palette[fg], img.a * mult), 1.0); +} diff --git a/src/main/resources/assets/computercraft/shaders/monitor.vert b/src/main/resources/assets/computercraft/shaders/monitor.vert new file mode 100644 index 000000000..15b81fbb7 --- /dev/null +++ b/src/main/resources/assets/computercraft/shaders/monitor.vert @@ -0,0 +1,12 @@ +#version 130 + +uniform mat4 u_mv; + +in vec3 v_pos; + +out vec2 f_pos; + +void main() { + gl_Position = gl_ProjectionMatrix * u_mv * vec4(v_pos.x, v_pos.y, 0, 1); + f_pos = v_pos.xy; +} diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/cable.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/cable.json new file mode 100644 index 000000000..69ed2c857 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/cable.json @@ -0,0 +1,43 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:cable" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_modem": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:cable" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_modem", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/computer_advanced.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/computer_advanced.json new file mode 100644 index 000000000..2f4cebec8 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/computer_advanced.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:computer_advanced" + ] + }, + "criteria": { + "has_components": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "minecraft:redstone" + }, + { + "item": "minecraft:gold_ingot" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:computer_advanced" + } + } + }, + "requirements": [ + [ + "has_components", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/computer_command.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/computer_command.json new file mode 100644 index 000000000..82a335701 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/computer_command.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:computer_command" + ] + }, + "criteria": { + "has_components": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "minecraft:command_block" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:computer_command" + } + } + }, + "requirements": [ + [ + "has_components", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/computer_normal.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/computer_normal.json new file mode 100644 index 000000000..df587895f --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/computer_normal.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:computer_normal" + ] + }, + "criteria": { + "has_redstone": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "forge:dusts/redstone" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:computer_normal" + } + } + }, + "requirements": [ + [ + "has_redstone", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_1.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_1.json new file mode 100644 index 000000000..4a511c507 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_1.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_1" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_1" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_10.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_10.json new file mode 100644 index 000000000..9d97d1db1 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_10.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_10" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_10" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_11.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_11.json new file mode 100644 index 000000000..4dc510b4e --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_11.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_11" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_11" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_12.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_12.json new file mode 100644 index 000000000..f5238fae6 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_12.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_12" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_12" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_13.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_13.json new file mode 100644 index 000000000..e81b64991 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_13.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_13" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_13" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_14.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_14.json new file mode 100644 index 000000000..a7322e1fa --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_14.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_14" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_14" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_15.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_15.json new file mode 100644 index 000000000..6ead3487a --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_15.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_15" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_15" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_16.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_16.json new file mode 100644 index 000000000..7bc970fc6 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_16.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_16" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_16" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_2.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_2.json new file mode 100644 index 000000000..b8671733b --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_2.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_2" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_2" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_3.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_3.json new file mode 100644 index 000000000..f646f0d05 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_3.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_3" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_3" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_4.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_4.json new file mode 100644 index 000000000..4b348588a --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_4.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_4" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_4" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_5.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_5.json new file mode 100644 index 000000000..40f6ef7de --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_5.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_5" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_5" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_6.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_6.json new file mode 100644 index 000000000..1b23baeec --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_6.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_6" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_6" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_7.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_7.json new file mode 100644 index 000000000..2cf60f3ac --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_7.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_7" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_7" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_8.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_8.json new file mode 100644 index 000000000..fac9be366 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_8.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_8" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_8" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_9.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_9.json new file mode 100644 index 000000000..b1eb4a9b5 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_9.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_9" + ] + }, + "criteria": { + "has_drive": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:disk_drive" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_9" + } + } + }, + "requirements": [ + [ + "has_drive", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_drive.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_drive.json new file mode 100644 index 000000000..7fb18766e --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/disk_drive.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:disk_drive" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:disk_drive" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/monitor_advanced.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/monitor_advanced.json new file mode 100644 index 000000000..dab4faad3 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/monitor_advanced.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:monitor_advanced" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:monitor_advanced" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/monitor_normal.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/monitor_normal.json new file mode 100644 index 000000000..fdd3c274c --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/monitor_normal.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:monitor_normal" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:monitor_normal" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/computercraft/speaker.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/computercraft/speaker.json new file mode 100644 index 000000000..043371408 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/computercraft/speaker.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_advanced/computercraft/speaker" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_advanced" + }, + { + "item": "computercraft:speaker" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_advanced/computercraft/speaker" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/computercraft/wireless_modem_advanced.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/computercraft/wireless_modem_advanced.json new file mode 100644 index 000000000..7c77d28a2 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/computercraft/wireless_modem_advanced.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_advanced/computercraft/wireless_modem_advanced" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_advanced" + }, + { + "item": "computercraft:wireless_modem_advanced" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_advanced/computercraft/wireless_modem_advanced" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/computercraft/wireless_modem_normal.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/computercraft/wireless_modem_normal.json new file mode 100644 index 000000000..e072333c9 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/computercraft/wireless_modem_normal.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_advanced/computercraft/wireless_modem_normal" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_advanced" + }, + { + "item": "computercraft:wireless_modem_normal" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_advanced/computercraft/wireless_modem_normal" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/crafting_table.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/crafting_table.json new file mode 100644 index 000000000..112ba7081 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/crafting_table.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_advanced/minecraft/crafting_table" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_advanced" + }, + { + "item": "minecraft:crafting_table" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_advanced/minecraft/crafting_table" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_axe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_axe.json new file mode 100644 index 000000000..7b5c315e4 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_axe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_advanced/minecraft/diamond_axe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_advanced" + }, + { + "item": "minecraft:diamond_axe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_advanced/minecraft/diamond_axe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_hoe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_hoe.json new file mode 100644 index 000000000..9f5872d9f --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_hoe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_advanced/minecraft/diamond_hoe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_advanced" + }, + { + "item": "minecraft:diamond_hoe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_advanced/minecraft/diamond_hoe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_pickaxe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_pickaxe.json new file mode 100644 index 000000000..7e1930b03 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_pickaxe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_advanced/minecraft/diamond_pickaxe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_advanced" + }, + { + "item": "minecraft:diamond_pickaxe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_advanced/minecraft/diamond_pickaxe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_shovel.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_shovel.json new file mode 100644 index 000000000..05222ce97 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_shovel.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_advanced/minecraft/diamond_shovel" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_advanced" + }, + { + "item": "minecraft:diamond_shovel" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_advanced/minecraft/diamond_shovel" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_sword.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_sword.json new file mode 100644 index 000000000..413e0375d --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_advanced/minecraft/diamond_sword.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_advanced/minecraft/diamond_sword" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_advanced" + }, + { + "item": "minecraft:diamond_sword" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_advanced/minecraft/diamond_sword" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_computer_advanced.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_computer_advanced.json new file mode 100644 index 000000000..468d54cc9 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_computer_advanced.json @@ -0,0 +1,43 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_computer_advanced" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_apple": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "minecraft:golden_apple" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_computer_advanced" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_apple", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_computer_normal.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_computer_normal.json new file mode 100644 index 000000000..4d40913a7 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_computer_normal.json @@ -0,0 +1,43 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_computer_normal" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_apple": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "minecraft:golden_apple" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_computer_normal" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_apple", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/computercraft/speaker.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/computercraft/speaker.json new file mode 100644 index 000000000..35ab06eea --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/computercraft/speaker.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_normal/computercraft/speaker" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_normal" + }, + { + "item": "computercraft:speaker" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_normal/computercraft/speaker" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/computercraft/wireless_modem_advanced.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/computercraft/wireless_modem_advanced.json new file mode 100644 index 000000000..104f9d30e --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/computercraft/wireless_modem_advanced.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_normal/computercraft/wireless_modem_advanced" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_normal" + }, + { + "item": "computercraft:wireless_modem_advanced" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_normal/computercraft/wireless_modem_advanced" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/computercraft/wireless_modem_normal.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/computercraft/wireless_modem_normal.json new file mode 100644 index 000000000..5f7431ad7 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/computercraft/wireless_modem_normal.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_normal/computercraft/wireless_modem_normal" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_normal" + }, + { + "item": "computercraft:wireless_modem_normal" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_normal/computercraft/wireless_modem_normal" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/crafting_table.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/crafting_table.json new file mode 100644 index 000000000..c507d2563 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/crafting_table.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_normal/minecraft/crafting_table" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_normal" + }, + { + "item": "minecraft:crafting_table" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_normal/minecraft/crafting_table" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_axe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_axe.json new file mode 100644 index 000000000..a14901a3e --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_axe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_normal/minecraft/diamond_axe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_normal" + }, + { + "item": "minecraft:diamond_axe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_normal/minecraft/diamond_axe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_hoe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_hoe.json new file mode 100644 index 000000000..5dba7dcd6 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_hoe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_normal/minecraft/diamond_hoe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_normal" + }, + { + "item": "minecraft:diamond_hoe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_normal/minecraft/diamond_hoe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_pickaxe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_pickaxe.json new file mode 100644 index 000000000..06a10b8ec --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_pickaxe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_normal/minecraft/diamond_pickaxe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_normal" + }, + { + "item": "minecraft:diamond_pickaxe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_normal/minecraft/diamond_pickaxe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_shovel.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_shovel.json new file mode 100644 index 000000000..22ccd7e60 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_shovel.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_normal/minecraft/diamond_shovel" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_normal" + }, + { + "item": "minecraft:diamond_shovel" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_normal/minecraft/diamond_shovel" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_sword.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_sword.json new file mode 100644 index 000000000..74378a20c --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/pocket_normal/minecraft/diamond_sword.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:pocket_normal/minecraft/diamond_sword" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:pocket_computer_normal" + }, + { + "item": "minecraft:diamond_sword" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:pocket_normal/minecraft/diamond_sword" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/printer.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/printer.json new file mode 100644 index 000000000..08a2ffeba --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/printer.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:printer" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:printer" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/speaker.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/speaker.json new file mode 100644 index 000000000..194f7caec --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/speaker.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:speaker" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:speaker" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/computercraft/speaker.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/computercraft/speaker.json new file mode 100644 index 000000000..c5ddd2003 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/computercraft/speaker.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_advanced/computercraft/speaker" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_advanced" + }, + { + "item": "computercraft:speaker" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_advanced/computercraft/speaker" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/computercraft/wireless_modem_advanced.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/computercraft/wireless_modem_advanced.json new file mode 100644 index 000000000..b429cf9e6 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/computercraft/wireless_modem_advanced.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_advanced/computercraft/wireless_modem_advanced" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_advanced" + }, + { + "item": "computercraft:wireless_modem_advanced" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_advanced/computercraft/wireless_modem_advanced" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/computercraft/wireless_modem_normal.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/computercraft/wireless_modem_normal.json new file mode 100644 index 000000000..6ac217096 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/computercraft/wireless_modem_normal.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_advanced/computercraft/wireless_modem_normal" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_advanced" + }, + { + "item": "computercraft:wireless_modem_normal" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_advanced/computercraft/wireless_modem_normal" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/crafting_table.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/crafting_table.json new file mode 100644 index 000000000..b2bb0df1a --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/crafting_table.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_advanced/minecraft/crafting_table" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_advanced" + }, + { + "item": "minecraft:crafting_table" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_advanced/minecraft/crafting_table" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_axe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_axe.json new file mode 100644 index 000000000..b11436e4b --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_axe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_advanced/minecraft/diamond_axe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_advanced" + }, + { + "item": "minecraft:diamond_axe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_advanced/minecraft/diamond_axe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_hoe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_hoe.json new file mode 100644 index 000000000..5808c4ed5 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_hoe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_advanced/minecraft/diamond_hoe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_advanced" + }, + { + "item": "minecraft:diamond_hoe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_advanced/minecraft/diamond_hoe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_pickaxe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_pickaxe.json new file mode 100644 index 000000000..2cf9a608b --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_pickaxe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_advanced/minecraft/diamond_pickaxe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_advanced" + }, + { + "item": "minecraft:diamond_pickaxe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_advanced/minecraft/diamond_pickaxe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_shovel.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_shovel.json new file mode 100644 index 000000000..c36478843 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_shovel.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_advanced/minecraft/diamond_shovel" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_advanced" + }, + { + "item": "minecraft:diamond_shovel" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_advanced/minecraft/diamond_shovel" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_sword.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_sword.json new file mode 100644 index 000000000..664b9f9e8 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_advanced/minecraft/diamond_sword.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_advanced/minecraft/diamond_sword" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_advanced" + }, + { + "item": "minecraft:diamond_sword" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_advanced/minecraft/diamond_sword" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/computercraft/speaker.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/computercraft/speaker.json new file mode 100644 index 000000000..dfd1b4664 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/computercraft/speaker.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_normal/computercraft/speaker" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_normal" + }, + { + "item": "computercraft:speaker" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_normal/computercraft/speaker" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/computercraft/wireless_modem_advanced.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/computercraft/wireless_modem_advanced.json new file mode 100644 index 000000000..74c5dac34 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/computercraft/wireless_modem_advanced.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_normal/computercraft/wireless_modem_advanced" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_normal" + }, + { + "item": "computercraft:wireless_modem_advanced" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_normal/computercraft/wireless_modem_advanced" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/computercraft/wireless_modem_normal.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/computercraft/wireless_modem_normal.json new file mode 100644 index 000000000..e54be71d1 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/computercraft/wireless_modem_normal.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_normal/computercraft/wireless_modem_normal" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_normal" + }, + { + "item": "computercraft:wireless_modem_normal" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_normal/computercraft/wireless_modem_normal" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/crafting_table.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/crafting_table.json new file mode 100644 index 000000000..fe1c547eb --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/crafting_table.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_normal/minecraft/crafting_table" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_normal" + }, + { + "item": "minecraft:crafting_table" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_normal/minecraft/crafting_table" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_axe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_axe.json new file mode 100644 index 000000000..afd62779d --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_axe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_normal/minecraft/diamond_axe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_normal" + }, + { + "item": "minecraft:diamond_axe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_normal/minecraft/diamond_axe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_hoe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_hoe.json new file mode 100644 index 000000000..a6a16729e --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_hoe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_normal/minecraft/diamond_hoe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_normal" + }, + { + "item": "minecraft:diamond_hoe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_normal/minecraft/diamond_hoe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_pickaxe.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_pickaxe.json new file mode 100644 index 000000000..e26cd09cf --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_pickaxe.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_normal/minecraft/diamond_pickaxe" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_normal" + }, + { + "item": "minecraft:diamond_pickaxe" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_normal/minecraft/diamond_pickaxe" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_shovel.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_shovel.json new file mode 100644 index 000000000..3d6b54ec4 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_shovel.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_normal/minecraft/diamond_shovel" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_normal" + }, + { + "item": "minecraft:diamond_shovel" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_normal/minecraft/diamond_shovel" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_sword.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_sword.json new file mode 100644 index 000000000..ab64bae40 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/turtle_normal/minecraft/diamond_sword.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:turtle_normal/minecraft/diamond_sword" + ] + }, + "criteria": { + "has_items": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:turtle_normal" + }, + { + "item": "minecraft:diamond_sword" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:turtle_normal/minecraft/diamond_sword" + } + } + }, + "requirements": [ + [ + "has_items", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/wired_modem.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/wired_modem.json new file mode 100644 index 000000000..f78501816 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/wired_modem.json @@ -0,0 +1,43 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:wired_modem" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_cable": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:cable" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:wired_modem" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_cable", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/wired_modem_full_from.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/wired_modem_full_from.json new file mode 100644 index 000000000..a54c82034 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/wired_modem_full_from.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:wired_modem_full_from" + ] + }, + "criteria": { + "has_modem": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:wired_modem" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:wired_modem_full_from" + } + } + }, + "requirements": [ + [ + "has_modem", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/wired_modem_full_to.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/wired_modem_full_to.json new file mode 100644 index 000000000..c0eaec110 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/wired_modem_full_to.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:wired_modem_full_to" + ] + }, + "criteria": { + "has_modem": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:wired_modem" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:wired_modem_full_to" + } + } + }, + "requirements": [ + [ + "has_modem", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/wireless_modem_advanced.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/wireless_modem_advanced.json new file mode 100644 index 000000000..5941483e3 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/wireless_modem_advanced.json @@ -0,0 +1,43 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:wireless_modem_advanced" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_wireless": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "item": "computercraft:wireless_modem_normal" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:wireless_modem_advanced" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_wireless", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/advancements/recipes/computercraft/wireless_modem_normal.json b/src/main/resources/data/computercraft/advancements/recipes/computercraft/wireless_modem_normal.json new file mode 100644 index 000000000..b0ce1c994 --- /dev/null +++ b/src/main/resources/data/computercraft/advancements/recipes/computercraft/wireless_modem_normal.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "computercraft:wireless_modem_normal" + ] + }, + "criteria": { + "has_computer": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "tag": "computercraft:computer" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "computercraft:wireless_modem_normal" + } + } + }, + "requirements": [ + [ + "has_computer", + "has_the_recipe" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/loot_tables/treasure_disk.json b/src/main/resources/data/computercraft/loot_tables/treasure_disk.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/main/resources/data/computercraft/loot_tables/treasure_disk.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/completion.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/completion.lua new file mode 100644 index 000000000..2ae2e4966 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/completion.lua @@ -0,0 +1,105 @@ +--- A collection of helper methods for working with input completion, such +-- as that require by @{read}. +-- +-- @module cc.completion +-- @see cc.shell.completion For additional helpers to use with +-- @{shell.setCompletionFunction}. + +local expect = require "cc.expect".expect + +local function choice_impl(text, choices, add_space) + local results = {} + for n = 1, #choices do + local option = choices[n] + if #option + (add_space and 1 or 0) > #text and option:sub(1, #text) == text then + local result = option:sub(#text + 1) + if add_space then + table.insert(results, result .. " ") + else + table.insert(results, result) + end + end + end + return results +end + +--- Complete from a choice of one or more strings. +-- +-- @tparam string text The input string to complete. +-- @tparam { string... } choices The list of choices to complete from. +-- @tparam[opt] boolean add_space Whether to add a space after the completed item. +-- @treturn { string... } A list of suffixes of matching strings. +-- @usage Call @{read}, completing the names of various animals. +-- +-- local animals = { "dog", "cat", "lion", "unicorn" } +-- read(nil, nil, function(text) return choice(text, animals) end) +local function choice(text, choices, add_space) + expect(1, text, "string") + expect(2, choices, "table") + expect(3, add_space, "boolean", "nil") + return choice_impl(text, choices, add_space) +end + +--- Complete the name of a currently attached peripheral. +-- +-- @tparam string text The input string to complete. +-- @tparam[opt] boolean add_space Whether to add a space after the completed name. +-- @treturn { string... } A list of suffixes of matching peripherals. +-- @usage read(nil, nil, peripheral) +local function peripheral_(text, add_space) + expect(1, text, "string") + expect(2, add_space, "boolean", "nil") + return choice_impl(text, peripheral.getNames(), add_space) +end + +local sides = redstone.getSides() + +--- Complete the side of a computer. +-- +-- @tparam string text The input string to complete. +-- @tparam[opt] boolean add_space Whether to add a space after the completed side. +-- @treturn { string... } A list of suffixes of matching sides. +-- @usage read(nil, nil, side) +local function side(text, add_space) + expect(1, text, "string") + expect(2, add_space, "boolean", "nil") + return choice_impl(text, sides, add_space) +end + +--- Complete a @{settings|setting}. +-- +-- @tparam string text The input string to complete. +-- @tparam[opt] boolean add_space Whether to add a space after the completed settings. +-- @treturn { string... } A list of suffixes of matching settings. +-- @usage read(nil, nil, setting) +local function setting(text, add_space) + expect(1, text, "string") + expect(2, add_space, "boolean", "nil") + return choice_impl(text, settings.getNames(), add_space) +end + +local command_list + +--- Complete the name of a Minecraft @{commands|command}. +-- +-- @tparam string text The input string to complete. +-- @tparam[opt] boolean add_space Whether to add a space after the completed command. +-- @treturn { string... } A list of suffixes of matching commands. +-- @usage read(nil, nil, command) +local function command(text, add_space) + expect(1, text, "string") + expect(2, add_space, "boolean", "nil") + if command_list == nil then + command_list = commands and commands.list() or {} + end + + return choice_impl(text, command_list, add_space) +end + +return { + choice = choice, + peripheral = peripheral_, + side = side, + setting = setting, + command = command, +} diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/expect.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/expect.lua new file mode 100644 index 000000000..24ca66d56 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/expect.lua @@ -0,0 +1,75 @@ +--- The @{cc.expect} library provides helper functions for verifying that +-- function arguments are well-formed and of the correct type. +-- +-- @module cc.expect + +local native_select, native_type = select, type + +local function get_type_names(...) + local types = table.pack(...) + for i = types.n, 1, -1 do + if types[i] == "nil" then table.remove(types, i) end + end + + if #types <= 1 then + return tostring(...) + else + return table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types] + end +end +--- Expect an argument to have a specific type. +-- +-- @tparam number index The 1-based argument index. +-- @param value The argument's value. +-- @tparam string ... The allowed types of the argument. +-- @return The given `value`. +-- @throws If the value is not one of the allowed types. +local function expect(index, value, ...) + local t = native_type(value) + for i = 1, native_select("#", ...) do + if t == native_select(i, ...) then return value end + end + + -- If we can determine the function name with a high level of confidence, try to include it. + local name + if native_type(debug) == "table" and native_type(debug.getinfo) == "function" then + local ok, info = pcall(debug.getinfo, 3, "nS") + if ok and info.name and #info.name ~= "" and info.what ~= "C" then name = info.name end + end + + local type_names = get_type_names(...) + if name then + error(("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3) + else + error(("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3) + end +end + +--- Expect an field to have a specific type. +-- +-- @tparam table tbl The table to index. +-- @tparam string index The field name to check. +-- @tparam string ... The allowed types of the argument. +-- @return The contents of the given field. +-- @throws If the field is not one of the allowed types. +local function field(tbl, index, ...) + expect(1, tbl, "table") + expect(2, index, "string") + + local value = tbl[index] + local t = native_type(value) + for i = 1, native_select("#", ...) do + if t == native_select(i, ...) then return value end + end + + if value == nil then + error(("field '%s' missing from table"):format(index), 3) + else + error(("bad field '%s' (expected %s, got %s)"):format(index, get_type_names(...), t), 3) + end +end + +return { + expect = expect, + field = field, +} diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua new file mode 100644 index 000000000..225abb6f7 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua @@ -0,0 +1,107 @@ +--- Provides utilities for working with "nft" images. +-- +-- nft ("Nitrogen Fingers Text") is a file format for drawing basic images. +-- Unlike the images that @{paintutils.parseImage} uses, nft supports coloured +-- text. +-- +-- @module cc.image.nft +-- @usage Load an image from `example.nft` and draw it. +-- +-- local nft = require "cc.image.nft" +-- local image = assert(nft.load("example.nft")) +-- nft.draw(image) + +local expect = require "cc.expect".expect + +--- Parse an nft image from a string. +-- +-- @tparam string image The image contents. +-- @return table The parsed image. +local function parse(image) + expect(1, image, "string") + + local result = {} + local line = 1 + local foreground = "0" + local background = "f" + + local i, len = 1, #image + while i <= len do + local c = image:sub(i, i) + if c == "\31" and i < len then + i = i + 1 + foreground = image:sub(i, i) + elseif c == "\30" and i < len then + i = i + 1 + background = image:sub(i, i) + elseif c == "\n" then + if result[line] == nil then + result[line] = { text = "", foreground = "", background = "" } + end + + line = line + 1 + else + local next = image:find("[\n\30\31]", i) or #image + 1 + local seg_len = next - i + + local this_line = result[line] + if this_line == nil then + this_line = { foreground = "", background = "", text = "" } + result[line] = this_line + end + + this_line.text = this_line.text .. image:sub(i, next - 1) + this_line.foreground = this_line.foreground .. foreground:rep(seg_len) + this_line.background = this_line.background .. background:rep(seg_len) + + i = next - 1 + end + + i = i + 1 + end + return result +end + +--- Load an nft image from a file. +-- +-- @tparam string path The file to load. +-- @treturn[1] table The parsed image. +-- @treturn[2] nil If the file does not exist or could not be loaded. +-- @treturn[2] string An error message explaining why the file could not be +-- loaded. +local function load(path) + expect(1, path, "string") + local file, err = io.open(path, "r") + if not file then return nil, err end + + local result = file:read("*a") + file:close() + return parse(result) +end + +--- Draw an nft image to the screen. +-- +-- @tparam table image An image, as returned from @{load} or @{draw}. +-- @tparam number xPos The x position to start drawing at. +-- @tparam number xPos The y position to start drawing at. +-- @tparam[opt] term.Redirect target The terminal redirect to draw to. Defaults to the +-- current terminal. +local function draw(image, xPos, yPos, target) + expect(1, image, "table") + expect(2, xPos, "number") + expect(3, yPos, "number") + expect(4, target, "table", "nil") + + if not target then target = term end + + for y, line in ipairs(image) do + target.setCursorPos(xPos, yPos + y - 1) + target.blit(line.text, line.foreground, line.background) + end +end + +return { + parse = parse, + load = load, + draw = draw, +} diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua new file mode 100644 index 000000000..b934aa995 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua @@ -0,0 +1,460 @@ +--- Provides a "pretty printer", for rendering data structures in an +-- aesthetically pleasing manner. +-- +-- In order to display something using @{cc.pretty}, you build up a series of +-- @{Doc|documents}. These behave a little bit like strings; you can concatenate +-- them together and then print them to the screen. +-- +-- However, documents also allow you to control how they should be printed. There +-- are several functions (such as @{nest} and @{group}) which allow you to control +-- the "layout" of the document. When you come to display the document, the 'best' +-- (most compact) layout is used. +-- +-- @module cc.pretty +-- @usage Print a table to the terminal +-- local pretty = require "cc.pretty" +-- pretty.write(pretty.dump({ 1, 2, 3 })) +-- +-- @usage Build a custom document and display it +-- local pretty = require "cc.pretty" +-- pretty.write(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world"))) + +local expect = require "cc.expect" +local expect, field = expect.expect, expect.field + +local type, getmetatable, setmetatable, colours, str_write, tostring = type, getmetatable, setmetatable, colours, write, tostring +local debug_info = type(debug) == "table" and type(debug.getinfo) == "function" and debug.getinfo +local debug_local = type(debug) == "table" and type(debug.getlocal) == "function" and debug.getlocal + +--- @{table.insert} alternative, but with the length stored inline. +local function append(out, value) + local n = out.n + 1 + out[n], out.n = value, n +end + +--- A document containing formatted text, with multiple possible layouts. +-- +-- Documents effectively represent a sequence of strings in alternative layouts, +-- which we will try to print in the most compact form necessary. +-- +-- @type Doc +local Doc = { } + +--- An empty document. +local empty = setmetatable({ tag = "nil" }, Doc) + +--- A document with a single space in it. +local space = setmetatable({ tag = "text", text = " " }, Doc) + +--- A line break. When collapsed with @{group}, this will be replaced with @{empty}. +local line = setmetatable({ tag = "line", flat = empty }, Doc) + +--- A line break. When collapsed with @{group}, this will be replaced with @{space}. +local space_line = setmetatable({ tag = "line", flat = space }, Doc) + +local text_cache = { [""] = empty, [" "] = space, ["\n"] = space_line } + +local function mk_text(text, colour) + return text_cache[text] or setmetatable({ tag = "text", text = text, colour = colour }, Doc) +end + +--- Create a new document from a string. +-- +-- If your string contains multiple lines, @{group} will flatten the string +-- into a single line, with spaces between each line. +-- +-- @tparam string text The string to construct a new document with. +-- @tparam[opt] number colour The colour this text should be printed with. If not given, we default to the current +-- colour. +-- @treturn Doc The document with the provided text. +local function text(text, colour) + expect(1, text, "string") + expect(2, colour, "number", "nil") + + local cached = text_cache[text] + if cached then return cached end + + local new_line = text:find("\n", 1) + if not new_line then return mk_text(text, colour) end + + -- Split the string by "\n". With a micro-optimisation to skip empty strings. + local doc = setmetatable({ tag = "concat", n = 0 }, Doc) + if new_line ~= 1 then append(doc, mk_text(text:sub(1, new_line - 1), colour)) end + + new_line = new_line + 1 + while true do + local next_line = text:find("\n", new_line) + append(doc, space_line) + if not next_line then + if new_line <= #text then append(doc, mk_text(text:sub(new_line), colour)) end + return doc + else + if new_line <= next_line - 1 then + append(doc, mk_text(text:sub(new_line, next_line - 1), colour)) + end + new_line = next_line + 1 + end + end +end + +--- Concatenate several documents together. This behaves very similar to string concatenation. +-- +-- @tparam Doc|string ... The documents to concatenate. +-- @treturn Doc The concatenated documents. +-- @usage pretty.concat(doc1, " - ", doc2) +-- @usage doc1 .. " - " .. doc2 +local function concat(...) + local args = table.pack(...) + for i = 1, args.n do + if type(args[i]) == "string" then args[i] = text(args[i]) end + if getmetatable(args[i]) ~= Doc then expect(i, args[i], "document") end + end + + if args.n == 0 then return empty end + if args.n == 1 then return args[1] end + + args.tag = "concat" + return setmetatable(args, Doc) +end + +Doc.__concat = concat --- @local + +--- Indent later lines of the given document with the given number of spaces. +-- +-- For instance, nesting the document +-- ```txt +-- foo +-- bar +-- ``` +-- by two spaces will produce +-- ```txt +-- foo +-- bar +-- ``` +-- +-- @tparam number depth The number of spaces with which the document should be indented. +-- @tparam Doc doc The document to indent. +-- @treturn Doc The nested document. +-- @usage pretty.nest(2, pretty.text("foo\nbar")) +local function nest(depth, doc) + expect(1, depth, "number") + if getmetatable(doc) ~= Doc then expect(2, doc, "document") end + if depth <= 0 then error("depth must be a positive number", 2) end + + return setmetatable({ tag = "nest", depth = depth, doc }, Doc) +end + +local function flatten(doc) + if doc.flat then return doc.flat end + + local kind = doc.tag + if kind == "nil" or kind == "text" then + return doc + elseif kind == "concat" then + local out = setmetatable({ tag = "concat", n = doc.n }, Doc) + for i = 1, doc.n do out[i] = flatten(doc[i]) end + doc.flat, out.flat = out, out -- cache the flattened node + return out + elseif kind == "nest" then + return flatten(doc[1]) + elseif kind == "group" then + return doc[1] + else + error("Unknown doc " .. kind) + end +end + +--- Builds a document which is displayed on a single line if there is enough +-- room, or as normal if not. +-- +-- @tparam Doc doc The document to group. +-- @treturn Doc The grouped document. +local function group(doc) + if getmetatable(doc) ~= Doc then expect(1, doc, "document") end + + if doc.tag == "group" then return doc end -- Skip if already grouped. + + local flattened = flatten(doc) + if flattened == doc then return doc end -- Also skip if flattening does nothing. + return setmetatable({ tag = "group", flattened, doc }, Doc) +end + +local function get_remaining(doc, width) + local kind = doc.tag + if kind == "nil" or kind == "line" then + return width + elseif kind == "text" then + return width - #doc.text + elseif kind == "concat" then + for i = 1, doc.n do + width = get_remaining(doc[i], width) + if width < 0 then break end + end + return width + elseif kind == "group" or kind == "nest" then + return get_remaining(kind[1]) + else + error("Unknown doc " .. kind) + end +end + +--- Display a document on the terminal. +-- +-- @tparam Doc doc The document to render +-- @tparam[opt] number ribbon_frac The maximum fraction of the width that we should write in. +local function write(doc, ribbon_frac) + if getmetatable(doc) ~= Doc then expect(1, doc, "document") end + expect(2, ribbon_frac, "number", "nil") + + local term = term + local width, height = term.getSize() + local ribbon_width = (ribbon_frac or 0.6) * width + if ribbon_width < 0 then ribbon_width = 0 end + if ribbon_width > width then ribbon_width = width end + + local def_colour = term.getTextColour() + local current_colour = def_colour + + local function go(doc, indent, col) + local kind = doc.tag + if kind == "nil" then + return col + elseif kind == "text" then + local doc_colour = doc.colour or def_colour + if doc_colour ~= current_colour then + term.setTextColour(doc_colour) + current_colour = doc_colour + end + + str_write(doc.text) + + return col + #doc.text + elseif kind == "line" then + local _, y = term.getCursorPos() + if y < height then + term.setCursorPos(indent + 1, y + 1) + else + term.scroll(1) + term.setCursorPos(indent + 1, height) + end + + return indent + elseif kind == "concat" then + for i = 1, doc.n do col = go(doc[i], indent, col) end + return col + elseif kind == "nest" then + return go(doc[1], indent + doc.depth, col) + elseif kind == "group" then + if get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then + return go(doc[1], indent, col) + else + return go(doc[2], indent, col) + end + else + error("Unknown doc " .. kind) + end + end + + local col = math.max(term.getCursorPos() - 1, 0) + go(doc, 0, col) + if current_colour ~= def_colour then term.setTextColour(def_colour) end +end + +--- Display a document on the terminal with a trailing new line. +-- +-- @tparam Doc doc The document to render. +-- @tparam[opt] number ribbon_frac The maximum fraction of the width that we should write in. +local function print(doc, ribbon_frac) + if getmetatable(doc) ~= Doc then expect(1, doc, "document") end + expect(2, ribbon_frac, "number", "nil") + write(doc, ribbon_frac) + str_write("\n") +end + +--- Render a document, converting it into a string. +-- +-- @tparam Doc doc The document to render. +-- @tparam[opt] number width The maximum width of this document. Note that long strings will not be wrapped to +-- fit this width - it is only used for finding the best layout. +-- @tparam[opt] number ribbon_frac The maximum fraction of the width that we should write in. +-- @treturn string The rendered document as a string. +local function render(doc, width, ribbon_frac) + if getmetatable(doc) ~= Doc then expect(1, doc, "document") end + expect(2, width, "number", "nil") + expect(3, ribbon_frac, "number", "nil") + + local ribbon_width + if width then + ribbon_width = (ribbon_frac or 0.6) * width + if ribbon_width < 0 then ribbon_width = 0 end + if ribbon_width > width then ribbon_width = width end + end + + local out = { n = 0 } + local function go(doc, indent, col) + local kind = doc.tag + if kind == "nil" then + return col + elseif kind == "text" then + append(out, doc.text) + return col + #doc.text + elseif kind == "line" then + append(out, "\n" .. (" "):rep(indent)) + return indent + elseif kind == "concat" then + for i = 1, doc.n do col = go(doc[i], indent, col) end + return col + elseif kind == "nest" then + return go(doc[1], indent + doc.depth, col) + elseif kind == "group" then + if not width or get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then + return go(doc[1], indent, col) + else + return go(doc[2], indent, col) + end + else + error("Unknown doc " .. kind) + end + end + + go(doc, 0, 0) + return table.concat(out, "", 1, out.n) +end + +Doc.__tostring = render --- @local + +local keywords = { + ["and"] = true, ["break"] = true, ["do"] = true, ["else"] = true, + ["elseif"] = true, ["end"] = true, ["false"] = true, ["for"] = true, + ["function"] = true, ["if"] = true, ["in"] = true, ["local"] = true, + ["nil"] = true, ["not"] = true, ["or"] = true, ["repeat"] = true, ["return"] = true, + ["then"] = true, ["true"] = true, ["until"] = true, ["while"] = true, + } + +local comma = text(",") +local braces = text("{}") +local obrace, cbrace = text("{"), text("}") +local obracket, cbracket = text("["), text("] = ") + +local function key_compare(a, b) + local ta, tb = type(a), type(b) + + if ta == "string" then return tb ~= "string" or a < b + elseif tb == "string" then return false + end + + if ta == "number" then return tb ~= "number" or a < b end + return false +end + +local function show_function(fn, options) + local info = debug_info and debug_info(fn, "Su") + + -- Include function source position if available + local name + if options.function_source and info and info.short_src and info.linedefined and info.linedefined >= 1 then + name = "function<" .. info.short_src .. ":" .. info.linedefined .. ">" + else + name = tostring(fn) + end + + -- Include arguments if a Lua function and if available. Lua will report "C" + -- functions as variadic. + if options.function_args and info and info.what == "Lua" and info.nparams and debug_local then + local args = {} + for i = 1, info.nparams do args[i] = debug_local(fn, i) or "?" end + if info.isvararg then args[#args + 1] = "..." end + name = name .. "(" .. table.concat(args, ", ") .. ")" + end + + return name +end + +local function pretty_impl(obj, options, tracking) + local obj_type = type(obj) + if obj_type == "string" then + local formatted = ("%q"):format(obj):gsub("\\\n", "\\n") + return text(formatted, colours.red) + elseif obj_type == "number" then + return text(tostring(obj), colours.magenta) + elseif obj_type == "function" then + return text(show_function(obj, options), colours.lightGrey) + elseif obj_type ~= "table" or tracking[obj] then + return text(tostring(obj), colours.lightGrey) + elseif getmetatable(obj) ~= nil and getmetatable(obj).__tostring then + return text(tostring(obj)) + elseif next(obj) == nil then + return braces + else + tracking[obj] = true + local doc = setmetatable({ tag = "concat", n = 1, space_line }, Doc) + + local length, keys, keysn = #obj, {}, 1 + for k in pairs(obj) do keys[keysn], keysn = k, keysn + 1 end + table.sort(keys, key_compare) + + for i = 1, keysn - 1 do + if i > 1 then append(doc, comma) append(doc, space_line) end + + local k = keys[i] + local v = obj[k] + local ty = type(k) + if ty == "number" and k % 1 == 0 and k >= 1 and k <= length then + append(doc, pretty_impl(v, options, tracking)) + elseif ty == "string" and not keywords[k] and k:match("^[%a_][%a%d_]*$") then + append(doc, text(k .. " = ")) + append(doc, pretty_impl(v, options, tracking)) + else + append(doc, obracket) + append(doc, pretty_impl(k, options, tracking)) + append(doc, cbracket) + append(doc, pretty_impl(v, options, tracking)) + end + end + + tracking[obj] = nil + return group(concat(obrace, nest(2, concat(table.unpack(doc, 1, doc.n))), space_line, cbrace)) + end +end + +--- Pretty-print an arbitrary object, converting it into a document. +-- +-- This can then be rendered with @{write} or @{print}. +-- +-- @param obj The object to pretty-print. +-- @tparam[opt] { function_args = boolean, function_source = boolean } options +-- Controls how various properties are displayed. +-- - `function_args`: Show the arguments to a function if known (`false` by default). +-- - `function_source`: Show where the function was defined, instead of +-- `function: xxxxxxxx` (`false` by default). +-- @treturn Doc The object formatted as a document. +-- @usage Display a table on the screen +-- local pretty = require "cc.pretty" +-- pretty.print(pretty.pretty({ 1, 2, 3 })) +local function pretty(obj, options) + expect(2, options, "table", "nil") + options = options or {} + + local actual_options = { + function_source = field(options, "function_source", "boolean", "nil") or false, + function_args = field(options, "function_args", "boolean", "nil") or false, + } + return pretty_impl(obj, actual_options, {}) +end + +return { + empty = empty, + space = space, + line = line, + space_line = space_line, + text = text, + concat = concat, + nest = nest, + group = group, + + write = write, + print = print, + render = render, + + pretty = pretty, +} diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/require.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/require.lua new file mode 100644 index 000000000..a90e189b8 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/require.lua @@ -0,0 +1,121 @@ +--- This provides a pure Lua implementation of the builtin @{require} function +-- and @{package} library. +-- +-- Generally you do not need to use this module - it is injected into the +-- every program's environment. However, it may be useful when building a +-- custom shell or when running programs yourself. +-- +-- @module cc.require +-- @usage Construct the package and require function, and insert them into a +-- custom environment. +-- +-- local env = setmetatable({}, { __index = _ENV }) +-- local r = require "cc.require" +-- env.require, env.package = r.make(env, "/") + +local expect = require and require("cc.expect") or dofile("rom/modules/main/cc/expect.lua") +local expect = expect.expect + +local function preload(package) + return function(name) + if package.preload[name] then + return package.preload[name] + else + return nil, "no field package.preload['" .. name .. "']" + end + end +end + +local function from_file(package, env, dir) + return function(name) + local fname = string.gsub(name, "%.", "/") + local sError = "" + for pattern in string.gmatch(package.path, "[^;]+") do + local sPath = string.gsub(pattern, "%?", fname) + if sPath:sub(1, 1) ~= "/" then + sPath = fs.combine(dir, sPath) + end + if fs.exists(sPath) and not fs.isDir(sPath) then + local fnFile, sError = loadfile(sPath, nil, env) + if fnFile then + return fnFile, sPath + else + return nil, sError + end + else + if #sError > 0 then + sError = sError .. "\n " + end + sError = sError .. "no file '" .. sPath .. "'" + end + end + return nil, sError + end +end + +local function make_require(package) + local sentinel = {} + return function(name) + expect(1, name, "string") + + if package.loaded[name] == sentinel then + error("loop or previous error loading module '" .. name .. "'", 0) + end + + if package.loaded[name] then + return package.loaded[name] + end + + local sError = "module '" .. name .. "' not found:" + for _, searcher in ipairs(package.loaders) do + local loader = table.pack(searcher(name)) + if loader[1] then + package.loaded[name] = sentinel + local result = loader[1](name, table.unpack(loader, 2, loader.n)) + if result == nil then result = true end + + package.loaded[name] = result + return result + else + sError = sError .. "\n " .. loader[2] + end + end + error(sError, 2) + end +end + +--- Build an implementation of Lua's @{package} library, and a @{require} +-- function to load modules within it. +-- +-- @tparam table env The environment to load packages into. +-- @tparam string dir The directory that relative packages are loaded from. +-- @treturn function The new @{require} function. +-- @treturn table The new @{package} library. +local function make_package(env, dir) + expect(1, env, "table") + expect(2, dir, "string") + + local package = {} + package.loaded = { + _G = _G, + bit32 = bit32, + coroutine = coroutine, + math = math, + package = package, + string = string, + table = table, + } + package.path = "?;?.lua;?/init.lua;/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua" + if turtle then + package.path = package.path .. ";/rom/modules/turtle/?;/rom/modules/turtle/?.lua;/rom/modules/turtle/?/init.lua" + elseif commands then + package.path = package.path .. ";/rom/modules/command/?;/rom/modules/command/?.lua;/rom/modules/command/?/init.lua" + end + package.config = "/\n;\n?\n!\n-" + package.preload = {} + package.loaders = { preload(package), from_file(package, env, dir) } + + return make_require(package), package +end + +return { make = make_package } diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua new file mode 100644 index 000000000..91d9ea7a4 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua @@ -0,0 +1,152 @@ +--- A collection of helper methods for working with shell completion. +-- +-- Most programs may be completed using the @{build} helper method, rather than +-- manually switching on the argument index. +-- +-- Note, the helper functions within this module do not accept an argument index, +-- and so are not directly usable with the @{shell.setCompletionFunction}. Instead, +-- wrap them using @{build}, or your own custom function. +-- +-- @module cc.shell.completion +-- @see cc.completion For more general helpers, suitable for use with @{read}. +-- @see shell.setCompletionFunction + +local expect = require "cc.expect".expect +local completion = require "cc.completion" + +--- Complete the name of a file relative to the current working directory. +-- +-- @tparam table shell The shell we're completing in +-- @tparam { string... } choices The list of choices to complete from. +-- @treturn { string... } A list of suffixes of matching files. +local function file(shell, text) + return fs.complete(text, shell.dir(), true, false) +end + +--- Complete the name of a directory relative to the current working directory. +-- +-- @tparam table shell The shell we're completing in +-- @tparam { string... } choices The list of choices to complete from. +-- @treturn { string... } A list of suffixes of matching directories. +local function dir(shell, text) + return fs.complete(text, shell.dir(), false, true) +end + +--- Complete the name of a file or directory relative to the current working +-- directory. +-- +-- @tparam table shell The shell we're completing in +-- @tparam { string... } choices The list of choices to complete from. +-- @tparam { string... } previous The shell arguments before this one. +-- @tparam[opt] boolean add_space Whether to add a space after the completed item. +-- @treturn { string... } A list of suffixes of matching files and directories. +local function dirOrFile(shell, text, previous, add_space) + local results = fs.complete(text, shell.dir(), true, true) + if add_space then + for n = 1, #results do + local result = results[n] + if result:sub(-1) ~= "/" then + results[n] = result .. " " + end + end + end + return results +end + +local function wrap(func) + return function(shell, text, previous, ...) + return func(text, ...) + end +end + +--- Complete the name of a program. +-- +-- @tparam table shell The shell we're completing in +-- @tparam { string... } choices The list of choices to complete from. +-- @treturn { string... } A list of suffixes of matching programs. +-- @see shell.completeProgram +local function program(shell, text) + return shell.completeProgram(text) +end + +--- A helper function for building shell completion arguments. +-- +-- This accepts a series of single-argument completion functions, and combines +-- them into a function suitable for use with @{shell.setCompletionFunction}. +-- +-- @tparam nil|table|function ... Every argument to @{build} represents an argument +-- to the program you wish to complete. Each argument can be one of three types: +-- +-- - `nil`: This argument will not be completed. +-- +-- - A function: This argument will be completed with the given function. It is +-- called with the @{shell} object, the string to complete and the arguments +-- before this one. +-- +-- - A table: This acts as a more powerful version of the function case. The table +-- must have a function as the first item - this will be called with the shell, +-- string and preceding arguments as above, but also followed by any additional +-- items in the table. This provides a more convenient interface to pass +-- options to your completion functions. +-- +-- If this table is the last argument, it may also set the `many` key to true, +-- which states this function should be used to complete any remaining arguments. +-- +-- @usage Prompt for a choice of options, followed by a directory, and then multiple +-- files. +-- +-- complete.build( +-- { complete.choice, { "get", "put" } }, +-- complete.dir, +-- { complete.file, many = true } +-- ) +local function build(...) + local arguments = table.pack(...) + for i = 1, arguments.n do + local arg = arguments[i] + if arg ~= nil then + expect(i, arg, "table", "function") + if type(arg) == "function" then + arg = { arg } + arguments[i] = arg + end + + if type(arg[1]) ~= "function" then + error(("Bad table entry #1 at argument #%d (expected function, got %s)"):format(i, type(arg[1])), 2) + end + + if arg.many and i < arguments.n then + error(("Unexpected 'many' field on argument #%d (should only occur on the last argument)"):format(i), 2) + end + end + end + + return function(shell, index, text, previous) + local arg = arguments[index] + if not arg then + if index <= arguments.n then return end + + arg = arguments[arguments.n] + if not arg or not arg.many then return end + end + + return arg[1](shell, text, previous, table.unpack(arg, 2)) + end +end + +return { + file = file, + dir = dir, + dirOrFile = dirOrFile, + program = program, + + -- Re-export various other functions + help = wrap(help.completeTopic), --- Wraps @{help.completeTopic} as a @{build} compatible function. + choice = wrap(completion.choice), --- Wraps @{cc.completion.choice} as a @{build} compatible function. + peripheral = wrap(completion.peripheral), --- Wraps @{cc.completion.peripheral} as a @{build} compatible function. + side = wrap(completion.side), --- Wraps @{cc.completion.side} as a @{build} compatible function. + setting = wrap(completion.setting), --- Wraps @{cc.completion.setting} as a @{build} compatible function. + command = wrap(completion.command), --- Wraps @{cc.completion.command} as a @{build} compatible function. + + build = build, +} diff --git a/src/main/resources/data/computercraft/lua/rom/motd.txt b/src/main/resources/data/computercraft/lua/rom/motd.txt new file mode 100644 index 000000000..8c8f16945 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/motd.txt @@ -0,0 +1,23 @@ +Please report bugs at https://github.com/SquidDev-CC/CC-Tweaked. Thanks! +View the documentation at https://wiki.computercraft.cc +Show off your programs or ask for help at our forum: https://forums.computercraft.cc +You can disable these messages by running "set motd.enable false". +Use "pastebin put" to upload a program to pastebin. +Use the "edit" program to create and edit your programs. +You can use "wget" to download a file from the internet. +On an advanced computer you can use "fg" or "bg" to run multiple programs at the same time. +Use an advanced computer to use colours and the mouse. +With a speaker you can play sounds. +Programs that are placed in the "startup" folder in the root of a computer are started on boot. +Use a modem to connect with other computers. +With the "gps" program you can get the position of a computer. +Use "monitor" to run a program on a attached monitor. +Don't forget to label your computer with "label set". +Feeling creative? Use a printer to print a book! +Files beginning with a "." are hidden from "list" by default. +Running "set" lists the current values of all settings. +Some programs are only available on advanced computers, turtles, pocket computers or command computers. +The "equip" programs let you add upgrades to a turtle without crafting. +You can change the color of a disk by crafting or right clicking it with dye. +You can print on a printed page again to get multiple colors. +Holding the Ctrl and T keys terminates the running program. diff --git a/src/main/resources/data/computercraft/lua/rom/programs/motd.lua b/src/main/resources/data/computercraft/lua/rom/programs/motd.lua new file mode 100644 index 000000000..c8c75a40b --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/programs/motd.lua @@ -0,0 +1,15 @@ +local tMotd = {} + +for sPath in string.gmatch(settings.get("motd.path"), "[^:]+") do + if fs.exists(sPath) then + for sLine in io.lines(sPath) do + table.insert(tMotd, sLine) + end + end +end + +if #tMotd == 0 then + print("missingno") +else + print(tMotd[math.random(1, #tMotd)]) +end diff --git a/src/main/resources/data/computercraft/recipes/disk_1.json b/src/main/resources/data/computercraft/recipes/disk_1.json new file mode 100644 index 000000000..66ce73a90 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_1.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:black_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:1118481}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_10.json b/src/main/resources/data/computercraft/recipes/disk_10.json new file mode 100644 index 000000000..9c064f0fb --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_10.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:pink_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:15905484}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_11.json b/src/main/resources/data/computercraft/recipes/disk_11.json new file mode 100644 index 000000000..bfd6b21dc --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_11.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:lime_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:8375321}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_12.json b/src/main/resources/data/computercraft/recipes/disk_12.json new file mode 100644 index 000000000..7fa0ff4be --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_12.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:yellow_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:14605932}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_13.json b/src/main/resources/data/computercraft/recipes/disk_13.json new file mode 100644 index 000000000..ab237e75c --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_13.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:light_blue_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:10072818}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_14.json b/src/main/resources/data/computercraft/recipes/disk_14.json new file mode 100644 index 000000000..458ad2a71 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_14.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:magenta_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:15040472}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_15.json b/src/main/resources/data/computercraft/recipes/disk_15.json new file mode 100644 index 000000000..35147838e --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_15.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:orange_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:15905331}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_16.json b/src/main/resources/data/computercraft/recipes/disk_16.json new file mode 100644 index 000000000..1f86b573a --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_16.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:white_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:15790320}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_2.json b/src/main/resources/data/computercraft/recipes/disk_2.json new file mode 100644 index 000000000..40f55f5a4 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_2.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:red_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:13388876}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_3.json b/src/main/resources/data/computercraft/recipes/disk_3.json new file mode 100644 index 000000000..5ed4b4d72 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_3.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:green_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:5744206}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_4.json b/src/main/resources/data/computercraft/recipes/disk_4.json new file mode 100644 index 000000000..ecc80c9e2 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_4.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:brown_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:8349260}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_5.json b/src/main/resources/data/computercraft/recipes/disk_5.json new file mode 100644 index 000000000..94beac68a --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_5.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:blue_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:3368652}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_6.json b/src/main/resources/data/computercraft/recipes/disk_6.json new file mode 100644 index 000000000..0416fba6e --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_6.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:purple_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:11691749}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_7.json b/src/main/resources/data/computercraft/recipes/disk_7.json new file mode 100644 index 000000000..99f64b544 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_7.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:cyan_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:5020082}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_8.json b/src/main/resources/data/computercraft/recipes/disk_8.json new file mode 100644 index 000000000..be1b7fea9 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_8.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:light_gray_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:10066329}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/disk_9.json b/src/main/resources/data/computercraft/recipes/disk_9.json new file mode 100644 index 000000000..48620dbdd --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/disk_9.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shapeless", + "group": "computercraft:disk", + "ingredients": [ + { + "tag": "forge:dusts/redstone" + }, + { + "item": "minecraft:paper" + }, + { + "item": "minecraft:gray_dye" + } + ], + "result": { + "item": "computercraft:disk", + "nbt": "{color:5000268}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_advanced/computercraft/speaker.json b/src/main/resources/data/computercraft/recipes/pocket_advanced/computercraft/speaker.json new file mode 100644 index 000000000..350a5019b --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_advanced/computercraft/speaker.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_advanced", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_advanced" + }, + "P": { + "item": "computercraft:speaker" + } + }, + "result": { + "item": "computercraft:pocket_computer_advanced" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_advanced/computercraft/wireless_modem_advanced.json b/src/main/resources/data/computercraft/recipes/pocket_advanced/computercraft/wireless_modem_advanced.json new file mode 100644 index 000000000..8896bd062 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_advanced/computercraft/wireless_modem_advanced.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_advanced", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_advanced" + }, + "P": { + "item": "computercraft:wireless_modem_advanced" + } + }, + "result": { + "item": "computercraft:pocket_computer_advanced" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_advanced/computercraft/wireless_modem_normal.json b/src/main/resources/data/computercraft/recipes/pocket_advanced/computercraft/wireless_modem_normal.json new file mode 100644 index 000000000..9e007b1cd --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_advanced/computercraft/wireless_modem_normal.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_advanced", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_advanced" + }, + "P": { + "item": "computercraft:wireless_modem_normal" + } + }, + "result": { + "item": "computercraft:pocket_computer_advanced" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/crafting_table.json b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/crafting_table.json new file mode 100644 index 000000000..0cd065680 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/crafting_table.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_advanced", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_advanced" + }, + "P": { + "item": "minecraft:crafting_table" + } + }, + "result": { + "item": "computercraft:pocket_computer_advanced" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_axe.json b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_axe.json new file mode 100644 index 000000000..7b8fa8a24 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_axe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_advanced", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_advanced" + }, + "P": { + "item": "minecraft:diamond_axe" + } + }, + "result": { + "item": "computercraft:pocket_computer_advanced" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_hoe.json b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_hoe.json new file mode 100644 index 000000000..9514a59cd --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_hoe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_advanced", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_advanced" + }, + "P": { + "item": "minecraft:diamond_hoe" + } + }, + "result": { + "item": "computercraft:pocket_computer_advanced" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_pickaxe.json b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_pickaxe.json new file mode 100644 index 000000000..e28008623 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_pickaxe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_advanced", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_advanced" + }, + "P": { + "item": "minecraft:diamond_pickaxe" + } + }, + "result": { + "item": "computercraft:pocket_computer_advanced" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_shovel.json b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_shovel.json new file mode 100644 index 000000000..b0ddf3016 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_shovel.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_advanced", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_advanced" + }, + "P": { + "item": "minecraft:diamond_shovel" + } + }, + "result": { + "item": "computercraft:pocket_computer_advanced" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_sword.json b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_sword.json new file mode 100644 index 000000000..cea0b2a0f --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_advanced/minecraft/diamond_sword.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_advanced", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_advanced" + }, + "P": { + "item": "minecraft:diamond_sword" + } + }, + "result": { + "item": "computercraft:pocket_computer_advanced" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_normal/computercraft/speaker.json b/src/main/resources/data/computercraft/recipes/pocket_normal/computercraft/speaker.json new file mode 100644 index 000000000..b5fb937a4 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_normal/computercraft/speaker.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_normal", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_normal" + }, + "P": { + "item": "computercraft:speaker" + } + }, + "result": { + "item": "computercraft:pocket_computer_normal" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_normal/computercraft/wireless_modem_advanced.json b/src/main/resources/data/computercraft/recipes/pocket_normal/computercraft/wireless_modem_advanced.json new file mode 100644 index 000000000..d8e9518e4 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_normal/computercraft/wireless_modem_advanced.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_normal", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_normal" + }, + "P": { + "item": "computercraft:wireless_modem_advanced" + } + }, + "result": { + "item": "computercraft:pocket_computer_normal" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_normal/computercraft/wireless_modem_normal.json b/src/main/resources/data/computercraft/recipes/pocket_normal/computercraft/wireless_modem_normal.json new file mode 100644 index 000000000..736753136 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_normal/computercraft/wireless_modem_normal.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_normal", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_normal" + }, + "P": { + "item": "computercraft:wireless_modem_normal" + } + }, + "result": { + "item": "computercraft:pocket_computer_normal" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/crafting_table.json b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/crafting_table.json new file mode 100644 index 000000000..dc1ddd884 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/crafting_table.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_normal", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_normal" + }, + "P": { + "item": "minecraft:crafting_table" + } + }, + "result": { + "item": "computercraft:pocket_computer_normal" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_axe.json b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_axe.json new file mode 100644 index 000000000..686d9d890 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_axe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_normal", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_normal" + }, + "P": { + "item": "minecraft:diamond_axe" + } + }, + "result": { + "item": "computercraft:pocket_computer_normal" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_hoe.json b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_hoe.json new file mode 100644 index 000000000..905a2640a --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_hoe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_normal", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_normal" + }, + "P": { + "item": "minecraft:diamond_hoe" + } + }, + "result": { + "item": "computercraft:pocket_computer_normal" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_pickaxe.json b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_pickaxe.json new file mode 100644 index 000000000..200d15862 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_pickaxe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_normal", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_normal" + }, + "P": { + "item": "minecraft:diamond_pickaxe" + } + }, + "result": { + "item": "computercraft:pocket_computer_normal" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_shovel.json b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_shovel.json new file mode 100644 index 000000000..fcae256c7 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_shovel.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_normal", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_normal" + }, + "P": { + "item": "minecraft:diamond_shovel" + } + }, + "result": { + "item": "computercraft:pocket_computer_normal" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_sword.json b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_sword.json new file mode 100644 index 000000000..564dbf7e5 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/pocket_normal/minecraft/diamond_sword.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:pocket_normal", + "pattern": [ + "#", + "P" + ], + "key": { + "#": { + "item": "computercraft:pocket_computer_normal" + }, + "P": { + "item": "minecraft:diamond_sword" + } + }, + "result": { + "item": "computercraft:pocket_computer_normal" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced/computercraft/speaker.json b/src/main/resources/data/computercraft/recipes/turtle_advanced/computercraft/speaker.json new file mode 100644 index 000000000..6526349af --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_advanced/computercraft/speaker.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_advanced", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_advanced" + }, + "T": { + "item": "computercraft:speaker" + } + }, + "result": { + "item": "computercraft:turtle_advanced", + "nbt": "{RightUpgrade:\"computercraft:speaker\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced/computercraft/wireless_modem_advanced.json b/src/main/resources/data/computercraft/recipes/turtle_advanced/computercraft/wireless_modem_advanced.json new file mode 100644 index 000000000..2aea48cae --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_advanced/computercraft/wireless_modem_advanced.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_advanced", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_advanced" + }, + "T": { + "item": "computercraft:wireless_modem_advanced" + } + }, + "result": { + "item": "computercraft:turtle_advanced", + "nbt": "{RightUpgrade:\"computercraft:wireless_modem_advanced\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced/computercraft/wireless_modem_normal.json b/src/main/resources/data/computercraft/recipes/turtle_advanced/computercraft/wireless_modem_normal.json new file mode 100644 index 000000000..39c434755 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_advanced/computercraft/wireless_modem_normal.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_advanced", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_advanced" + }, + "T": { + "item": "computercraft:wireless_modem_normal" + } + }, + "result": { + "item": "computercraft:turtle_advanced", + "nbt": "{RightUpgrade:\"computercraft:wireless_modem_normal\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/crafting_table.json b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/crafting_table.json new file mode 100644 index 000000000..f7178ff09 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/crafting_table.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_advanced", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_advanced" + }, + "T": { + "item": "minecraft:crafting_table" + } + }, + "result": { + "item": "computercraft:turtle_advanced", + "nbt": "{RightUpgrade:\"minecraft:crafting_table\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_axe.json b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_axe.json new file mode 100644 index 000000000..ab2831b56 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_axe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_advanced", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_advanced" + }, + "T": { + "item": "minecraft:diamond_axe" + } + }, + "result": { + "item": "computercraft:turtle_advanced", + "nbt": "{RightUpgrade:\"minecraft:diamond_axe\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_hoe.json b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_hoe.json new file mode 100644 index 000000000..13c2f694c --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_hoe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_advanced", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_advanced" + }, + "T": { + "item": "minecraft:diamond_hoe" + } + }, + "result": { + "item": "computercraft:turtle_advanced", + "nbt": "{RightUpgrade:\"minecraft:diamond_hoe\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_pickaxe.json b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_pickaxe.json new file mode 100644 index 000000000..3f3763457 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_pickaxe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_advanced", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_advanced" + }, + "T": { + "item": "minecraft:diamond_pickaxe" + } + }, + "result": { + "item": "computercraft:turtle_advanced", + "nbt": "{RightUpgrade:\"minecraft:diamond_pickaxe\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_shovel.json b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_shovel.json new file mode 100644 index 000000000..248f62209 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_shovel.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_advanced", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_advanced" + }, + "T": { + "item": "minecraft:diamond_shovel" + } + }, + "result": { + "item": "computercraft:turtle_advanced", + "nbt": "{RightUpgrade:\"minecraft:diamond_shovel\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_sword.json b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_sword.json new file mode 100644 index 000000000..8cab2a90b --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_advanced/minecraft/diamond_sword.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_advanced", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_advanced" + }, + "T": { + "item": "minecraft:diamond_sword" + } + }, + "result": { + "item": "computercraft:turtle_advanced", + "nbt": "{RightUpgrade:\"minecraft:diamond_sword\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_normal/computercraft/speaker.json b/src/main/resources/data/computercraft/recipes/turtle_normal/computercraft/speaker.json new file mode 100644 index 000000000..28f425aec --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_normal/computercraft/speaker.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_normal", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_normal" + }, + "T": { + "item": "computercraft:speaker" + } + }, + "result": { + "item": "computercraft:turtle_normal", + "nbt": "{RightUpgrade:\"computercraft:speaker\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_normal/computercraft/wireless_modem_advanced.json b/src/main/resources/data/computercraft/recipes/turtle_normal/computercraft/wireless_modem_advanced.json new file mode 100644 index 000000000..3af190a52 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_normal/computercraft/wireless_modem_advanced.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_normal", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_normal" + }, + "T": { + "item": "computercraft:wireless_modem_advanced" + } + }, + "result": { + "item": "computercraft:turtle_normal", + "nbt": "{RightUpgrade:\"computercraft:wireless_modem_advanced\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_normal/computercraft/wireless_modem_normal.json b/src/main/resources/data/computercraft/recipes/turtle_normal/computercraft/wireless_modem_normal.json new file mode 100644 index 000000000..f387a143d --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_normal/computercraft/wireless_modem_normal.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_normal", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_normal" + }, + "T": { + "item": "computercraft:wireless_modem_normal" + } + }, + "result": { + "item": "computercraft:turtle_normal", + "nbt": "{RightUpgrade:\"computercraft:wireless_modem_normal\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/crafting_table.json b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/crafting_table.json new file mode 100644 index 000000000..8e8a1dcc9 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/crafting_table.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_normal", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_normal" + }, + "T": { + "item": "minecraft:crafting_table" + } + }, + "result": { + "item": "computercraft:turtle_normal", + "nbt": "{RightUpgrade:\"minecraft:crafting_table\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_axe.json b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_axe.json new file mode 100644 index 000000000..85a16309d --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_axe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_normal", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_normal" + }, + "T": { + "item": "minecraft:diamond_axe" + } + }, + "result": { + "item": "computercraft:turtle_normal", + "nbt": "{RightUpgrade:\"minecraft:diamond_axe\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_hoe.json b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_hoe.json new file mode 100644 index 000000000..45491a5e8 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_hoe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_normal", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_normal" + }, + "T": { + "item": "minecraft:diamond_hoe" + } + }, + "result": { + "item": "computercraft:turtle_normal", + "nbt": "{RightUpgrade:\"minecraft:diamond_hoe\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_pickaxe.json b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_pickaxe.json new file mode 100644 index 000000000..db9ef1ae6 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_pickaxe.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_normal", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_normal" + }, + "T": { + "item": "minecraft:diamond_pickaxe" + } + }, + "result": { + "item": "computercraft:turtle_normal", + "nbt": "{RightUpgrade:\"minecraft:diamond_pickaxe\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_shovel.json b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_shovel.json new file mode 100644 index 000000000..66f98c2c9 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_shovel.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_normal", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_normal" + }, + "T": { + "item": "minecraft:diamond_shovel" + } + }, + "result": { + "item": "computercraft:turtle_normal", + "nbt": "{RightUpgrade:\"minecraft:diamond_shovel\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_sword.json b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_sword.json new file mode 100644 index 000000000..c99c73c03 --- /dev/null +++ b/src/main/resources/data/computercraft/recipes/turtle_normal/minecraft/diamond_sword.json @@ -0,0 +1,19 @@ +{ + "type": "computercraft:impostor_shaped", + "group": "computercraft:turtle_normal", + "pattern": [ + "#T" + ], + "key": { + "#": { + "item": "computercraft:turtle_normal" + }, + "T": { + "item": "minecraft:diamond_sword" + } + }, + "result": { + "item": "computercraft:turtle_normal", + "nbt": "{RightUpgrade:\"minecraft:diamond_sword\"}" + } +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/tags/items/computer.json b/src/main/resources/data/computercraft/tags/items/computer.json new file mode 100644 index 000000000..bcd0e8037 --- /dev/null +++ b/src/main/resources/data/computercraft/tags/items/computer.json @@ -0,0 +1,8 @@ +{ + "replace": false, + "values": [ + "computercraft:computer_normal", + "computercraft:computer_advanced", + "computercraft:computer_command" + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/tags/items/monitor.json b/src/main/resources/data/computercraft/tags/items/monitor.json new file mode 100644 index 000000000..babaefa8b --- /dev/null +++ b/src/main/resources/data/computercraft/tags/items/monitor.json @@ -0,0 +1,7 @@ +{ + "replace": false, + "values": [ + "computercraft:monitor_normal", + "computercraft:monitor_advanced" + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/tags/items/turtle.json b/src/main/resources/data/computercraft/tags/items/turtle.json new file mode 100644 index 000000000..e4277edfe --- /dev/null +++ b/src/main/resources/data/computercraft/tags/items/turtle.json @@ -0,0 +1,7 @@ +{ + "replace": false, + "values": [ + "computercraft:turtle_normal", + "computercraft:turtle_advanced" + ] +} \ No newline at end of file diff --git a/src/main/resources/data/computercraft/tags/items/wired_modem.json b/src/main/resources/data/computercraft/tags/items/wired_modem.json new file mode 100644 index 000000000..57db1557f --- /dev/null +++ b/src/main/resources/data/computercraft/tags/items/wired_modem.json @@ -0,0 +1,7 @@ +{ + "replace": false, + "values": [ + "computercraft:wired_modem", + "computercraft:wired_modem_full" + ] +} \ No newline at end of file diff --git a/src/main/resources/data/minecraft/tags/items/piglin_loved.json b/src/main/resources/data/minecraft/tags/items/piglin_loved.json new file mode 100644 index 000000000..8eedcc427 --- /dev/null +++ b/src/main/resources/data/minecraft/tags/items/piglin_loved.json @@ -0,0 +1,10 @@ +{ + "replace": false, + "values": [ + "computercraft:computer_advanced", + "computercraft:turtle_advanced", + "computercraft:wireless_modem_advanced", + "computercraft:pocket_computer_advanced", + "computercraft:monitor_advanced" + ] +} \ No newline at end of file