diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index 7a0de01ca..8f362483a 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -16,4 +16,4 @@ RUN sudo apt-get -q update && \ sudo apt install -yq openjdk-8-jdk openjdk-16-jdk # This is so that you can use java 8 until such a time as you switch to java 16 -RUN sudo update-java-alternatives --set java-1.8.0-openjdk-amd64 +RUN sudo update-java-alternatives --set java-1.16.0-openjdk-amd64 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7665b0fa9..05679dc3c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index 107acd32c..ac1b06f93 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/remappedSrc/dan200/computercraft/ComputerCraft.java b/remappedSrc/dan200/computercraft/ComputerCraft.java new file mode 100644 index 000000000..27aa35429 --- /dev/null +++ b/remappedSrc/dan200/computercraft/ComputerCraft.java @@ -0,0 +1,138 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft; + +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.shared.ComputerCraftRegistry.ModBlocks; +import dan200.computercraft.shared.common.ColourableRecipe; +import dan200.computercraft.shared.computer.core.ClientComputerRegistry; +import dan200.computercraft.shared.computer.core.ServerComputerRegistry; +import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe; +import dan200.computercraft.shared.data.BlockNamedEntityLootCondition; +import dan200.computercraft.shared.data.HasComputerIdLootCondition; +import dan200.computercraft.shared.data.PlayerCreativeLootCondition; +import dan200.computercraft.shared.media.recipes.DiskRecipe; +import dan200.computercraft.shared.media.recipes.PrintoutRecipe; +import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; +import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe; +import dan200.computercraft.shared.proxy.ComputerCraftProxyCommon; +import dan200.computercraft.shared.turtle.recipes.TurtleRecipe; +import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe; +import dan200.computercraft.shared.util.ImpostorRecipe; +import dan200.computercraft.shared.util.ImpostorShapelessRecipe; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.client.itemgroup.FabricItemGroupBuilder; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.ResourcePackActivationType; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static dan200.computercraft.shared.ComputerCraftRegistry.ModBlocks; +import static dan200.computercraft.shared.ComputerCraftRegistry.init; + +public final class ComputerCraft implements ModInitializer +{ + public static final String MOD_ID = "computercraft"; + + // Configuration fields + 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( Arrays.asList( + AddressRule.parse( "$private", null, Action.DENY.toPartial() ), + AddressRule.parse( "*", null, Action.ALLOW.toPartial() ) + ) ); + 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 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; + + // 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 ); + + public static ItemGroup MAIN_GROUP = FabricItemGroupBuilder.build( new Identifier( MOD_ID, "main" ), () -> new ItemStack( ModBlocks.COMPUTER_NORMAL ) ); + + @Override + public void onInitialize() + { + ComputerCraftProxyCommon.init(); + Registry.register( Registry.RECIPE_SERIALIZER, new Identifier( ComputerCraft.MOD_ID, "colour" ), ColourableRecipe.SERIALIZER ); + Registry.register( Registry.RECIPE_SERIALIZER, new Identifier( ComputerCraft.MOD_ID, "computer_upgrade" ), ComputerUpgradeRecipe.SERIALIZER ); + Registry.register( Registry.RECIPE_SERIALIZER, + new Identifier( ComputerCraft.MOD_ID, "pocket_computer_upgrade" ), + PocketComputerUpgradeRecipe.SERIALIZER ); + Registry.register( Registry.RECIPE_SERIALIZER, new Identifier( ComputerCraft.MOD_ID, "disk" ), DiskRecipe.SERIALIZER ); + Registry.register( Registry.RECIPE_SERIALIZER, new Identifier( ComputerCraft.MOD_ID, "printout" ), PrintoutRecipe.SERIALIZER ); + Registry.register( Registry.RECIPE_SERIALIZER, new Identifier( ComputerCraft.MOD_ID, "turtle" ), TurtleRecipe.SERIALIZER ); + Registry.register( Registry.RECIPE_SERIALIZER, new Identifier( ComputerCraft.MOD_ID, "turtle_upgrade" ), TurtleUpgradeRecipe.SERIALIZER ); + Registry.register( Registry.RECIPE_SERIALIZER, new Identifier( ComputerCraft.MOD_ID, "impostor_shaped" ), ImpostorRecipe.SERIALIZER ); + Registry.register( Registry.RECIPE_SERIALIZER, new Identifier( ComputerCraft.MOD_ID, "impostor_shapeless" ), ImpostorShapelessRecipe.SERIALIZER ); + Registry.register( Registry.LOOT_CONDITION_TYPE, new Identifier( ComputerCraft.MOD_ID, "block_named" ), BlockNamedEntityLootCondition.TYPE ); + Registry.register( Registry.LOOT_CONDITION_TYPE, new Identifier( ComputerCraft.MOD_ID, "player_creative" ), PlayerCreativeLootCondition.TYPE ); + Registry.register( Registry.LOOT_CONDITION_TYPE, new Identifier( ComputerCraft.MOD_ID, "has_id" ), HasComputerIdLootCondition.TYPE ); + init(); + FabricLoader.getInstance().getModContainer( MOD_ID ).ifPresent( modContainer -> { + ResourceManagerHelper.registerBuiltinResourcePack( new Identifier( MOD_ID, "classic" ), modContainer, ResourcePackActivationType.NORMAL ); + ResourceManagerHelper.registerBuiltinResourcePack( new Identifier( MOD_ID, "overhaul" ), modContainer, ResourcePackActivationType.NORMAL ); + } ); + } +} diff --git a/remappedSrc/dan200/computercraft/ComputerCraftAPIImpl.java b/remappedSrc/dan200/computercraft/ComputerCraftAPIImpl.java new file mode 100644 index 000000000..5e8128645 --- /dev/null +++ b/remappedSrc/dan200/computercraft/ComputerCraftAPIImpl.java @@ -0,0 +1,204 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft; + +import dan200.computercraft.api.ComputerCraftAPI.IComputerCraftAPI; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.GenericSource; +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.asm.GenericMethod; +import dan200.computercraft.core.filesystem.FileMount; +import dan200.computercraft.core.filesystem.ResourceMount; +import dan200.computercraft.fabric.mixin.MinecraftServerAccess; +import dan200.computercraft.shared.*; +import dan200.computercraft.shared.peripheral.modem.wired.TileCable; +import dan200.computercraft.shared.peripheral.modem.wired.TileWiredModemFull; +import dan200.computercraft.shared.peripheral.modem.wireless.WirelessNetwork; +import dan200.computercraft.shared.util.IDAssigner; +import dan200.computercraft.shared.wired.WiredNode; +import me.shedaniel.cloth.api.utils.v1.GameInstanceUtils; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.resource.ReloadableResourceManager; +import net.minecraft.server.MinecraftServer; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +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 ) + { + MinecraftServer server = GameInstanceUtils.getServer(); + if( server != null ) + { + ReloadableResourceManager manager = (ReloadableResourceManager) ((MinecraftServerAccess) server).getServerResourceManager().getResourceManager(); + try + { + return manager.getResource( new Identifier( domain, subPath ) ) + .getInputStream(); + } + catch( IOException ignored ) + { + return null; + } + } + return null; + } + + @Nonnull + @Override + public String getInstalledVersion() + { + if( version != null ) + { + return version; + } + return version = FabricLoader.getInstance() + .getModContainer( ComputerCraft.MOD_ID ) + .map( x -> x.getMetadata() + .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 ) + { + MinecraftServer server = GameInstanceUtils.getServer(); + if( server != null ) + { + ReloadableResourceManager manager = (ReloadableResourceManager) ((MinecraftServerAccess) server).getServerResourceManager().getResourceManager(); + ResourceMount mount = ResourceMount.get( domain, subPath, manager ); + return mount.exists( "" ) ? mount : null; + } + return 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 ); + } + + @Override + public void registerGenericSource( @Nonnull GenericSource source ) + { + GenericMethod.register( source ); + } + + @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 ); + } + + @Nullable + @Override + public IWiredElement getWiredElementAt( @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + BlockEntity tile = world.getBlockEntity( pos ); + if( tile instanceof TileCable ) + { + return ((TileCable) tile).getElement( side ); + } + else if( tile instanceof TileWiredModemFull ) + { + return ((TileWiredModemFull) tile).getElement(); + } + return null; + } +} diff --git a/remappedSrc/dan200/computercraft/api/ComputerCraftAPI.java b/remappedSrc/dan200/computercraft/api/ComputerCraftAPI.java new file mode 100644 index 000000000..c1f8ed540 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/ComputerCraftAPI.java @@ -0,0 +1,297 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api; + +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.GenericSource; +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 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 +{ + private static IComputerCraftAPI instance; + + @Nonnull + @Deprecated + public static String getAPIVersion() + { + return getInstalledVersion(); + } + + @Nonnull + public static String getInstalledVersion() + { + return getInstance().getInstalledVersion(); + } + + @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 ); + } + } + + /** + * 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. For instance, ComputerCraft's resources are stored in + * "/data/computercraft/lua/rom". We construct a mount for that with + * {@code createResourceMount("computercraft", "lua/rom")}. + * + * @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 method source for generic peripherals. + * + * @param source The method source to register. + * @see GenericSource + */ + public static void registerGenericSource( @Nonnull GenericSource source ) + { + getInstance().registerGenericSource( source ); + } + + /** + * 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() + */ + @Nullable + public static IWiredElement getWiredElementAt( @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + return getInstance().getWiredElementAt( world, pos, side ); + } + + 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 registerGenericSource( @Nonnull GenericSource source ); + + 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 ); + + @Nullable + IWiredElement getWiredElementAt( @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction side ); + } +} diff --git a/remappedSrc/dan200/computercraft/api/IUpgradeBase.java b/remappedSrc/dan200/computercraft/api/IUpgradeBase.java new file mode 100644 index 000000000..ee9b6e500 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/IUpgradeBase.java @@ -0,0 +1,86 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api; + +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.util.Identifier; + +import javax.annotation.Nonnull; + +/** + * Common functionality between {@link ITurtleUpgrade} and {@link IPocketUpgrade}. + */ +public interface IUpgradeBase +{ + /** + * 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 unique ID for this upgrade. + */ + @Nonnull + Identifier getUpgradeID(); + + /** + * Return an unlocalised string to describe this type of computer in 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 an item stack representing the type of item that a computer must be crafted + * with to create a version which holds this upgrade. This item stack is also used + * to determine the upgrade given by {@code turtle.equipLeft()} or {@code pocket.equipBack()} + * + * This should be constant over a session (or at least a datapack reload). It is recommended + * that you cache the stack 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(); + + /** + * Determine if an item is suitable for being used for this upgrade. + * + * When un-equipping an upgrade, we return {@link #getCraftingItem()} rather than + * the original stack. In order to prevent people losing items with enchantments (or + * repairing items with non-0 damage), we impose additional checks on the item. + * + * The default check requires that any non-capability NBT is exactly the same as the + * crafting item, but this may be relaxed for your upgrade. + * + * @param stack The stack to check. This is guaranteed to be non-empty and have the same item as + * {@link #getCraftingItem()}. + * @return If this stack may be used to equip this upgrade. + * @see net.minecraftforge.common.crafting.NBTIngredient#test(ItemStack) For the implementation of the default + * check. + */ + default boolean isItemSuitable( @Nonnull ItemStack stack ) + { + ItemStack crafting = getCraftingItem(); + + // A more expanded form of ItemStack.areShareTagsEqual, but allowing an empty tag to be equal to a + // null one. + NbtCompound shareTag = stack.getNbt(); + NbtCompound craftingShareTag = crafting.getNbt(); + if( shareTag == craftingShareTag ) return true; + if( shareTag == null ) return craftingShareTag.isEmpty(); + if( craftingShareTag == null ) return shareTag.isEmpty(); + return shareTag.equals( craftingShareTag ); + } +} diff --git a/remappedSrc/dan200/computercraft/api/client/TransformedModel.java b/remappedSrc/dan200/computercraft/api/client/TransformedModel.java new file mode 100644 index 000000000..c44d6f04d --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/client/TransformedModel.java @@ -0,0 +1,93 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.client; + +import dan200.computercraft.fabric.mixin.AffineTransformationAccess; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +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.MatrixStack; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.AffineTransformation; +import javax.annotation.Nonnull; +import java.util.Objects; + +/** + * A model to render, combined with a transformation matrix to apply. + */ +@Environment( EnvType.CLIENT ) +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 ); + 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; + } + + public void push( MatrixStack matrixStack ) + { + matrixStack.push(); + + AffineTransformationAccess access = (AffineTransformationAccess) (Object) matrix; + if( access.getTranslation() != null ) + { + matrixStack.translate( access.getTranslation().getX(), access.getTranslation().getY(), access.getTranslation().getZ() ); + } + + matrixStack.multiply( matrix.getRotation2() ); + + if( access.getScale() != null ) + { + matrixStack.scale( access.getScale().getX(), access.getScale().getY(), access.getScale().getZ() ); + } + + if( access.getRotation1() != null ) + { + matrixStack.multiply( access.getRotation1() ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/api/filesystem/FileAttributes.java b/remappedSrc/dan200/computercraft/api/filesystem/FileAttributes.java new file mode 100644 index 000000000..20db37caa --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/filesystem/FileAttributes.java @@ -0,0 +1,82 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/filesystem/FileOperationException.java b/remappedSrc/dan200/computercraft/api/filesystem/FileOperationException.java new file mode 100644 index 000000000..dc08e0268 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/filesystem/FileOperationException.java @@ -0,0 +1,42 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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" ) ); + filename = null; + } + + @Nullable + public String getFilename() + { + return filename; + } +} diff --git a/remappedSrc/dan200/computercraft/api/filesystem/IFileSystem.java b/remappedSrc/dan200/computercraft/api/filesystem/IFileSystem.java new file mode 100644 index 000000000..8c74731e3 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/filesystem/IFileSystem.java @@ -0,0 +1,44 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/filesystem/IMount.java b/remappedSrc/dan200/computercraft/api/filesystem/IMount.java new file mode 100644 index 000000000..9634cbb57 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/filesystem/IMount.java @@ -0,0 +1,95 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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 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; + + /** + * 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 ) ); + } + + /** + * 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 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; +} diff --git a/remappedSrc/dan200/computercraft/api/filesystem/IWritableMount.java b/remappedSrc/dan200/computercraft/api/filesystem/IWritableMount.java new file mode 100644 index 000000000..d39eec86c --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/lua/GenericSource.java b/remappedSrc/dan200/computercraft/api/lua/GenericSource.java new file mode 100644 index 000000000..f109f9d37 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/GenericSource.java @@ -0,0 +1,58 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IPeripheralProvider; +import dan200.computercraft.core.asm.LuaMethod; +import net.minecraft.util.Identifier; + +import javax.annotation.Nonnull; + +/** + * A generic source of {@link LuaMethod} functions. + * + * Unlike normal objects ({@link IDynamicLuaObject} or {@link IPeripheral}), methods do not target this object but + * instead are defined as {@code static} and accept their target as the first parameter. This allows you to inject + * methods onto objects you do not own, as well as declaring methods for a specific "trait" (for instance, a + * {@link Capability}). + * + * Currently the "generic peripheral" system is incompatible with normal peripherals. Normal {@link IPeripheralProvider} + * or {@link IPeripheral} implementations take priority. Tile entities which use this system are given a peripheral name + * determined by their id, rather than any peripheral provider. This will hopefully change in the future, once a suitable + * design has been established. + * + * For example, the main CC: Tweaked mod defines a generic source for inventories, which works on {@link IItemHandler}s: + * + *
{@code
+ * public class InventoryMethods implements GenericSource {
+ *     \@LuaFunction( mainThread = true )
+ *     public static int size(IItemHandler inventory) {
+ *         return inventory.getSlots();
+ *     }
+ *
+ *     // ...
+ * }
+ * }
+ * + * @see ComputerCraftAPI#registerGenericSource(GenericSource) + * @see ComputerCraftAPI#registerGenericCapability(Capability) New capabilities (those not built into Forge) must be + * explicitly given to the generic peripheral system, as there is no way to enumerate all capabilities. + */ +public interface GenericSource +{ + /** + * A unique identifier for this generic source. + * + * This is currently unused, but may be used in the future to allow disabling specific sources. It is recommended + * to return an identifier using your mod's ID. + * + * @return This source's identifier. + */ + @Nonnull + Identifier id(); +} diff --git a/remappedSrc/dan200/computercraft/api/lua/IArguments.java b/remappedSrc/dan200/computercraft/api/lua/IArguments.java new file mode 100644 index 000000000..5546176ca --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/IArguments.java @@ -0,0 +1,461 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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 +{ + /** + * 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 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 ); + + /** + * 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 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 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 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 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 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 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 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 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 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 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. + * @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 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 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 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. + * @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 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. + * @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 ); + } + + /** + * 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 ); + } +} diff --git a/remappedSrc/dan200/computercraft/api/lua/IComputerSystem.java b/remappedSrc/dan200/computercraft/api/lua/IComputerSystem.java new file mode 100644 index 000000000..d9069b52a --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/lua/IDynamicLuaObject.java b/remappedSrc/dan200/computercraft/api/lua/IDynamicLuaObject.java new file mode 100644 index 000000000..49c4d6a47 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/IDynamicLuaObject.java @@ -0,0 +1,41 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/lua/ILuaAPI.java b/remappedSrc/dan200/computercraft/api/lua/ILuaAPI.java new file mode 100644 index 000000000..ca33136dc --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/ILuaAPI.java @@ -0,0 +1,54 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/lua/ILuaAPIFactory.java b/remappedSrc/dan200/computercraft/api/lua/ILuaAPIFactory.java new file mode 100644 index 000000000..9b4fbbc47 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/ILuaAPIFactory.java @@ -0,0 +1,31 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/lua/ILuaCallback.java b/remappedSrc/dan200/computercraft/api/lua/ILuaCallback.java new file mode 100644 index 000000000..b24e4b25e --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/lua/ILuaContext.java b/remappedSrc/dan200/computercraft/api/lua/ILuaContext.java new file mode 100644 index 000000000..d871786e4 --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/lua/ILuaFunction.java b/remappedSrc/dan200/computercraft/api/lua/ILuaFunction.java new file mode 100644 index 000000000..70d900c2c --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/ILuaFunction.java @@ -0,0 +1,30 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/lua/ILuaObject.java b/remappedSrc/dan200/computercraft/api/lua/ILuaObject.java new file mode 100644 index 000000000..eba77bc19 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/ILuaObject.java @@ -0,0 +1,18 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public interface ILuaObject +{ + @Nonnull + String[] getMethodNames(); + + @Nullable + Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException; +} diff --git a/remappedSrc/dan200/computercraft/api/lua/ILuaTask.java b/remappedSrc/dan200/computercraft/api/lua/ILuaTask.java new file mode 100644 index 000000000..44a429ed4 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/ILuaTask.java @@ -0,0 +1,29 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/lua/LuaException.java b/remappedSrc/dan200/computercraft/api/lua/LuaException.java new file mode 100644 index 000000000..df97ed0fa --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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 ); + hasLevel = false; + level = 1; + } + + public LuaException( @Nullable String message, int level ) + { + super( message ); + 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/remappedSrc/dan200/computercraft/api/lua/LuaFunction.java b/remappedSrc/dan200/computercraft/api/lua/LuaFunction.java new file mode 100644 index 000000000..064149466 --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/lua/LuaValues.java b/remappedSrc/dan200/computercraft/api/lua/LuaValues.java new file mode 100644 index 000000000..a78091060 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/LuaValues.java @@ -0,0 +1,184 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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(); + } + + /** + * 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 + ")" ); + } + + /** + * 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"; + } + + /** + * 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; + } + + /** + * 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"; + } + + /** + * 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/remappedSrc/dan200/computercraft/api/lua/MethodResult.java b/remappedSrc/dan200/computercraft/api/lua/MethodResult.java new file mode 100644 index 000000000..8787bf911 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/MethodResult.java @@ -0,0 +1,177 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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 ) + { + result = arguments; + this.callback = callback; + adjust = 0; + } + + private MethodResult( Object[] arguments, ILuaCallback callback, int adjust ) + { + 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/remappedSrc/dan200/computercraft/api/lua/ObjectArguments.java b/remappedSrc/dan200/computercraft/api/lua/ObjectArguments.java new file mode 100644 index 000000000..d800d0ac6 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/lua/ObjectArguments.java @@ -0,0 +1,76 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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 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() ) ); + } + + @Override + public Object[] getAll() + { + return args.toArray(); + } + + @Override + public int count() + { + return args.size(); + } + + @Nullable + @Override + public Object get( int index ) + { + return index >= args.size() ? null : args.get( index ); + } +} diff --git a/remappedSrc/dan200/computercraft/api/media/IMedia.java b/remappedSrc/dan200/computercraft/api/media/IMedia.java new file mode 100644 index 000000000..986da717e --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/media/IMedia.java @@ -0,0 +1,88 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/media/IMediaProvider.java b/remappedSrc/dan200/computercraft/api/media/IMediaProvider.java new file mode 100644 index 000000000..7afa1e632 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/media/IMediaProvider.java @@ -0,0 +1,31 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/network/IPacketNetwork.java b/remappedSrc/dan200/computercraft/api/network/IPacketNetwork.java new file mode 100644 index 000000000..b897717c6 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/network/IPacketNetwork.java @@ -0,0 +1,60 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/network/IPacketReceiver.java b/remappedSrc/dan200/computercraft/api/network/IPacketReceiver.java new file mode 100644 index 000000000..660344a2b --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/network/IPacketSender.java b/remappedSrc/dan200/computercraft/api/network/IPacketSender.java new file mode 100644 index 000000000..f3c06a397 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/network/IPacketSender.java @@ -0,0 +1,43 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/network/Packet.java b/remappedSrc/dan200/computercraft/api/network/Packet.java new file mode 100644 index 000000000..34b2bd41f --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/network/Packet.java @@ -0,0 +1,130 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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 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; + } + + @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 ); + } +} diff --git a/remappedSrc/dan200/computercraft/api/network/wired/IWiredElement.java b/remappedSrc/dan200/computercraft/api/network/wired/IWiredElement.java new file mode 100644 index 000000000..5ca0ddb9f --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/network/wired/IWiredElement.java @@ -0,0 +1,33 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/network/wired/IWiredNetwork.java b/remappedSrc/dan200/computercraft/api/network/wired/IWiredNetwork.java new file mode 100644 index 000000000..fa44c4d1b --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/network/wired/IWiredNetwork.java @@ -0,0 +1,81 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/network/wired/IWiredNetworkChange.java b/remappedSrc/dan200/computercraft/api/network/wired/IWiredNetworkChange.java new file mode 100644 index 000000000..bef4b1048 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/network/wired/IWiredNetworkChange.java @@ -0,0 +1,38 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/network/wired/IWiredNode.java b/remappedSrc/dan200/computercraft/api/network/wired/IWiredNode.java new file mode 100644 index 000000000..73afe2fd7 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/network/wired/IWiredNode.java @@ -0,0 +1,104 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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(); + + /** + * 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 ); + } + + /** + * 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(); + + /** + * 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/remappedSrc/dan200/computercraft/api/network/wired/IWiredSender.java b/remappedSrc/dan200/computercraft/api/network/wired/IWiredSender.java new file mode 100644 index 000000000..c306b0a6b --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/network/wired/IWiredSender.java @@ -0,0 +1,29 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/peripheral/IComputerAccess.java b/remappedSrc/dan200/computercraft/api/peripheral/IComputerAccess.java new file mode 100644 index 000000000..0514ea509 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/peripheral/IComputerAccess.java @@ -0,0 +1,198 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.peripheral; + +import dan200.computercraft.api.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 ); + + /** + * 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(); + + /** + * 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 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/remappedSrc/dan200/computercraft/api/peripheral/IDynamicPeripheral.java b/remappedSrc/dan200/computercraft/api/peripheral/IDynamicPeripheral.java new file mode 100644 index 000000000..e06c14096 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/peripheral/IDynamicPeripheral.java @@ -0,0 +1,49 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.peripheral; + +import dan200.computercraft.api.lua.*; + +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/remappedSrc/dan200/computercraft/api/peripheral/IPeripheral.java b/remappedSrc/dan200/computercraft/api/peripheral/IPeripheral.java new file mode 100644 index 000000000..09a63c64d --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/peripheral/IPeripheral.java @@ -0,0 +1,88 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.peripheral; + +import dan200.computercraft.api.lua.LuaFunction; + +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 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/remappedSrc/dan200/computercraft/api/peripheral/IPeripheralProvider.java b/remappedSrc/dan200/computercraft/api/peripheral/IPeripheralProvider.java new file mode 100644 index 000000000..ae704dfc7 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/peripheral/IPeripheralProvider.java @@ -0,0 +1,38 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.peripheral; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import java.util.Optional; + +/** + * This interface is used to create peripheral implementations for blocks. + * + * If you have a {@link BlockEntity} 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 Optional#empty()} if there is not a peripheral here you'd like to handle. + * @see dan200.computercraft.api.ComputerCraftAPI#registerPeripheralProvider(IPeripheralProvider) + */ + @Nonnull + IPeripheral getPeripheral( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ); +} diff --git a/remappedSrc/dan200/computercraft/api/peripheral/IPeripheralTile.java b/remappedSrc/dan200/computercraft/api/peripheral/IPeripheralTile.java new file mode 100644 index 000000000..b12d7f5c0 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/peripheral/IPeripheralTile.java @@ -0,0 +1,31 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.peripheral; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A {@link net.minecraft.block.entity.BlockEntity} which may act as a peripheral. + * + * If you need more complex capabilities (such as handling TEs not belonging to your mod), you should use {@link IPeripheralProvider}. + */ +public interface IPeripheralTile +{ + /** + * Get the peripheral on the given {@code side}. + * + * @param side The side to get the peripheral from. + * @return A peripheral, or {@code null} if there is not a peripheral here. + * @see IPeripheralProvider#getPeripheral(World, BlockPos, Direction) + */ + @Nullable + IPeripheral getPeripheral( @Nonnull Direction side ); +} diff --git a/remappedSrc/dan200/computercraft/api/peripheral/IWorkMonitor.java b/remappedSrc/dan200/computercraft/api/peripheral/IWorkMonitor.java new file mode 100644 index 000000000..21ae59438 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/peripheral/IWorkMonitor.java @@ -0,0 +1,80 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.peripheral; + +import 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, 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(); + + /** + * 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; + } + + /** + * If the owning computer is currently allowed to execute work. + * + * @return If we can execute work right now. + */ + boolean canWork(); + + /** + * 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 ); +} diff --git a/remappedSrc/dan200/computercraft/api/peripheral/NotAttachedException.java b/remappedSrc/dan200/computercraft/api/peripheral/NotAttachedException.java new file mode 100644 index 000000000..335841cfb --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.peripheral; + +/** + * 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/remappedSrc/dan200/computercraft/api/pocket/AbstractPocketUpgrade.java b/remappedSrc/dan200/computercraft/api/pocket/AbstractPocketUpgrade.java new file mode 100644 index 000000000..d17820409 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/pocket/AbstractPocketUpgrade.java @@ -0,0 +1,67 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.pocket; + +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; + +import javax.annotation.Nonnull; + +/** + * 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 ItemStack stack; + + protected AbstractPocketUpgrade( Identifier id, ItemConvertible item ) + { + this( id, Util.createTranslationKey( "upgrade", id ) + ".adjective", item ); + } + + protected AbstractPocketUpgrade( Identifier id, String adjective, ItemConvertible item ) + { + this.id = id; + this.adjective = adjective; + stack = new ItemStack( item ); + } + + protected AbstractPocketUpgrade( Identifier id, String adjective, ItemStack stack ) + { + this.id = id; + this.adjective = adjective; + this.stack = stack; + } + + + @Nonnull + @Override + public final Identifier getUpgradeID() + { + return id; + } + + @Nonnull + @Override + public final String getUnlocalisedAdjective() + { + return adjective; + } + + @Nonnull + @Override + public final ItemStack getCraftingItem() + { + return stack; + } +} diff --git a/remappedSrc/dan200/computercraft/api/pocket/IPocketAccess.java b/remappedSrc/dan200/computercraft/api/pocket/IPocketAccess.java new file mode 100644 index 000000000..0638f6c5c --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.pocket; + +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.entity.Entity; +import net.minecraft.nbt.NbtCompound; +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 + NbtCompound 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/remappedSrc/dan200/computercraft/api/pocket/IPocketUpgrade.java b/remappedSrc/dan200/computercraft/api/pocket/IPocketUpgrade.java new file mode 100644 index 000000000..d8b9d0c13 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/pocket/IPocketUpgrade.java @@ -0,0 +1,62 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.pocket; + +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.IUpgradeBase; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Additional peripherals for pocket computers. + * + * @see ComputerCraftAPI#registerPocketUpgrade(IPocketUpgrade) + */ +public interface IPocketUpgrade extends IUpgradeBase +{ + /** + * 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/remappedSrc/dan200/computercraft/api/redstone/IBundledRedstoneProvider.java b/remappedSrc/dan200/computercraft/api/redstone/IBundledRedstoneProvider.java new file mode 100644 index 000000000..ad92f3903 --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/turtle/AbstractTurtleUpgrade.java b/remappedSrc/dan200/computercraft/api/turtle/AbstractTurtleUpgrade.java new file mode 100644 index 000000000..e53215bbd --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/AbstractTurtleUpgrade.java @@ -0,0 +1,79 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.turtle; + +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; + +import javax.annotation.Nonnull; + +/** + * 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 ItemStack stack; + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, String adjective, ItemConvertible item ) + { + this( id, type, adjective, new ItemStack( item ) ); + } + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, String adjective, ItemStack stack ) + { + this.id = id; + this.type = type; + this.adjective = adjective; + this.stack = stack; + } + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, ItemConvertible item ) + { + this( id, type, new ItemStack( item ) ); + } + + protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, ItemStack stack ) + { + this( id, type, Util.createTranslationKey( "upgrade", id ) + ".adjective", stack ); + } + + + @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; + } +} diff --git a/remappedSrc/dan200/computercraft/api/turtle/FakePlayer.java b/remappedSrc/dan200/computercraft/api/turtle/FakePlayer.java new file mode 100644 index 000000000..a73471f6b --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/FakePlayer.java @@ -0,0 +1,356 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.turtle; + +import com.mojang.authlib.GameProfile; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import net.minecraft.block.entity.CommandBlockBlockEntity; +import net.minecraft.block.entity.SignBlockEntity; +import net.minecraft.command.argument.EntityAnchorArgumentType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.damage.DamageSource; +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.network.*; +import net.minecraft.network.packet.c2s.play.RequestCommandCompletionsC2SPacket; +import net.minecraft.network.packet.c2s.play.VehicleMoveC2SPacket; +import net.minecraft.recipe.Recipe; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.network.ServerPlayerInteractionManager; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.text.Text; +import net.minecraft.util.Hand; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.village.TradeOfferList; +import net.minecraft.world.GameMode; + +import javax.annotation.Nullable; +import javax.crypto.Cipher; +import java.util.Collection; +import java.util.OptionalInt; +import java.util.UUID; + +/** + * A wrapper for {@link ServerPlayerEntity} which denotes a "fake" player. + * + * Please note that this does not implement any of the traditional fake player behaviour. It simply exists to prevent me passing in normal players. + */ +public class FakePlayer extends ServerPlayerEntity +{ + public FakePlayer( ServerWorld world, GameProfile gameProfile ) + { + super( world.getServer(), world, gameProfile, new ServerPlayerInteractionManager( world ) ); + networkHandler = new FakeNetHandler( this ); + } + + // region Direct networkHandler access + @Override + public void enterCombat() + { + } + + @Override + public void endCombat() + { + } + + @Override + public void tick() + { + } + + @Override + public void playerTick() + { + } + + @Override + public void onDeath( DamageSource damage ) + { + } + + @Override + public Entity moveToWorld( ServerWorld destination ) + { + return this; + } + + @Override + public void wakeUp( boolean bl, boolean updateSleepingPlayers ) + { + + } + + @Override + public boolean startRiding( Entity entity, boolean flag ) + { + return false; + } + + @Override + public void stopRiding() + { + } + + @Override + public void openEditSignScreen( SignBlockEntity tile ) + { + } + + @Override + public OptionalInt openHandledScreen( @Nullable NamedScreenHandlerFactory container ) + { + return OptionalInt.empty(); + } + + @Override + public void sendTradeOffers( int id, TradeOfferList list, int level, int experience, boolean levelled, boolean refreshable ) + { + } + + @Override + public void openHorseInventory( HorseBaseEntity horse, Inventory inventory ) + { + } + + @Override + public void useBook( ItemStack stack, Hand hand ) + { + } + + @Override + public void openCommandBlockScreen( CommandBlockBlockEntity block ) + { + } + + @Override + public void onSlotUpdate( ScreenHandler container, int slot, ItemStack stack ) + { + } + + @Override + public void onHandlerRegistered( ScreenHandler container, DefaultedList defaultedList ) + { + } + + @Override + public void onPropertyUpdate( ScreenHandler container, int key, int value ) + { + } + + @Override + public void closeHandledScreen() + { + } + + @Override + public void updateCursorStack() + { + } + + @Override + public int unlockRecipes( Collection> recipes ) + { + return 0; + } + + // Indirect + @Override + public int lockRecipes( Collection> recipes ) + { + return 0; + } + + @Override + public void sendMessage( Text textComponent, boolean status ) + { + } + + @Override + protected void consumeItem() + { + } + + @Override + public void lookAt( EntityAnchorArgumentType.EntityAnchor anchor, Vec3d vec3d ) + { + } + + @Override + public void lookAtEntity( EntityAnchorArgumentType.EntityAnchor self, Entity entity, EntityAnchorArgumentType.EntityAnchor target ) + { + } + + @Override + protected void onStatusEffectApplied( StatusEffectInstance statusEffectInstance ) + { + } + + @Override + protected void onStatusEffectUpgraded( StatusEffectInstance statusEffectInstance, boolean particles ) + { + } + + @Override + protected void onStatusEffectRemoved( StatusEffectInstance statusEffectInstance ) + { + } + + @Override + public void requestTeleport( double x, double y, double z ) + { + } + + @Override + public void changeGameMode( GameMode gameMode ) + { + } + + @Override + public void sendMessage( Text message, MessageType type, UUID senderUuid ) + { + + } + + @Override + public String getIp() + { + return "[Fake Player]"; + } + + @Override + public void sendResourcePackUrl( String url, String hash ) + { + } + + @Override + public void onStoppedTracking( Entity entity ) + { + } + + @Override + public void setCameraEntity( Entity entity ) + { + } + + @Override + public void teleport( ServerWorld serverWorld, double x, double y, double z, float pitch, float yaw ) + { + } + + @Override + public void sendInitialChunkPackets( ChunkPos chunkPos, Packet packet, Packet packet2 ) + { + } + + @Override + public void sendUnloadChunkPacket( ChunkPos chunkPos ) + { + } + + @Override + public void playSound( SoundEvent soundEvent, SoundCategory soundCategory, float volume, float pitch ) + { + } + + private static class FakeNetHandler extends ServerPlayNetworkHandler + { + FakeNetHandler( ServerPlayerEntity player ) + { + super( player.server, new FakeConnection(), player ); + } + + @Override + public void disconnect( Text message ) + { + } + + @Override + public void onVehicleMove( VehicleMoveC2SPacket move ) + { + } + + @Override + public void onRequestCommandCompletions( RequestCommandCompletionsC2SPacket packet ) + { + } + + @Override + public void sendPacket( Packet packet, @Nullable GenericFutureListener> listener ) + { + } + } + + private static class FakeConnection extends ClientConnection + { + FakeConnection() + { + super( NetworkSide.CLIENTBOUND ); + } + + @Override + public void channelActive( ChannelHandlerContext active ) + { + } + + @Override + public void setState( NetworkState state ) + { + } + + @Override + public void exceptionCaught( ChannelHandlerContext context, Throwable err ) + { + } + + @Override + protected void channelRead0( ChannelHandlerContext context, Packet packet ) + { + } + + @Override + public void send( Packet packet, @Nullable GenericFutureListener> listener ) + { + } + + @Override + public void tick() + { + } + + @Override + public void disconnect( Text message ) + { + } + + @Override + public void setupEncryption( Cipher cipher, Cipher cipher2 ) + { + super.setupEncryption( cipher, cipher2 ); + } + + @Override + public void disableAutoRead() + { + } + + @Override + public void setCompressionThreshold( int size ) + { + } + } +} diff --git a/remappedSrc/dan200/computercraft/api/turtle/ITurtleAccess.java b/remappedSrc/dan200/computercraft/api/turtle/ITurtleAccess.java new file mode 100644 index 000000000..c50b83eb3 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/ITurtleAccess.java @@ -0,0 +1,282 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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 dan200.computercraft.shared.util.ItemStorage; +import net.minecraft.inventory.Inventory; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +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 ); + + /** + * 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(); + + /** + * 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 player who owns this turtle, namely whoever placed it. + * + * @return This turtle's owner. + */ + @Nullable + GameProfile getOwningPlayer(); + + /** + * 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 + NbtCompound 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 ); + + default ItemStorage getItemHandler() + { + return ItemStorage.wrap( getInventory() ); + } + + /** + * Get the inventory of this turtle. + * + * Note: this inventory should only be accessed and modified on the server thread. + * + * @return This turtle's inventory + */ + @Nonnull + Inventory getInventory(); +} diff --git a/remappedSrc/dan200/computercraft/api/turtle/ITurtleCommand.java b/remappedSrc/dan200/computercraft/api/turtle/ITurtleCommand.java new file mode 100644 index 000000000..134bb346d --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/ITurtleCommand.java @@ -0,0 +1,34 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/turtle/ITurtleUpgrade.java b/remappedSrc/dan200/computercraft/api/turtle/ITurtleUpgrade.java new file mode 100644 index 000000000..bc9d9e018 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/ITurtleUpgrade.java @@ -0,0 +1,90 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.turtle; + +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.IUpgradeBase; +import dan200.computercraft.api.client.TransformedModel; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.util.math.Direction; + +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 extends IUpgradeBase +{ + /** + * 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(); + + /** + * 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. + * + * @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. + * + * @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/remappedSrc/dan200/computercraft/api/turtle/TurtleAnimation.java b/remappedSrc/dan200/computercraft/api/turtle/TurtleAnimation.java new file mode 100644 index 000000000..f4468b5c9 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/TurtleAnimation.java @@ -0,0 +1,83 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/turtle/TurtleCommandResult.java b/remappedSrc/dan200/computercraft/api/turtle/TurtleCommandResult.java new file mode 100644 index 000000000..17f42bb85 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/TurtleCommandResult.java @@ -0,0 +1,118 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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 ); + 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; + } + + /** + * 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 ); + } + + /** + * 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/remappedSrc/dan200/computercraft/api/turtle/TurtleSide.java b/remappedSrc/dan200/computercraft/api/turtle/TurtleSide.java new file mode 100644 index 000000000..86edade62 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/TurtleSide.java @@ -0,0 +1,23 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/turtle/TurtleUpgradeType.java b/remappedSrc/dan200/computercraft/api/turtle/TurtleUpgradeType.java new file mode 100644 index 000000000..a6c6fede4 --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/turtle/TurtleVerb.java b/remappedSrc/dan200/computercraft/api/turtle/TurtleVerb.java new file mode 100644 index 000000000..d4622b1a0 --- /dev/null +++ b/remappedSrc/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-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/turtle/event/TurtleAction.java b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleAction.java new file mode 100644 index 000000000..ca5ccecf6 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleAction.java @@ -0,0 +1,84 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/turtle/event/TurtleActionEvent.java b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleActionEvent.java new file mode 100644 index 000000000..b6c129855 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleActionEvent.java @@ -0,0 +1,85 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.turtle.event; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.TurtleCommandResult; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * An event fired when a turtle is performing a known action. + */ +public class TurtleActionEvent extends TurtleEvent +{ + private final TurtleAction action; + private String failureMessage; + private boolean cancelled = false; + + 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. + */ + @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 ) + { + cancelled = true; + 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; + } + + public boolean isCancelled() + { + return cancelled; + } +} diff --git a/remappedSrc/dan200/computercraft/api/turtle/event/TurtleAttackEvent.java b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleAttackEvent.java new file mode 100644 index 000000000..fa8c492cc --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleAttackEvent.java @@ -0,0 +1,73 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.turtle.event; + +import dan200.computercraft.api.turtle.FakePlayer; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import net.minecraft.entity.Entity; + +import javax.annotation.Nonnull; +import java.util.Objects; + +/** + * Fired when a turtle attempts to attack an entity. + * + * @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/remappedSrc/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java new file mode 100644 index 000000000..2a195e62a --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java @@ -0,0 +1,224 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.turtle.event; + +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.turtle.FakePlayer; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import net.minecraft.block.BlockState; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +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. + * + * @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/remappedSrc/dan200/computercraft/api/turtle/event/TurtleEvent.java b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleEvent.java new file mode 100644 index 000000000..726a059ae --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleEvent.java @@ -0,0 +1,52 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.turtle.event; + +import com.google.common.eventbus.EventBus; +import dan200.computercraft.api.turtle.ITurtleAccess; + +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 +{ + public static final EventBus EVENT_BUS = new EventBus(); + + private final ITurtleAccess turtle; + + protected TurtleEvent( @Nonnull ITurtleAccess turtle ) + { + Objects.requireNonNull( turtle, "turtle cannot be null" ); + this.turtle = turtle; + } + + public static boolean post( TurtleActionEvent event ) + { + EVENT_BUS.post( event ); + return event.isCancelled(); + } + + /** + * Get the turtle which is performing this action. + * + * @return The access for this turtle. + */ + @Nonnull + public ITurtleAccess getTurtle() + { + return turtle; + } + +} diff --git a/remappedSrc/dan200/computercraft/api/turtle/event/TurtleInspectItemEvent.java b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleInspectItemEvent.java new file mode 100644 index 000000000..d22b6673d --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleInspectItemEvent.java @@ -0,0 +1,90 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/api/turtle/event/TurtleInventoryEvent.java b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleInventoryEvent.java new file mode 100644 index 000000000..37b01ecb5 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleInventoryEvent.java @@ -0,0 +1,87 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.turtle.event; + +import dan200.computercraft.api.turtle.FakePlayer; +import dan200.computercraft.api.turtle.ITurtleAccess; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +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 Inventory handler; + + protected TurtleInventoryEvent( @Nonnull ITurtleAccess turtle, @Nonnull TurtleAction action, @Nonnull FakePlayer player, @Nonnull World world, + @Nonnull BlockPos pos, @Nullable Inventory 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 Inventory 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 Inventory 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 Inventory 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/remappedSrc/dan200/computercraft/api/turtle/event/TurtlePlayerEvent.java b/remappedSrc/dan200/computercraft/api/turtle/event/TurtlePlayerEvent.java new file mode 100644 index 000000000..463a6602c --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/event/TurtlePlayerEvent.java @@ -0,0 +1,44 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.turtle.event; + +import dan200.computercraft.api.turtle.FakePlayer; +import dan200.computercraft.api.turtle.ITurtleAccess; + +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/remappedSrc/dan200/computercraft/api/turtle/event/TurtleRefuelEvent.java b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleRefuelEvent.java new file mode 100644 index 000000000..a5374d684 --- /dev/null +++ b/remappedSrc/dan200/computercraft/api/turtle/event/TurtleRefuelEvent.java @@ -0,0 +1,89 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +package dan200.computercraft.api.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/remappedSrc/dan200/computercraft/client/ClientRegistry.java b/remappedSrc/dan200/computercraft/client/ClientRegistry.java new file mode 100644 index 000000000..eb66e181a --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/ClientRegistry.java @@ -0,0 +1,127 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.ComputerCraftRegistry; +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.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry; +import net.fabricmc.fabric.api.event.client.ClientSpriteRegistryCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.ModelLoader; +import net.minecraft.client.render.model.ModelRotation; +import net.minecraft.client.render.model.UnbakedModel; +import net.minecraft.client.texture.SpriteAtlasTexture; +import net.minecraft.client.util.ModelIdentifier; +import net.minecraft.resource.ResourceManager; +import net.minecraft.util.Identifier; + +import java.util.HashSet; +import java.util.function.Consumer; + +/** + * Registers textures and models for items. + */ +@SuppressWarnings( { + "MethodCallSideOnly", + "LocalVariableDeclarationSideOnly" +} ) +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() {} + + public static void onTextureStitchEvent( SpriteAtlasTexture atlasTexture, ClientSpriteRegistryCallback.Registry registry ) + { + for( String extra : EXTRA_TEXTURES ) + { + registry.register( new Identifier( ComputerCraft.MOD_ID, extra ) ); + } + } + + @SuppressWarnings( "NewExpressionSideOnly" ) + public static void onModelBakeEvent( ResourceManager manager, Consumer out ) + { + for( String model : EXTRA_MODELS ) + { + out.accept( new ModelIdentifier( new Identifier( ComputerCraft.MOD_ID, model ), "inventory" ) ); + } + } + + public static void onItemColours() + { + ColorProviderRegistry.ITEM.register( ( stack, layer ) -> { + return layer == 1 ? ((ItemDisk) stack.getItem()).getColour( stack ) : 0xFFFFFF; + }, ComputerCraftRegistry.ModItems.DISK ); + + ColorProviderRegistry.ITEM.register( ( stack, layer ) -> layer == 1 ? ItemTreasureDisk.getColour( stack ) : 0xFFFFFF, + ComputerCraftRegistry.ModItems.TREASURE_DISK ); + + ColorProviderRegistry.ITEM.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; + } + }, ComputerCraftRegistry.ModItems.POCKET_COMPUTER_NORMAL, ComputerCraftRegistry.ModItems.POCKET_COMPUTER_ADVANCED ); + + // Setup turtle colours + ColorProviderRegistry.ITEM.register( ( stack, tintIndex ) -> tintIndex == 0 ? ((IColouredItem) stack.getItem()).getColour( stack ) : 0xFFFFFF, + ComputerCraftRegistry.ModBlocks.TURTLE_NORMAL, + ComputerCraftRegistry.ModBlocks.TURTLE_ADVANCED ); + } + + private static BakedModel bake( ModelLoader loader, UnbakedModel model, Identifier identifier ) + { + model.getTextureDependencies( loader::getOrLoadModel, new HashSet<>() ); + return model.bake( loader, + spriteIdentifier -> MinecraftClient.getInstance() + .getSpriteAtlas( spriteIdentifier.getAtlasId() ) + .apply( spriteIdentifier.getTextureId() ), + ModelRotation.X0_Y0, + identifier ); + } +} diff --git a/remappedSrc/dan200/computercraft/client/ClientTableFormatter.java b/remappedSrc/dan200/computercraft/client/ClientTableFormatter.java new file mode 100644 index 000000000..8a0fdb581 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/ClientTableFormatter.java @@ -0,0 +1,99 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client; + +import dan200.computercraft.fabric.mixin.ChatHudAccess; +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; + +@SuppressWarnings( { + "MethodCallSideOnly", + "LocalVariableDeclarationSideOnly" +} ) +public class ClientTableFormatter implements TableFormatter +{ + public static final ClientTableFormatter INSTANCE = new ClientTableFormatter(); + + private static Int2IntOpenHashMap lastHeights = new Int2IntOpenHashMap(); + + @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 ); + } + + private static TextRenderer renderer() + { + return MinecraftClient.getInstance().textRenderer; + } + + @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 ); + ((ChatHudAccess) chat).callAddMessage( 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++ ) + { + ((ChatHudAccess) chat).callRemoveMessage( i + table.getId() ); + } + return height; + } +} diff --git a/remappedSrc/dan200/computercraft/client/FrameInfo.java b/remappedSrc/dan200/computercraft/client/FrameInfo.java new file mode 100644 index 000000000..600f44e05 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/FrameInfo.java @@ -0,0 +1,48 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client; + +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; + +public final class FrameInfo +{ + private static int tick; + private static long renderFrame; + + private FrameInfo() + { + } + + public static void init() + { + ClientTickEvents.START_CLIENT_TICK.register( m -> { + tick++; + } ); + } + + public static boolean getGlobalCursorBlink() + { + return (tick / 8) % 2 == 0; + } + + public static long getRenderFrame() + { + return renderFrame; + } + + // TODO Call this in a callback + public static void onTick() + { + tick++; + } + + // TODO Call this in a callback + public static void onRenderFrame() + { + renderFrame++; + } +} diff --git a/remappedSrc/dan200/computercraft/client/gui/FixedWidthFontRenderer.java b/remappedSrc/dan200/computercraft/client/gui/FixedWidthFontRenderer.java new file mode 100644 index 000000000..0e42a3ead --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/gui/FixedWidthFontRenderer.java @@ -0,0 +1,421 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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 net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.*; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.AffineTransformation; +import net.minecraft.util.math.Matrix4f; +import org.lwjgl.opengl.GL11; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class FixedWidthFontRenderer +{ + 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; + private static final Matrix4f IDENTITY = AffineTransformation.identity() + .getMatrix(); + private static final Identifier FONT = new Identifier( "computercraft", "textures/gui/term_font.png" ); + public static final RenderLayer TYPE = Type.MAIN; + + + private FixedWidthFontRenderer() + { + } + + 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(); + } + + private static void bindFont() + { + MinecraftClient.getInstance() + .getTextureManager() + .bindTexture( FONT ); + RenderSystem.texParameter( GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP ); + } + + 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 ); + } + + } + + 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 int getColour( char c, Colour def ) + { + return 15 - Terminal.getColour( c, def ); + } + + public static float toGreyscale( double[] rgb ) + { + return (float) ((rgb[0] + rgb[1] + rgb[2]) / 3); + } + + 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, 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 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(); + } + + 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( float x, float y, float width, float height ) + { + drawEmptyTerminal( IDENTITY, x, y, width, height ); + } + + 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( @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 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 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( ALL_MASK ) + .lightmap( DISABLE_LIGHTMAP ) + .build( false ) ); + + private Type( String name, Runnable setup, Runnable destroy ) + { + super( name, setup, destroy ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/client/gui/GuiComputer.java b/remappedSrc/dan200/computercraft/client/gui/GuiComputer.java new file mode 100644 index 000000000..a4f40a783 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/gui/GuiComputer.java @@ -0,0 +1,158 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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 class GuiComputer extends HandledScreen +{ + protected final ComputerFamily family; + protected final ClientComputer computer; + private final int termWidth; + private final int termHeight; + + protected WidgetTerminal terminal; + protected WidgetWrapper terminalWrapper; + + protected GuiComputer( T container, PlayerInventory player, Text title, int termWidth, int termHeight ) + { + super( container, player, title ); + this.family = container.getFamily(); + this.computer = (ClientComputer) container.getComputer(); + this.termWidth = termWidth; + this.termHeight = termHeight; + this.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() ); + } + + protected void initTerminal( int border, int widthExtra, int heightExtra ) + { + client.keyboard.setRepeatEvents( true ); + + int termPxWidth = termWidth * FixedWidthFontRenderer.FONT_WIDTH; + int termPxHeight = termHeight * FixedWidthFontRenderer.FONT_HEIGHT; + + backgroundWidth = termPxWidth + MARGIN * 2 + border * 2 + widthExtra; + backgroundHeight = termPxHeight + MARGIN * 2 + border * 2 + heightExtra; + + 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 + protected void init() + { + initTerminal( BORDER, 0, 0 ); + } + + @Override + public void render( @Nonnull MatrixStack stack, int mouseX, int mouseY, float partialTicks ) + { + this.renderBackground( stack ); + super.render( stack, mouseX, mouseY, partialTicks ); + drawMouseoverTooltip( stack, mouseX, mouseY ); + } + + @Override + protected void drawForeground( @Nonnull MatrixStack transform, int mouseX, int mouseY ) + { + // Skip rendering labels. + } + + @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 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 + public boolean mouseReleased( double mouseX, double mouseY, int button ) + { + return (getFocused() != null && getFocused().mouseReleased( mouseX, mouseY, button )) || super.mouseReleased( x, y, button ); + } + + @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 removed() + { + super.removed(); + children.remove( terminal ); + terminal = null; + client.keyboard.setRepeatEvents( false ); + } + + @Override + public void tick() + { + super.tick(); + terminal.update(); + } +} diff --git a/remappedSrc/dan200/computercraft/client/gui/GuiDiskDrive.java b/remappedSrc/dan200/computercraft/client/gui/GuiDiskDrive.java new file mode 100644 index 000000000..773ac2c00 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/gui/GuiDiskDrive.java @@ -0,0 +1,44 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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 + public void render( @Nonnull MatrixStack transform, int mouseX, int mouseY, float partialTicks ) + { + renderBackground( transform ); + super.render( transform, mouseX, mouseY, partialTicks ); + drawMouseoverTooltip( transform, mouseX, mouseY ); + } + + @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 ); + } +} diff --git a/remappedSrc/dan200/computercraft/client/gui/GuiPrinter.java b/remappedSrc/dan200/computercraft/client/gui/GuiPrinter.java new file mode 100644 index 000000000..f0e29a49b --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/gui/GuiPrinter.java @@ -0,0 +1,57 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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 + 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 + 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 ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/client/gui/GuiPrintout.java b/remappedSrc/dan200/computercraft/client/gui/GuiPrintout.java new file mode 100644 index 000000000..261948e19 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/gui/GuiPrintout.java @@ -0,0 +1,152 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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 book; + private final int pages; + private final TextBuffer[] text; + private final TextBuffer[] colours; + private int page; + + public GuiPrintout( ContainerHeldItem container, PlayerInventory player, Text title ) + { + super( container, player, title ); + + backgroundHeight = Y_SIZE; + + String[] text = ItemPrintout.getText( container.getStack() ); + this.text = new TextBuffer[text.length]; + for( int i = 0; i < this.text.length; i++ ) + { + this.text[i] = new TextBuffer( text[i] ); + } + + String[] colours = ItemPrintout.getColours( container.getStack() ); + this.colours = new TextBuffer[colours.length]; + for( int i = 0; i < this.colours.length; i++ ) + { + this.colours[i] = new TextBuffer( colours[i] ); + } + + page = 0; + pages = Math.max( this.text.length / ItemPrintout.LINES_PER_PAGE, 1 ); + book = ((ItemPrintout) container.getStack() + .getItem()).getType() == ItemPrintout.Type.BOOK; + } + + @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( page < pages - 1 ) + { + page++; + } + return true; + } + + if( delta > 0 ) + { + // Scroll down goes to the previous page + if( page > 0 ) + { + page--; + } + return true; + } + + return false; + } + + @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. + } + + @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(), page, pages, book ); + drawText( matrix, renderer, x + X_TEXT_MARGIN, y + Y_TEXT_MARGIN, ItemPrintout.LINES_PER_PAGE * page, text, colours ); + renderer.draw(); + } + + @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( page < pages - 1 ) + { + page++; + } + return true; + } + + if( key == GLFW.GLFW_KEY_LEFT ) + { + if( page > 0 ) + { + page--; + } + return true; + } + + return false; + } +} diff --git a/remappedSrc/dan200/computercraft/client/gui/GuiTurtle.java b/remappedSrc/dan200/computercraft/client/gui/GuiTurtle.java new file mode 100644 index 000000000..af78829ad --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/gui/GuiTurtle.java @@ -0,0 +1,65 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.turtle.inventory.ContainerTurtle; +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 GuiTurtle extends GuiComputer +{ + 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 final ContainerTurtle container; + + public GuiTurtle( ContainerTurtle container, PlayerInventory player, Text title ) + { + super( container, player, title, ComputerCraft.turtleTermWidth, ComputerCraft.turtleTermHeight ); + + this.container = container; + } + + @Override + protected void init() + { + initTerminal( 8, 0, 80 ); + } + + @Override + public void drawBackground( @Nonnull MatrixStack transform, float partialTicks, int mouseX, int mouseY ) + { + // Draw term + Identifier texture = 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 = 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 ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/client/gui/widgets/WidgetTerminal.java b/remappedSrc/dan200/computercraft/client/gui/widgets/WidgetTerminal.java new file mode 100644 index 000000000..495a98a96 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/gui/widgets/WidgetTerminal.java @@ -0,0 +1,400 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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 net.minecraft.SharedConstants; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Element; +import org.lwjgl.glfw.GLFW; + +import java.util.BitSet; +import java.util.function.Supplier; + +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 final Supplier computer; + private final int termWidth; + private final int termHeight; + private final int leftMargin; + private final int rightMargin; + private final int topMargin; + private final int bottomMargin; + private final BitSet keysDown = new BitSet( 256 ); + private boolean focused; + private float terminateTimer = -1; + private float rebootTimer = -1; + private float shutdownTimer = -1; + private int lastMouseButton = -1; + private int lastMouseX = -1; + private int lastMouseY = -1; + + 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 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; + } + + @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 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 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; + } + + @Override + public boolean isMouseOver( double x, double y ) + { + return true; + } + + private void queueEvent( String event, Object... args ) + { + ClientComputer computer = this.computer.get(); + if( computer != null ) + { + computer.queueEvent( event, args ); + } + } + + 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(); + } + } + } + + private void queueEvent( String event ) + { + ClientComputer computer = this.computer.get(); + if( computer != null ) + { + computer.queueEvent( event ); + } + } + + 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 ); + } + } + } +} diff --git a/remappedSrc/dan200/computercraft/client/gui/widgets/WidgetWrapper.java b/remappedSrc/dan200/computercraft/client/gui/widgets/WidgetWrapper.java new file mode 100644 index 000000000..c805a16ce --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/gui/widgets/WidgetWrapper.java @@ -0,0 +1,106 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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 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 ); + } + + @Override + public boolean changeFocus( boolean b ) + { + return listener.changeFocus( b ); + } + + @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; + } + + public int getX() + { + return x; + } + + public int getY() + { + return y; + } + + public int getWidth() + { + return width; + } + + public int getHeight() + { + return height; + } +} diff --git a/remappedSrc/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java b/remappedSrc/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java new file mode 100644 index 000000000..fa7c0a8f1 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java @@ -0,0 +1,138 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client.proxy; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.ClientRegistry; +import dan200.computercraft.client.FrameInfo; +import dan200.computercraft.client.gui.*; +import dan200.computercraft.client.render.TileEntityMonitorRenderer; +import dan200.computercraft.client.render.TileEntityTurtleRenderer; +import dan200.computercraft.client.render.TurtleModelLoader; +import dan200.computercraft.client.render.TurtlePlayerRenderer; +import dan200.computercraft.fabric.events.ClientUnloadWorldEvent; +import dan200.computercraft.shared.ComputerCraftRegistry; +import dan200.computercraft.shared.common.ContainerHeldItem; +import dan200.computercraft.shared.common.IColouredItem; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.computer.inventory.ContainerComputer; +import dan200.computercraft.shared.computer.inventory.ContainerViewComputer; +import dan200.computercraft.shared.peripheral.diskdrive.ContainerDiskDrive; +import dan200.computercraft.shared.peripheral.monitor.ClientMonitor; +import dan200.computercraft.shared.peripheral.printer.ContainerPrinter; +import dan200.computercraft.shared.pocket.inventory.ContainerPocketComputer; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import dan200.computercraft.shared.turtle.inventory.ContainerTurtle; +import dan200.computercraft.shared.util.Config; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientBlockEntityEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.model.ModelLoadingRegistry; +import net.fabricmc.fabric.api.client.rendereregistry.v1.BlockEntityRendererRegistry; +import net.fabricmc.fabric.api.client.rendereregistry.v1.EntityRendererRegistry; +import net.fabricmc.fabric.api.client.screenhandler.v1.ScreenRegistry; +import net.fabricmc.fabric.api.event.client.ClientSpriteRegistryCallback; +import net.fabricmc.fabric.mixin.object.builder.ModelPredicateProviderRegistrySpecificAccessor; +import net.minecraft.client.item.ModelPredicateProvider; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.item.Item; +import net.minecraft.screen.PlayerScreenHandler; +import net.minecraft.util.Identifier; + +import java.util.function.Supplier; + +@Environment( EnvType.CLIENT ) +public final class ComputerCraftProxyClient implements ClientModInitializer +{ + + private static void initEvents() + { + ClientBlockEntityEvents.BLOCK_ENTITY_UNLOAD.register( ( blockEntity, world ) -> { + if( blockEntity instanceof TileGeneric ) + { + ((TileGeneric) blockEntity).onChunkUnloaded(); + } + } ); + + ClientUnloadWorldEvent.EVENT.register( () -> ClientMonitor.destroyAll() ); + + // Config + ClientLifecycleEvents.CLIENT_STARTED.register( Config::clientStarted ); + } + + @Override + public void onInitializeClient() + { + FrameInfo.init(); + registerContainers(); + + // While turtles themselves are not transparent, their upgrades may be. + BlockRenderLayerMap.INSTANCE.putBlock( ComputerCraftRegistry.ModBlocks.TURTLE_NORMAL, RenderLayer.getTranslucent() ); + BlockRenderLayerMap.INSTANCE.putBlock( ComputerCraftRegistry.ModBlocks.TURTLE_ADVANCED, RenderLayer.getTranslucent() ); + + // Monitors' textures have transparent fronts and so count as cutouts. + BlockRenderLayerMap.INSTANCE.putBlock( ComputerCraftRegistry.ModBlocks.MONITOR_NORMAL, RenderLayer.getCutout() ); + BlockRenderLayerMap.INSTANCE.putBlock( ComputerCraftRegistry.ModBlocks.MONITOR_ADVANCED, RenderLayer.getCutout() ); + + // Setup TESRs + BlockEntityRendererRegistry.INSTANCE.register( ComputerCraftRegistry.ModTiles.MONITOR_NORMAL, TileEntityMonitorRenderer::new ); + BlockEntityRendererRegistry.INSTANCE.register( ComputerCraftRegistry.ModTiles.MONITOR_ADVANCED, TileEntityMonitorRenderer::new ); + BlockEntityRendererRegistry.INSTANCE.register( ComputerCraftRegistry.ModTiles.TURTLE_NORMAL, TileEntityTurtleRenderer::new ); + BlockEntityRendererRegistry.INSTANCE.register( ComputerCraftRegistry.ModTiles.TURTLE_ADVANCED, TileEntityTurtleRenderer::new ); + + ClientSpriteRegistryCallback.event( PlayerScreenHandler.BLOCK_ATLAS_TEXTURE ) + .register( ClientRegistry::onTextureStitchEvent ); + ModelLoadingRegistry.INSTANCE.registerAppender( ClientRegistry::onModelBakeEvent ); + ModelLoadingRegistry.INSTANCE.registerResourceProvider( loader -> ( name, context ) -> TurtleModelLoader.INSTANCE.accepts( name ) ? + TurtleModelLoader.INSTANCE.loadModel( + name ) : null ); + + EntityRendererRegistry.INSTANCE.register( ComputerCraftRegistry.ModEntities.TURTLE_PLAYER, TurtlePlayerRenderer::new ); + + registerItemProperty( "state", + ( stack, world, player ) -> ItemPocketComputer.getState( stack ) + .ordinal(), + () -> ComputerCraftRegistry.ModItems.POCKET_COMPUTER_NORMAL, + () -> ComputerCraftRegistry.ModItems.POCKET_COMPUTER_ADVANCED ); + registerItemProperty( "state", + ( stack, world, player ) -> IColouredItem.getColourBasic( stack ) != -1 ? 1 : 0, + () -> ComputerCraftRegistry.ModItems.POCKET_COMPUTER_NORMAL, + () -> ComputerCraftRegistry.ModItems.POCKET_COMPUTER_ADVANCED ); + ClientRegistry.onItemColours(); + + initEvents(); + } + + // My IDE doesn't think so, but we do actually need these generics. + private static void registerContainers() + { + ScreenRegistry.>register( ComputerCraftRegistry.ModContainers.COMPUTER, GuiComputer::create ); + ScreenRegistry.>register( ComputerCraftRegistry.ModContainers.POCKET_COMPUTER, + GuiComputer::createPocket ); + ScreenRegistry.register( ComputerCraftRegistry.ModContainers.TURTLE, GuiTurtle::new ); + + ScreenRegistry.register( ComputerCraftRegistry.ModContainers.PRINTER, GuiPrinter::new ); + ScreenRegistry.register( ComputerCraftRegistry.ModContainers.DISK_DRIVE, GuiDiskDrive::new ); + ScreenRegistry.register( ComputerCraftRegistry.ModContainers.PRINTOUT, GuiPrintout::new ); + + ScreenRegistry.>register( ComputerCraftRegistry.ModContainers.VIEW_COMPUTER, + GuiComputer::createView ); + } + + @SafeVarargs + private static void registerItemProperty( String name, ModelPredicateProvider getter, Supplier... items ) + { + Identifier id = new Identifier( ComputerCraft.MOD_ID, name ); + for( Supplier item : items ) + { + ModelPredicateProviderRegistrySpecificAccessor.callRegister( item.get(), id, getter ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/client/render/CableHighlightRenderer.java b/remappedSrc/dan200/computercraft/client/render/CableHighlightRenderer.java new file mode 100644 index 000000000..2532e044f --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/CableHighlightRenderer.java @@ -0,0 +1,68 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client.render; + +import dan200.computercraft.shared.ComputerCraftRegistry; +import dan200.computercraft.shared.peripheral.modem.wired.BlockCable; +import dan200.computercraft.shared.peripheral.modem.wired.CableShapes; +import dan200.computercraft.shared.util.WorldUtil; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.block.BlockState; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.shape.VoxelShape; + +@Environment( EnvType.CLIENT ) +public final class CableHighlightRenderer +{ + private CableHighlightRenderer() + { + } + + public static boolean drawHighlight( MatrixStack stack, VertexConsumer consumer, Entity entity, double d, double e, double f, BlockPos pos, + BlockState state ) + { + Camera info = MinecraftClient.getInstance().gameRenderer.getCamera(); + + // We only care about instances with both cable and modem. + if( state.getBlock() != ComputerCraftRegistry.ModBlocks.CABLE || state.get( BlockCable.MODEM ) + .getFacing() == null || !state.get( BlockCable.CABLE ) ) + { + return false; + } + + VoxelShape shape = WorldUtil.isVecInside( CableShapes.getModemShape( state ), + new Vec3d( d, e, f ).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(); + Matrix4f matrix4f = stack.peek() + .getModel(); + shape.forEachEdge( ( x1, y1, z1, x2, y2, z2 ) -> { + consumer.vertex( matrix4f, (float) (x1 + xOffset), (float) (y1 + yOffset), (float) (z1 + zOffset) ) + .color( 0, 0, 0, 0.4f ) + .next(); + consumer.vertex( matrix4f, (float) (x2 + xOffset), (float) (y2 + yOffset), (float) (z2 + zOffset) ) + .color( 0, 0, 0, 0.4f ) + .next(); + } ); + + return true; + } +} diff --git a/remappedSrc/dan200/computercraft/client/render/ComputerBorderRenderer.java b/remappedSrc/dan200/computercraft/client/render/ComputerBorderRenderer.java new file mode 100644 index 000000000..65166b05d --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/ComputerBorderRenderer.java @@ -0,0 +1,174 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.client.render; + +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.ComputerFamily; +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; +import org.lwjgl.opengl.GL11; + +import javax.annotation.Nonnull; + +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" ); + /** + * 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 Matrix4f IDENTITY = new Matrix4f(); + 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 LIGHT_BORDER_Y = 56; + private static final int LIGHT_CORNER_Y = 80; + + public static final int LIGHT_HEIGHT = 8; + private static final float TEX_SCALE = 1 / 256.0f; + + static + { + IDENTITY.loadIdentity(); + } + + 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, false, r, g, b ); + } + + public static void render( Matrix4f transform, VertexConsumer buffer, int x, int y, int z, int width, int height, boolean withLight, float r, float g, float b ) + { + new ComputerBorderRenderer( transform, buffer, z, r, g, b ).doRender( x, y, width, height, withLight ); + } + + public void doRender( int x, int y, int width, int height, boolean withLight ) + { + 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( withLight ) + { + renderTexture( x, endY, 0, LIGHT_BORDER_Y, endX - x, BORDER + LIGHT_HEIGHT, BORDER, BORDER + LIGHT_HEIGHT ); + renderTexture( x - BORDER, endY, CORNER_LEFT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT ); + renderTexture( endX, endY, CORNER_RIGHT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT ); + } + else + { + 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 ); + } + } + + 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 renderCorner( int x, int y, int u, int v ) + { + renderTexture( x, y, u, v, BORDER, BORDER, 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/remappedSrc/dan200/computercraft/client/render/ItemMapLikeRenderer.java b/remappedSrc/dan200/computercraft/client/render/ItemMapLikeRenderer.java new file mode 100644 index 000000000..6ecd99254 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/ItemMapLikeRenderer.java @@ -0,0 +1,149 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client.render; + +import dan200.computercraft.fabric.mixin.HeldItemRendererAccess; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +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.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Arm; +import net.minecraft.util.Hand; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3f; + +@Environment( EnvType.CLIENT ) +public abstract class ItemMapLikeRenderer +{ + public 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(); + } + + /** + * 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 + */ + 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 ); + + HeldItemRendererAccess access = (HeldItemRendererAccess) renderer; + float pitchAngle = access.callGetMapAngle( pitch ); + transform.translate( 0, 0.04F + equipProgress * -1.2f + pitchAngle * -0.5f, -0.72f ); + transform.multiply( Vec3f.POSITIVE_X.getDegreesQuaternion( pitchAngle * -85.0f ) ); + if( !minecraft.player.isInvisible() ) + { + transform.push(); + transform.multiply( Vec3f.POSITIVE_Y.getDegreesQuaternion( 90.0F ) ); + access.callRenderArm( transform, render, combinedLight, Arm.RIGHT ); + access.callRenderArm( transform, render, combinedLight, Arm.LEFT ); + transform.pop(); + } + + float rX = MathHelper.sin( swingRt * (float) Math.PI ); + transform.multiply( Vec3f.POSITIVE_X.getDegreesQuaternion( rX * 20.0F ) ); + transform.scale( 2.0F, 2.0F, 2.0F ); + + renderItem( transform, render, stack ); + } + + /** + * 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 + */ + 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( Vec3f.POSITIVE_Z.getDegreesQuaternion( offset * 10f ) ); + ((HeldItemRendererAccess) minecraft.getHeldItemRenderer()) + .callRenderArmHoldingItem( 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( Vec3f.POSITIVE_X.getDegreesQuaternion( f2 * -45f ) ); + transform.multiply( Vec3f.POSITIVE_Y.getDegreesQuaternion( offset * f2 * -30f ) ); + + renderItem( transform, render, stack ); + + transform.pop(); + } + + /** + * 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 + */ + protected abstract void renderItem( MatrixStack transform, VertexConsumerProvider render, ItemStack stack ); +} diff --git a/remappedSrc/dan200/computercraft/client/render/ItemPocketRenderer.java b/remappedSrc/dan200/computercraft/client/render/ItemPocketRenderer.java new file mode 100644 index 000000000..ec96c0d81 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/ItemPocketRenderer.java @@ -0,0 +1,151 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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.item.ItemStack; +import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Vec3f; +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.*; + +/** + * Emulates map rendering for pocket computers. + */ +public final class ItemPocketRenderer extends ItemMapLikeRenderer +{ + public static final ItemPocketRenderer INSTANCE = new ItemPocketRenderer(); + + private ItemPocketRenderer() + { + } + + @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( Vec3f.POSITIVE_Y.getDegreesQuaternion( 180f ) ); + transform.multiply( Vec3f.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 ) + { + RenderSystem.enableBlend(); + 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, true, r, g, b ); + + tessellator.draw(); + } + + private static void renderLight( Matrix4f transform, int colour, int width, int height ) + { + 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/remappedSrc/dan200/computercraft/client/render/ItemPrintoutRenderer.java b/remappedSrc/dan200/computercraft/client/render/ItemPrintoutRenderer.java new file mode 100644 index 000000000..e45ec3ef4 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/ItemPrintoutRenderer.java @@ -0,0 +1,96 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client.render; + +import dan200.computercraft.shared.media.items.ItemPrintout; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Vec3f; + +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. + */ +public final class ItemPrintoutRenderer extends ItemMapLikeRenderer +{ + public static final ItemPrintoutRenderer INSTANCE = new ItemPrintoutRenderer(); + + private ItemPrintoutRenderer() + { + } + + @Override + protected void renderItem( MatrixStack transform, VertexConsumerProvider render, ItemStack stack ) + { + transform.multiply( Vec3f.POSITIVE_X.getDegreesQuaternion( 180f ) ); + transform.scale( 0.42f, 0.42f, -0.42f ); + transform.translate( -0.5f, -0.48f, 0.0f ); + + drawPrintout( transform, render, 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 ) ); + } + + public boolean renderInFrame( MatrixStack matrixStack, VertexConsumerProvider consumerProvider, ItemStack stack ) + { + if( !(stack.getItem() instanceof ItemPrintout) ) + { + return false; + } + + // Move a little bit forward to ensure we're not clipping with the frame + matrixStack.translate( 0.0f, 0.0f, -0.001f ); + matrixStack.multiply( Vec3f.POSITIVE_Z.getDegreesQuaternion( 180f ) ); + matrixStack.scale( 0.95f, 0.95f, -0.95f ); + matrixStack.translate( -0.5f, -0.5f, 0.0f ); + + drawPrintout( matrixStack, consumerProvider, stack ); + + return true; + } +} diff --git a/remappedSrc/dan200/computercraft/client/render/ModelTransformer.java b/remappedSrc/dan200/computercraft/client/render/ModelTransformer.java new file mode 100644 index 000000000..ce01b25b6 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/ModelTransformer.java @@ -0,0 +1,97 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.client.render; + +import dan200.computercraft.fabric.mixin.BakedQuadAccess; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.render.VertexFormat; +import net.minecraft.client.render.VertexFormatElement; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.render.model.BakedQuad; +import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Vector4f; +import java.util.List; + +/** + * Transforms vertices of a model, remaining aware of winding order, and rearranging vertices if needed. + */ +@Environment( EnvType.CLIENT ) +public final class ModelTransformer +{ + private static final Matrix4f identity; + + static + { + identity = new Matrix4f(); + identity.loadIdentity(); + } + + private ModelTransformer() + { + } + + public static void transformQuadsTo( List output, List input, Matrix4f transform ) + { + transformQuadsTo( VertexFormats.POSITION_COLOR_TEXTURE_LIGHT_NORMAL, output, input, transform ); + } + + public static void transformQuadsTo( VertexFormat format, List output, List input, Matrix4f transform ) + { + if( transform == null || transform.equals( identity ) ) + { + output.addAll( input ); + } + else + { + for( BakedQuad quad : input ) + { + output.add( doTransformQuad( format, quad, transform ) ); + } + } + } + + private static BakedQuad doTransformQuad( VertexFormat format, BakedQuad quad, Matrix4f transform ) + { + int[] vertexData = quad.getVertexData().clone(); + BakedQuad copy = new BakedQuad( vertexData, -1, quad.getFace(), ((BakedQuadAccess) quad).getSprite(), true ); + + int offsetBytes = 0; + for( int v = 0; v < 4; ++v ) + { + for( VertexFormatElement element : format.getElements() ) // For each vertex element + { + int start = offsetBytes / Integer.BYTES; + if( element.getType() == VertexFormatElement.Type.POSITION && element.getDataType() == VertexFormatElement.DataType.FLOAT ) // When we find a position element + { + Vector4f pos = new Vector4f( Float.intBitsToFloat( vertexData[start] ), + Float.intBitsToFloat( vertexData[start + 1] ), + Float.intBitsToFloat( vertexData[start + 2] ), + 1 ); + + // Transform the position + pos.transform( transform ); + + // Insert the position + vertexData[start] = Float.floatToRawIntBits( pos.getX() ); + vertexData[start + 1] = Float.floatToRawIntBits( pos.getY() ); + vertexData[start + 2] = Float.floatToRawIntBits( pos.getZ() ); + } + offsetBytes += element.getByteLength(); + } + } + return copy; + } + + public static BakedQuad transformQuad( VertexFormat format, BakedQuad input, Matrix4f transform ) + { + if( transform == null || transform.equals( identity ) ) + { + return input; + } + return doTransformQuad( format, input, transform ); + } +} diff --git a/remappedSrc/dan200/computercraft/client/render/MonitorHighlightRenderer.java b/remappedSrc/dan200/computercraft/client/render/MonitorHighlightRenderer.java new file mode 100644 index 000000000..d0117b2ac --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/MonitorHighlightRenderer.java @@ -0,0 +1,153 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client.render; + +import dan200.computercraft.shared.peripheral.monitor.TileMonitor; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.Entity; +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 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. + */ +@Environment( EnvType.CLIENT ) +public final class MonitorHighlightRenderer +{ + private MonitorHighlightRenderer() + { + } + + public static boolean drawHighlight( + MatrixStack matrixStack, VertexConsumer vertexConsumer, Entity entity, double d, double e, double f, BlockPos pos, BlockState blockState + ) + { + // Preserve normal behaviour when crouching. + if( entity.isInSneakingPose() ) + { + return false; + } + + World world = entity.getEntityWorld(); + + BlockEntity tile = world.getBlockEntity( pos ); + if( !(tile instanceof TileMonitor) ) + { + return false; + } + + TileMonitor monitor = (TileMonitor) tile; + + // 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() ); + } + + Vec3d cameraPos = MinecraftClient.getInstance().gameRenderer.getCamera() + .getPos(); + matrixStack.push(); + matrixStack.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 + Matrix4f transform = matrixStack.peek() + .getModel(); + if( faces.contains( NORTH ) || faces.contains( WEST ) ) + { + line( vertexConsumer, transform, 0, 0, 0, UP ); + } + if( faces.contains( SOUTH ) || faces.contains( WEST ) ) + { + line( vertexConsumer, transform, 0, 0, 1, UP ); + } + if( faces.contains( NORTH ) || faces.contains( EAST ) ) + { + line( vertexConsumer, transform, 1, 0, 0, UP ); + } + if( faces.contains( SOUTH ) || faces.contains( EAST ) ) + { + line( vertexConsumer, transform, 1, 0, 1, UP ); + } + if( faces.contains( NORTH ) || faces.contains( DOWN ) ) + { + line( vertexConsumer, transform, 0, 0, 0, EAST ); + } + if( faces.contains( SOUTH ) || faces.contains( DOWN ) ) + { + line( vertexConsumer, transform, 0, 0, 1, EAST ); + } + if( faces.contains( NORTH ) || faces.contains( UP ) ) + { + line( vertexConsumer, transform, 0, 1, 0, EAST ); + } + if( faces.contains( SOUTH ) || faces.contains( UP ) ) + { + line( vertexConsumer, transform, 0, 1, 1, EAST ); + } + if( faces.contains( WEST ) || faces.contains( DOWN ) ) + { + line( vertexConsumer, transform, 0, 0, 0, SOUTH ); + } + if( faces.contains( EAST ) || faces.contains( DOWN ) ) + { + line( vertexConsumer, transform, 1, 0, 0, SOUTH ); + } + if( faces.contains( WEST ) || faces.contains( UP ) ) + { + line( vertexConsumer, transform, 0, 1, 0, SOUTH ); + } + if( faces.contains( EAST ) || faces.contains( UP ) ) + { + line( vertexConsumer, transform, 1, 1, 0, SOUTH ); + } + + matrixStack.pop(); + + return true; + } + + 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/remappedSrc/dan200/computercraft/client/render/MonitorTextureBufferShader.java b/remappedSrc/dan200/computercraft/client/render/MonitorTextureBufferShader.java new file mode 100644 index 000000000..405f056b8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/MonitorTextureBufferShader.java @@ -0,0 +1,184 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 net.minecraft.client.texture.TextureUtil; +import net.minecraft.util.math.Matrix4f; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL20; + +import java.io.InputStream; +import java.nio.FloatBuffer; + +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.writeColumnMajor( MATRIX_BUFFER ); + MATRIX_BUFFER.rewind(); + RenderSystem.glUniformMatrix4( uniformMv, false, MATRIX_BUFFER ); + + RenderSystem.glUniform1i( uniformWidth, width ); + RenderSystem.glUniform1i( uniformHeight, height ); + + 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/remappedSrc/dan200/computercraft/client/render/PrintoutRenderer.java b/remappedSrc/dan200/computercraft/client/render/PrintoutRenderer.java new file mode 100644 index 000000000..489506aae --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/PrintoutRenderer.java @@ -0,0 +1,209 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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.*; +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 +{ + /** + * 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; + /** + * Size of the leather cover. + */ + public static final int COVER_SIZE = 12; + private static final Identifier BG = new Identifier( "computercraft", "textures/gui/printout.png" ); + private static final float BG_SIZE = 256.0f; + /** + * Width of the extra page texture. + */ + private static final int X_FOLD_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 ); + } + } + + public static float offsetAt( int page ) + { + return (float) (32 * (1 - Math.pow( 1.2, -page ))); + } + + 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(); + } + + 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/remappedSrc/dan200/computercraft/client/render/TileEntityMonitorRenderer.java b/remappedSrc/dan200/computercraft/client/render/TileEntityMonitorRenderer.java new file mode 100644 index 000000000..bc7ce88d0 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/TileEntityMonitorRenderer.java @@ -0,0 +1,263 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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.*; +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.MatrixStack; +import net.minecraft.util.math.AffineTransformation; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Vec3f; +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 final Matrix4f IDENTITY = AffineTransformation.identity() + .getMatrix(); + private static ByteBuffer tboContents; + + 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( Vec3f.NEGATIVE_Y.getDegreesQuaternion( yaw ) ); + transform.multiply( Vec3f.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.50 ); + 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 background blocker + 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) ); + + // Set the contents slightly off the surface to prevent z-fighting + transform.translate( 0.0, 0.0, 0.001 ); + + // 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() ); + + // To go along with sneaky hack above: make sure state changes are undone. I would have thought this would + // happen automatically after these buffers are drawn, but chests will render weird around monitors without this. + FixedWidthFontRenderer.TYPE.endDrawing(); + + transform.pop(); + } + else + { + FixedWidthFontRenderer.drawEmptyTerminal( transform.peek() + .getModel(), + renderer, + -MARGIN, + MARGIN, + (float) (xSize + 2 * MARGIN), + (float) -(ySize + 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/remappedSrc/dan200/computercraft/client/render/TileEntityTurtleRenderer.java b/remappedSrc/dan200/computercraft/client/render/TileEntityTurtleRenderer.java new file mode 100644 index 000000000..67ad4c76a --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/TileEntityTurtleRenderer.java @@ -0,0 +1,223 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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.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.minecraft.util.math.Vec3f; +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; + } + + 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.quad( matrix, + bakedquad, + new float[] { 1.0F, 1.0F, 1.0F, 1.0F }, + f, + f1, + f2, + new int[] { lightmapCoord, lightmapCoord, lightmapCoord, lightmapCoord }, + overlayLight, + true ); + } + } + + @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 = mc.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( Vec3f.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(); + } + + public static 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( Vec3f.NEGATIVE_X.getDegreesQuaternion( toolAngle ) ); + transform.translate( 0.0f, -0.5f, -0.5f ); + + TransformedModel model = upgrade.getModel( turtle.getAccess(), side ); + model.push( transform ); + TileEntityTurtleRenderer.renderModel( transform, renderer, lightmapCoord, overlayLight, model.getModel(), null ); + transform.pop(); + + transform.pop(); + } + + public static 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 ); + } + + public static void renderModel( @Nonnull MatrixStack transform, @Nonnull VertexConsumer renderer, int lightmapCoord, int overlayLight, BakedModel model, + int[] tints ) + { + Random random = new Random(); + random.setSeed( 0 ); + renderQuads( transform, renderer, lightmapCoord, overlayLight, model.getQuads( null, null, random ), tints ); + for( Direction facing : DirectionUtil.FACINGS ) + { + renderQuads( transform, renderer, lightmapCoord, overlayLight, model.getQuads( null, facing, random ), tints ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/client/render/TurtleModelLoader.java b/remappedSrc/dan200/computercraft/client/render/TurtleModelLoader.java new file mode 100644 index 000000000..1882ac2bd --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/TurtleModelLoader.java @@ -0,0 +1,104 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client.render; + +import com.mojang.datafixers.util.Pair; +import dan200.computercraft.ComputerCraft; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +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.texture.Sprite; +import net.minecraft.client.util.SpriteIdentifier; +import net.minecraft.util.Identifier; + +import javax.annotation.Nonnull; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Environment( EnvType.CLIENT ) +public final class TurtleModelLoader +{ + public static final TurtleModelLoader INSTANCE = new TurtleModelLoader(); + private static final Identifier NORMAL_TURTLE_MODEL = new Identifier( ComputerCraft.MOD_ID, "block/turtle_normal" ); + private static final Identifier ADVANCED_TURTLE_MODEL = new Identifier( ComputerCraft.MOD_ID, "block/turtle_advanced" ); + private static final Identifier COLOUR_TURTLE_MODEL = new Identifier( ComputerCraft.MOD_ID, "block/turtle_colour" ); + + private TurtleModelLoader() + { + } + + public boolean accepts( @Nonnull Identifier name ) + { + return name.getNamespace() + .equals( ComputerCraft.MOD_ID ) && (name.getPath() + .equals( "item/turtle_normal" ) || name.getPath() + .equals( "item/turtle_advanced" )); + } + + @Nonnull + public UnbakedModel loadModel( @Nonnull Identifier name ) + { + if( name.getNamespace() + .equals( ComputerCraft.MOD_ID ) ) + { + switch( name.getPath() ) + { + case "item/turtle_normal": + return new TurtleModel( NORMAL_TURTLE_MODEL ); + case "item/turtle_advanced": + return new TurtleModel( ADVANCED_TURTLE_MODEL ); + } + } + + throw new IllegalStateException( "Loader does not accept " + name ); + } + + private static final class TurtleModel implements UnbakedModel + { + private final Identifier family; + + private TurtleModel( Identifier family ) + { + this.family = family; + } + + @Override + public Collection getTextureDependencies( Function modelGetter, + Set> missingTextureErrors ) + { + return getModelDependencies() + .stream() + .flatMap( x -> modelGetter.apply( x ) + .getTextureDependencies( modelGetter, missingTextureErrors ) + .stream() ) + .collect( Collectors.toSet() ); + } + + @Nonnull + @Override + public Collection getModelDependencies() + { + return Arrays.asList( family, COLOUR_TURTLE_MODEL ); + } + + @Override + public BakedModel bake( @Nonnull ModelLoader loader, @Nonnull Function spriteGetter, @Nonnull ModelBakeSettings state, + Identifier modelId ) + { + return new TurtleSmartItemModel( loader.getOrLoadModel( family ) + .bake( loader, spriteGetter, state, modelId ), + loader.getOrLoadModel( COLOUR_TURTLE_MODEL ) + .bake( loader, spriteGetter, state, modelId ) ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/client/render/TurtleMultiModel.java b/remappedSrc/dan200/computercraft/client/render/TurtleMultiModel.java new file mode 100644 index 000000000..f6803af3e --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/TurtleMultiModel.java @@ -0,0 +1,142 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.client.render; + +import dan200.computercraft.api.client.TransformedModel; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +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.render.model.json.ModelTransformation; +import net.minecraft.client.texture.Sprite; +import net.minecraft.util.math.AffineTransformation; +import net.minecraft.util.math.Direction; + +import javax.annotation.Nonnull; +import java.util.*; + +@Environment( EnvType.CLIENT ) +public class TurtleMultiModel implements BakedModel +{ + private final BakedModel baseModel; + private final BakedModel overlayModel; + private final AffineTransformation generalTransform; + private final TransformedModel leftUpgradeModel; + private final TransformedModel rightUpgradeModel; + private List generalQuads = null; + private Map> faceQuads = new EnumMap<>( Direction.class ); + + public TurtleMultiModel( BakedModel baseModel, BakedModel overlayModel, AffineTransformation generalTransform, TransformedModel leftUpgradeModel, + TransformedModel rightUpgradeModel ) + { + // Get the models + this.baseModel = baseModel; + this.overlayModel = overlayModel; + this.leftUpgradeModel = leftUpgradeModel; + this.rightUpgradeModel = rightUpgradeModel; + this.generalTransform = generalTransform; + } + + @Nonnull + @Override + public List getQuads( BlockState state, Direction side, @Nonnull Random rand ) + { + if( side != null ) + { + if( !faceQuads.containsKey( side ) ) + { + faceQuads.put( side, buildQuads( state, side, rand ) ); + } + return faceQuads.get( side ); + } + else + { + if( generalQuads == null ) + { + generalQuads = buildQuads( state, side, rand ); + } + return generalQuads; + } + } + + private List buildQuads( BlockState state, Direction side, Random rand ) + { + ArrayList quads = new ArrayList<>(); + + + ModelTransformer.transformQuadsTo( quads, baseModel.getQuads( state, side, rand ), generalTransform.getMatrix() ); + if( overlayModel != null ) + { + ModelTransformer.transformQuadsTo( quads, overlayModel.getQuads( state, side, rand ), generalTransform.getMatrix() ); + } + if( leftUpgradeModel != null ) + { + AffineTransformation upgradeTransform = generalTransform.multiply( leftUpgradeModel.getMatrix() ); + ModelTransformer.transformQuadsTo( quads, leftUpgradeModel.getModel() + .getQuads( state, side, rand ), + upgradeTransform.getMatrix() ); + } + if( rightUpgradeModel != null ) + { + AffineTransformation upgradeTransform = generalTransform.multiply( rightUpgradeModel.getMatrix() ); + ModelTransformer.transformQuadsTo( quads, rightUpgradeModel.getModel() + .getQuads( state, side, rand ), + upgradeTransform.getMatrix() ); + } + quads.trimToSize(); + return quads; + } + + @Override + public boolean useAmbientOcclusion() + { + return baseModel.useAmbientOcclusion(); + } + + @Override + public boolean hasDepth() + { + return baseModel.hasDepth(); + } + + @Override + public boolean isSideLit() + { + return baseModel.isSideLit(); + } + + @Override + public boolean isBuiltin() + { + return baseModel.isBuiltin(); + } + + @Nonnull + @Override + @Deprecated + public Sprite getSprite() + { + return baseModel.getSprite(); + } + + @Nonnull + @Override + @Deprecated + public ModelTransformation getTransformation() + { + return baseModel.getTransformation(); + } + + @Nonnull + @Override + public ModelOverrideList getOverrides() + { + return ModelOverrideList.EMPTY; + } +} diff --git a/remappedSrc/dan200/computercraft/client/render/TurtlePlayerRenderer.java b/remappedSrc/dan200/computercraft/client/render/TurtlePlayerRenderer.java new file mode 100644 index 000000000..636cc4b4d --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/TurtlePlayerRenderer.java @@ -0,0 +1,42 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.client.render; + +import dan200.computercraft.shared.turtle.core.TurtlePlayer; +import net.fabricmc.fabric.api.client.rendereregistry.v1.EntityRendererRegistry; +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; + +import javax.annotation.Nonnull; + +public class TurtlePlayerRenderer extends EntityRenderer +{ + public TurtlePlayerRenderer( EntityRenderDispatcher renderManager ) + { + super( renderManager ); + } + + public TurtlePlayerRenderer( EntityRenderDispatcher entityRenderDispatcher, EntityRendererRegistry.Context context ) + { + super( entityRenderDispatcher ); + } + + @Override + public void render( @Nonnull TurtlePlayer entityIn, float entityYaw, float partialTicks, @Nonnull MatrixStack transform, + @Nonnull VertexConsumerProvider buffer, int packedLightIn ) + { + } + + @Nonnull + @Override + public Identifier getTexture( @Nonnull TurtlePlayer entity ) + { + return ComputerBorderRenderer.BACKGROUND_NORMAL; + } +} diff --git a/remappedSrc/dan200/computercraft/client/render/TurtleSmartItemModel.java b/remappedSrc/dan200/computercraft/client/render/TurtleSmartItemModel.java new file mode 100644 index 000000000..e632e4840 --- /dev/null +++ b/remappedSrc/dan200/computercraft/client/render/TurtleSmartItemModel.java @@ -0,0 +1,221 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.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.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +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.texture.Sprite; +import net.minecraft.client.util.ModelIdentifier; +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.AffineTransformation; +import net.minecraft.util.math.Direction; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Random; + +@Environment( EnvType.CLIENT ) +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 final BakedModel familyModel; + private final BakedModel colourModel; + private final HashMap cachedModels = new HashMap<>(); + private final ModelOverrideList overrides; + + public TurtleSmartItemModel( BakedModel familyModel, BakedModel colourModel ) + { + this.familyModel = familyModel; + this.colourModel = colourModel; + + // this actually works I think, trust me + overrides = new ModelOverrideList( null, null, null, Collections.emptyList() ) + { + @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 ); + // TODO make upside down turtle items render properly (currently inivisible) + //boolean flip = label != null && (label.equals("Dinnerbone") || label.equals("Grumm")); + boolean flip = false; + TurtleModelCombination combo = new TurtleModelCombination( colour != -1, leftUpgrade, rightUpgrade, overlay, christmas, flip ); + + BakedModel model = cachedModels.get( combo ); + if( model == null ) + { + cachedModels.put( combo, model = buildModel( combo ) ); + } + return model; + } + }; + } + + private BakedModel buildModel( TurtleModelCombination combo ) + { + MinecraftClient mc = MinecraftClient.getInstance(); + BakedModelManager modelManager = mc.getItemRenderer() + .getModels() + .getModelManager(); + ModelIdentifier overlayModelLocation = TileEntityTurtleRenderer.getTurtleOverlayModel( combo.overlay, combo.christmas ); + + BakedModel baseModel = combo.colour ? colourModel : familyModel; + BakedModel overlayModel = overlayModelLocation != null ? modelManager.getModel( overlayModelLocation ) : null; + AffineTransformation transform = combo.flip ? flip : identity; + TransformedModel leftModel = combo.leftUpgrade != null ? combo.leftUpgrade.getModel( null, TurtleSide.LEFT ) : null; + TransformedModel rightModel = combo.rightUpgrade != null ? combo.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 ); + } + + @Override + public boolean useAmbientOcclusion() + { + return familyModel.useAmbientOcclusion(); + } + + @Override + public boolean hasDepth() + { + return familyModel.hasDepth(); + } + + @Override + public boolean isSideLit() + { + return familyModel.isSideLit(); + } + + @Override + public boolean isBuiltin() + { + return familyModel.isBuiltin(); + } + + @Nonnull + @Override + @Deprecated + public Sprite getSprite() + { + return familyModel.getSprite(); + } + + @Nonnull + @Override + @Deprecated + public ModelTransformation getTransformation() + { + return familyModel.getTransformation(); + } + + @Nonnull + @Override + public ModelOverrideList getOverrides() + { + return overrides; + } + + private static class TurtleModelCombination + { + final boolean colour; + final ITurtleUpgrade leftUpgrade; + final ITurtleUpgrade rightUpgrade; + final Identifier overlay; + final boolean christmas; + final boolean flip; + + TurtleModelCombination( boolean colour, ITurtleUpgrade leftUpgrade, ITurtleUpgrade rightUpgrade, Identifier overlay, boolean christmas, + boolean flip ) + { + this.colour = colour; + this.leftUpgrade = leftUpgrade; + this.rightUpgrade = rightUpgrade; + this.overlay = overlay; + this.christmas = christmas; + this.flip = flip; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 0; + result = prime * result + (colour ? 1 : 0); + result = prime * result + (leftUpgrade != null ? leftUpgrade.hashCode() : 0); + result = prime * result + (rightUpgrade != null ? rightUpgrade.hashCode() : 0); + result = prime * result + (overlay != null ? overlay.hashCode() : 0); + result = prime * result + (christmas ? 1 : 0); + result = prime * result + (flip ? 1 : 0); + return result; + } + + @Override + public boolean equals( Object other ) + { + if( other == this ) + { + return true; + } + if( !(other instanceof TurtleModelCombination) ) + { + return false; + } + + TurtleModelCombination otherCombo = (TurtleModelCombination) other; + return otherCombo.colour == colour && otherCombo.leftUpgrade == leftUpgrade && otherCombo.rightUpgrade == rightUpgrade && Objects.equal( + otherCombo.overlay, overlay ) && otherCombo.christmas == christmas && otherCombo.flip == flip; + } + } + +} diff --git a/remappedSrc/dan200/computercraft/core/apis/ApiFactories.java b/remappedSrc/dan200/computercraft/core/apis/ApiFactories.java new file mode 100644 index 000000000..0f801e865 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/ApiFactories.java @@ -0,0 +1,35 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/apis/ComputerAccess.java b/remappedSrc/dan200/computercraft/core/apis/ComputerAccess.java new file mode 100644 index 000000000..39c4a2ff3 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/ComputerAccess.java @@ -0,0 +1,147 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 environment; + private final Set mounts = new HashSet<>(); + + protected ComputerAccess( IAPIEnvironment environment ) + { + this.environment = environment; + } + + public void unmountAll() + { + FileSystem fileSystem = environment.getFileSystem(); + for( String mount : mounts ) + { + fileSystem.unmount( mount ); + } + 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 = 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 ) 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 = 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 ) mounts.add( location ); + return location; + } + + @Override + public void unmount( String location ) + { + if( location == null ) return; + if( !mounts.contains( location ) ) throw new IllegalStateException( "You didn't mount this location" ); + + environment.getFileSystem().unmount( location ); + mounts.remove( location ); + } + + @Override + public int getID() + { + return environment.getComputerID(); + } + + @Override + public void queueEvent( @Nonnull String event, Object... arguments ) + { + Objects.requireNonNull( event, "event cannot be null" ); + environment.queueEvent( event, arguments ); + } + + @Nonnull + @Override + public IWorkMonitor getMainThreadMonitor() + { + return environment.getMainThreadMonitor(); + } + + private String findFreeLocation( String desiredLoc ) + { + try + { + FileSystem fileSystem = 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/remappedSrc/dan200/computercraft/core/apis/FSAPI.java b/remappedSrc/dan200/computercraft/core/apis/FSAPI.java new file mode 100644 index 000000000..6d1e5f2f6 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/FSAPI.java @@ -0,0 +1,508 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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.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 several parts of a path into one full path, adding separators as + * needed. + * + * @param arguments The paths to combine. + * @return The new path, with separators added between parts as needed. + * @throws LuaException On argument errors. + * @cc.tparam string path The first part of the path. For example, a parent directory path. + * @cc.tparam string ... Additional parts of the path to combine. + */ + @LuaFunction + public final String combine( IArguments arguments ) throws LuaException + { + StringBuilder result = new StringBuilder(); + result.append( FileSystem.sanitizePath( arguments.getString( 0 ), true ) ); + + for( int i = 1, n = arguments.count(); i < n; i++ ) + { + String part = FileSystem.sanitizePath( arguments.getString( i ), true ); + if( result.length() != 0 && !part.isEmpty() ) result.append( '/' ); + result.append( part ); + } + + return FileSystem.sanitizePath( result.toString(), true ); + } + + /** + * 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, + * when it was created and last modified, and whether it is read only. + * + * 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, isReadOnly = 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() ); + result.put( "isReadOnly", fileSystem.isReadOnly( path ) ); + 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/remappedSrc/dan200/computercraft/core/apis/FastLuaException.java b/remappedSrc/dan200/computercraft/core/apis/FastLuaException.java new file mode 100644 index 000000000..45e4db0b2 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/FastLuaException.java @@ -0,0 +1,34 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/apis/HTTPAPI.java b/remappedSrc/dan200/computercraft/core/apis/HTTPAPI.java new file mode 100644 index 000000000..2ba8f744b --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/HTTPAPI.java @@ -0,0 +1,214 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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.HttpHeaderNames; +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 apiEnvironment; + + private final ResourceGroup checkUrls = new ResourceGroup<>( ResourceGroup.DEFAULT ); + private final ResourceGroup requests = new ResourceQueue<>( () -> ComputerCraft.httpMaxRequests ); + private final ResourceGroup websockets = new ResourceGroup<>( () -> ComputerCraft.httpMaxWebsockets ); + + public HTTPAPI( IAPIEnvironment environment ) + { + 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, apiEnvironment, address, postString, headers, binary, redirect ); + + // Make the request + if( !request.queue( r -> r.request( uri, httpMethod ) ) ) + { + throw new LuaException( "Too many ongoing HTTP requests" ); + } + + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; + } + } + + @LuaFunction + public final Object[] checkURL( String address ) throws LuaException + { + try + { + URI uri = HttpRequest.checkUri( address ); + if( !new CheckUrl( checkUrls, apiEnvironment, address, uri ).queue( CheckUrl::run ) ) + { + throw new LuaException( "Too many ongoing checkUrl calls" ); + } + + 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, 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 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() ); + } + } + } + + if( !headers.contains( HttpHeaderNames.USER_AGENT ) ) + { + headers.set( HttpHeaderNames.USER_AGENT, apiEnvironment.getComputerEnvironment().getUserAgent() ); + } + return headers; + } +} diff --git a/remappedSrc/dan200/computercraft/core/apis/IAPIEnvironment.java b/remappedSrc/dan200/computercraft/core/apis/IAPIEnvironment.java new file mode 100644 index 000000000..25960c580 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/IAPIEnvironment.java @@ -0,0 +1,79 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/apis/LuaDateTime.java b/remappedSrc/dan200/computercraft/core/apis/LuaDateTime.java new file mode 100644 index 000000000..3293d2b7c --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/LuaDateTime.java @@ -0,0 +1,280 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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, 2 ); + 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/remappedSrc/dan200/computercraft/core/apis/OSAPI.java b/remappedSrc/dan200/computercraft/core/apis/OSAPI.java new file mode 100644 index 000000000..0ef4899bc --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/OSAPI.java @@ -0,0 +1,469 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 alarms = new Int2ObjectOpenHashMap<>(); + private int clock; + private double time; + private int day; + + private int nextAlarmToken = 0; + + private static class Alarm implements Comparable + { + final double time; + final int day; + + Alarm( double time, int day ) + { + this.time = time; + this.day = day; + } + + @Override + public int compareTo( @Nonnull Alarm o ) + { + double t = day * 24.0 + time; + double ot = day * 24.0 + time; + return Double.compare( t, ot ); + } + } + + public OSAPI( IAPIEnvironment environment ) + { + apiEnvironment = environment; + } + + @Override + public String[] getNames() + { + return new String[] { "os" }; + } + + @Override + public void startup() + { + time = apiEnvironment.getComputerEnvironment().getTimeOfDay(); + day = apiEnvironment.getComputerEnvironment().getDay(); + clock = 0; + + synchronized( alarms ) + { + alarms.clear(); + } + } + + @Override + public void update() + { + clock++; + + // Wait for all of our alarms + synchronized( alarms ) + { + double previousTime = time; + int previousDay = day; + double time = apiEnvironment.getComputerEnvironment().getTimeOfDay(); + int day = apiEnvironment.getComputerEnvironment().getDay(); + + if( time > previousTime || day > previousDay ) + { + double now = this.day * 24.0 + this.time; + Iterator> it = alarms.int2ObjectEntrySet().iterator(); + while( it.hasNext() ) + { + Int2ObjectMap.Entry entry = it.next(); + Alarm alarm = entry.getValue(); + double t = alarm.day * 24.0 + alarm.time; + if( now >= t ) + { + apiEnvironment.queueEvent( "alarm", entry.getIntKey() ); + it.remove(); + } + } + } + + this.time = time; + this.day = day; + } + } + + @Override + public void shutdown() + { + synchronized( alarms ) + { + 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 {@code timer} event will be added to the queue with + * the ID returned from this function as the first parameter. + * + * As with @{os.sleep|sleep}, {@code timer} will automatically be rounded up + * to the nearest multiple of 0.05 seconds, as it waits for a fixed amount + * of world ticks. + * + * @param timer The number of seconds until the timer fires. + * @return The ID of the new timer. This can be used to filter the + * {@code timer} event, or {@link #cancelTimer cancel the timer}. + * @throws LuaException If the time is below zero. + * @see #cancelTimer To cancel a timer. + */ + @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 {@code alarm} event will be added to the event queue with the ID + * returned from this function as the first parameter. + * + * @param time The time at which to fire the alarm, in the range [0.0, 24.0). + * @return The ID of the new alarm. This can be used to filter the + * {@code alarm} event, or {@link #cancelAlarm cancel the alarm}. + * @throws LuaException If the time is out of range. + * @see #cancelAlarm To cancel an alarm. + */ + @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( alarms ) + { + int day = time > this.time ? this.day : this.day + 1; + alarms.put( nextAlarmToken, new Alarm( time, day ) ); + return 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( alarms ) + { + 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 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 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 day; + default: + throw new LuaException( "Unsupported operation" ); + } + } + + /** + * Returns the number of milliseconds since an epoch depending on the locale. + * + * * If called with {@code ingame}, returns the number of milliseconds since the + * world was created. This is the default. + * * If called with {@code utc}, returns the number of milliseconds since 1 + * January 1970 in the UTC timezone. + * * If called with {@code local}, returns the number of milliseconds since 1 + * January 1970 in the server's local timezone. + * + * @param args The locale to get the milliseconds for. Defaults to {@code ingame} if not set. + * @return The milliseconds 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( alarms ) + { + return day * 86400000L + (long) (time * 3600000.0); + } + 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/remappedSrc/dan200/computercraft/core/apis/PeripheralAPI.java b/remappedSrc/dan200/computercraft/core/apis/PeripheralAPI.java new file mode 100644 index 000000000..02e382090 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/PeripheralAPI.java @@ -0,0 +1,368 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/apis/RedstoneAPI.java b/remappedSrc/dan200/computercraft/core/apis/RedstoneAPI.java new file mode 100644 index 000000000..186199302 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/RedstoneAPI.java @@ -0,0 +1,215 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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.getBundledInput( 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/remappedSrc/dan200/computercraft/core/apis/TableHelper.java b/remappedSrc/dan200/computercraft/core/apis/TableHelper.java new file mode 100644 index 000000000..d16083534 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/TableHelper.java @@ -0,0 +1,208 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/apis/TermAPI.java b/remappedSrc/dan200/computercraft/core/apis/TermAPI.java new file mode 100644 index 000000000..6b1816096 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/TermAPI.java @@ -0,0 +1,76 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/apis/TermMethods.java b/remappedSrc/dan200/computercraft/core/apis/TermMethods.java new file mode 100644 index 000000000..ce5545124 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/TermMethods.java @@ -0,0 +1,382 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/apis/handles/ArrayByteChannel.java b/remappedSrc/dan200/computercraft/core/apis/handles/ArrayByteChannel.java new file mode 100644 index 000000000..464e23a4e --- /dev/null +++ b/remappedSrc/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-2021. 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/remappedSrc/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java b/remappedSrc/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java new file mode 100644 index 000000000..4477dde92 --- /dev/null +++ b/remappedSrc/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-2021. 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.core.filesystem.TrackingCloseable; + +import java.io.ByteArrayOutputStream; +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, TrackingCloseable closeable ) + { + super( closeable ); + this.reader = reader; + this.seekable = seekable; + } + + public static BinaryReadableHandle of( ReadableByteChannel channel, TrackingCloseable 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, new TrackingCloseable.Impl( 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, TrackingCloseable 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/remappedSrc/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java b/remappedSrc/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java new file mode 100644 index 000000000..796582855 --- /dev/null +++ b/remappedSrc/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-2021. 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 dan200.computercraft.core.filesystem.TrackingCloseable; + +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, TrackingCloseable closeable ) + { + super( closeable ); + this.writer = writer; + this.seekable = seekable; + } + + public static BinaryWritableHandle of( WritableByteChannel channel, TrackingCloseable 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, new TrackingCloseable.Impl( 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, TrackingCloseable 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/remappedSrc/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java b/remappedSrc/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java new file mode 100644 index 000000000..28576f70d --- /dev/null +++ b/remappedSrc/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-2021. 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.core.filesystem.TrackingCloseable; + +import javax.annotation.Nonnull; +import java.io.BufferedReader; +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 TrackingCloseable closable ) + { + super( closable ); + this.reader = reader; + } + + public EncodedReadableHandle( @Nonnull BufferedReader reader ) + { + this( reader, new TrackingCloseable.Impl( 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/remappedSrc/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java b/remappedSrc/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java new file mode 100644 index 000000000..b012b6d0d --- /dev/null +++ b/remappedSrc/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-2021. 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.core.filesystem.TrackingCloseable; +import dan200.computercraft.shared.util.StringUtil; + +import javax.annotation.Nonnull; +import java.io.BufferedWriter; +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 TrackingCloseable 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 value 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 value 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/remappedSrc/dan200/computercraft/core/apis/handles/HandleGeneric.java b/remappedSrc/dan200/computercraft/core/apis/handles/HandleGeneric.java new file mode 100644 index 000000000..fc1954354 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/handles/HandleGeneric.java @@ -0,0 +1,112 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.apis.handles; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.channels.Channel; +import java.nio.channels.SeekableByteChannel; +import java.util.Optional; + +public abstract class HandleGeneric +{ + private TrackingCloseable closeable; + + protected HandleGeneric( @Nonnull TrackingCloseable closeable ) + { + this.closeable = closeable; + } + + protected void checkOpen() throws LuaException + { + TrackingCloseable closeable = this.closeable; + if( closeable == null || !closeable.isOpen() ) throw new LuaException( "attempt to use a closed file" ); + } + + protected final void close() + { + IoUtil.closeQuietly( closeable ); + closeable = 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/remappedSrc/dan200/computercraft/core/apis/http/CheckUrl.java b/remappedSrc/dan200/computercraft/core/apis/http/CheckUrl.java new file mode 100644 index 000000000..d3313b1fc --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/CheckUrl.java @@ -0,0 +1,68 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 URI uri; + + public CheckUrl( ResourceGroup limiter, IAPIEnvironment environment, String address, URI uri ) + { + super( limiter ); + this.environment = environment; + this.address = address; + this.uri = uri; + } + + public void run() + { + if( isClosed() ) return; + future = NetworkUtils.EXECUTOR.submit( this::doRun ); + checkClosed(); + } + + private void doRun() + { + if( isClosed() ) return; + + try + { + boolean ssl = uri.getScheme().equalsIgnoreCase( "https" ); + InetSocketAddress netAddress = NetworkUtils.getAddress( uri, ssl ); + NetworkUtils.getOptions( uri.getHost(), 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/remappedSrc/dan200/computercraft/core/apis/http/HTTPRequestException.java b/remappedSrc/dan200/computercraft/core/apis/http/HTTPRequestException.java new file mode 100644 index 000000000..abf0cdabc --- /dev/null +++ b/remappedSrc/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-2021. 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/remappedSrc/dan200/computercraft/core/apis/http/NetworkUtils.java b/remappedSrc/dan200/computercraft/core/apis/http/NetworkUtils.java new file mode 100644 index 000000000..cbbaf20c5 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/NetworkUtils.java @@ -0,0 +1,196 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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.ConnectTimeoutException; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.TooLongFrameException; +import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.timeout.ReadTimeoutException; + +import javax.annotation.Nonnull; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManagerFactory; +import java.net.InetSocketAddress; +import java.net.URI; +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 a {@link URI}. + * + * Note, this may require a DNS lookup, and so should not be executed on the main CC thread. + * + * @param uri The URI to fetch. + * @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( URI uri, boolean ssl ) throws HTTPRequestException + { + return getAddress( uri.getHost(), uri.getPort(), ssl ); + } + + /** + * 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 ); + 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; + } + + @Nonnull + public static String toFriendlyError( @Nonnull Throwable cause ) + { + if( cause instanceof WebSocketHandshakeException || cause instanceof HTTPRequestException ) + { + return cause.getMessage(); + } + else if( cause instanceof TooLongFrameException ) + { + return "Message is too large"; + } + else if( cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException ) + { + return "Timed out"; + } + else if( cause instanceof SSLHandshakeException || (cause instanceof DecoderException && cause.getCause() instanceof SSLHandshakeException) ) + { + return "Could not create a secure connection"; + } + else + { + return "Could not connect"; + } + } +} diff --git a/remappedSrc/dan200/computercraft/core/apis/http/Resource.java b/remappedSrc/dan200/computercraft/core/apis/http/Resource.java new file mode 100644 index 000000000..142fb1dca --- /dev/null +++ b/remappedSrc/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-2021. 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 final 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/remappedSrc/dan200/computercraft/core/apis/http/ResourceGroup.java b/remappedSrc/dan200/computercraft/core/apis/http/ResourceGroup.java new file mode 100644 index 000000000..8411eebbe --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/ResourceGroup.java @@ -0,0 +1,85 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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> +{ + public static final int DEFAULT_LIMIT = 512; + public static final IntSupplier DEFAULT = () -> DEFAULT_LIMIT; + + 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/remappedSrc/dan200/computercraft/core/apis/http/ResourceQueue.java b/remappedSrc/dan200/computercraft/core/apis/http/ResourceQueue.java new file mode 100644 index 000000000..0b79c8f5e --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/ResourceQueue.java @@ -0,0 +1,62 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 ) ) return true; + if( pending.size() > DEFAULT_LIMIT ) return false; + + 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/remappedSrc/dan200/computercraft/core/apis/http/options/Action.java b/remappedSrc/dan200/computercraft/core/apis/http/options/Action.java new file mode 100644 index 000000000..f994ec3a3 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/options/Action.java @@ -0,0 +1,22 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/apis/http/options/AddressPredicate.java b/remappedSrc/dan200/computercraft/core/apis/http/options/AddressPredicate.java new file mode 100644 index 000000000..9109a3457 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/options/AddressPredicate.java @@ -0,0 +1,148 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.apis.http.options; + +import com.google.common.net.InetAddresses; +import dan200.computercraft.ComputerCraft; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.regex.Pattern; + +/** + * A predicate on an address. Matches against a domain and an ip address. + * + * @see AddressRule#apply(Iterable, String, InetSocketAddress) for the actual handling of this rule. + */ +interface AddressPredicate +{ + default boolean matches( String domain ) + { + return false; + } + + default boolean matches( InetAddress socketAddress ) + { + return false; + } + + final class HostRange implements AddressPredicate + { + private final byte[] min; + private final byte[] max; + + HostRange( byte[] min, byte[] max ) + { + this.min = min; + this.max = max; + } + + @Override + public boolean matches( 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; + } + + public static HostRange parse( String addressStr, String prefixSizeStr ) + { + 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 '{}'.", + addressStr + '/' + prefixSizeStr, 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 '{}'.", + addressStr + '/' + prefixSizeStr, 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 HostRange( minBytes, maxBytes ); + } + } + + final class DomainPattern implements AddressPredicate + { + private final Pattern pattern; + + DomainPattern( Pattern pattern ) + { + this.pattern = pattern; + } + + @Override + public boolean matches( String domain ) + { + return pattern.matcher( domain ).matches(); + } + + @Override + public boolean matches( InetAddress socketAddress ) + { + return pattern.matcher( socketAddress.getHostAddress() ).matches(); + } + } + + + final class PrivatePattern implements AddressPredicate + { + static final PrivatePattern INSTANCE = new PrivatePattern(); + + @Override + public boolean matches( InetAddress socketAddress ) + { + return socketAddress.isAnyLocalAddress() + || socketAddress.isLoopbackAddress() + || socketAddress.isLinkLocalAddress() + || socketAddress.isSiteLocalAddress(); + } + } + +} diff --git a/remappedSrc/dan200/computercraft/core/apis/http/options/AddressRule.java b/remappedSrc/dan200/computercraft/core/apis/http/options/AddressRule.java new file mode 100644 index 000000000..955e89d4d --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/options/AddressRule.java @@ -0,0 +1,114 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.apis.http.options; + +import com.google.common.net.InetAddresses; +import dan200.computercraft.core.apis.http.options.AddressPredicate.DomainPattern; +import dan200.computercraft.core.apis.http.options.AddressPredicate.HostRange; +import dan200.computercraft.core.apis.http.options.AddressPredicate.PrivatePattern; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +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 final AddressPredicate predicate; + private final Integer port; + private final PartialOptions partial; + + private AddressRule( @Nonnull AddressPredicate predicate, @Nullable Integer port, @Nonnull PartialOptions partial ) + { + this.predicate = predicate; + this.partial = partial; + this.port = port; + } + + @Nullable + public static AddressRule parse( String filter, @Nullable Integer port, @Nonnull PartialOptions partial ) + { + int cidr = filter.indexOf( '/' ); + if( cidr >= 0 ) + { + String addressStr = filter.substring( 0, cidr ); + String prefixSizeStr = filter.substring( cidr + 1 ); + HostRange range = HostRange.parse( addressStr, prefixSizeStr ); + return range == null ? null : new AddressRule( range, port, partial ); + } + else if( filter.equalsIgnoreCase( "$private" ) ) + { + return new AddressRule( PrivatePattern.INSTANCE, port, partial ); + } + else + { + Pattern pattern = Pattern.compile( "^\\Q" + filter.replaceAll( "\\*", "\\\\E.*\\\\Q" ) + "\\E$", Pattern.CASE_INSENSITIVE ); + return new AddressRule( new DomainPattern( pattern ), port, partial ); + } + } + + /** + * Determine whether the given address matches a series of patterns. + * + * @param domain The domain to match + * @param port The port of the address. + * @param address The address to check. + * @param ipv4Address An ipv4 version of the address, if the original was an ipv6 address. + * @return Whether it matches any of these patterns. + */ + private boolean matches( String domain, int port, InetAddress address, Inet4Address ipv4Address ) + { + if( this.port != null && this.port != port ) return false; + return predicate.matches( domain ) + || predicate.matches( address ) + || (ipv4Address != null && predicate.matches( ipv4Address )); + } + + public static Options apply( Iterable rules, String domain, InetSocketAddress socketAddress ) + { + PartialOptions options = null; + boolean hasMany = false; + + int port = socketAddress.getPort(); + InetAddress address = socketAddress.getAddress(); + Inet4Address ipv4Address = address instanceof Inet6Address && InetAddresses.is6to4Address( (Inet6Address) address ) + ? InetAddresses.get6to4IPv4Address( (Inet6Address) address ) : null; + + for( AddressRule rule : rules ) + { + if( !rule.matches( domain, port, address, ipv4Address ) ) 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/remappedSrc/dan200/computercraft/core/apis/http/options/AddressRuleConfig.java b/remappedSrc/dan200/computercraft/core/apis/http/options/AddressRuleConfig.java new file mode 100644 index 000000000..862ff39b6 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/options/AddressRuleConfig.java @@ -0,0 +1,134 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 ); + Integer port = get( builder, "port", Number.class ).map( Number::intValue ).orElse( null ); + return hostObj != null && checkEnum( builder, "action", Action.class ) + && check( builder, "port", Number.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, port, 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 port = get( builder, "port", Number.class ).map( Number::intValue ).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, port, 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/remappedSrc/dan200/computercraft/core/apis/http/options/Options.java b/remappedSrc/dan200/computercraft/core/apis/http/options/Options.java new file mode 100644 index 000000000..d6074a9a4 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/options/Options.java @@ -0,0 +1,30 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/apis/http/options/PartialOptions.java b/remappedSrc/dan200/computercraft/core/apis/http/options/PartialOptions.java new file mode 100644 index 000000000..ef0801966 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/options/PartialOptions.java @@ -0,0 +1,58 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/apis/http/request/HttpRequest.java b/remappedSrc/dan200/computercraft/core/apis/http/request/HttpRequest.java new file mode 100644 index 000000000..2049575ff --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/request/HttpRequest.java @@ -0,0 +1,257 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.*; +import io.netty.handler.ssl.SslContext; +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, 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( NetworkUtils.toFriendlyError( c.cause() ) ); + } ); + + // Do an additional check for cancellation + checkClosed(); + } + catch( HTTPRequestException e ) + { + failure( e.getMessage() ); + } + catch( Exception e ) + { + failure( NetworkUtils.toFriendlyError( e ) ); + 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( 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/remappedSrc/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java b/remappedSrc/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java new file mode 100644 index 000000000..4ef42ff44 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java @@ -0,0 +1,259 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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.net.URI; +import java.net.URISyntaxException; +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" ); + } + 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( NetworkUtils.toFriendlyError( 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( location ) ); + } + catch( IllegalArgumentException | URISyntaxException e ) + { + return null; + } + } + + @Override + public void close() + { + closed = true; + if( responseBody != null ) + { + responseBody.release(); + responseBody = null; + } + } +} diff --git a/remappedSrc/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java b/remappedSrc/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java new file mode 100644 index 000000000..c5d72134b --- /dev/null +++ b/remappedSrc/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-2021. 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.tweaked.cc](https://example.tweaked.cc), and print the + * returned headers. + *
{@code
+     * local request = http.get("https://example.tweaked.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/remappedSrc/dan200/computercraft/core/apis/http/websocket/Websocket.java b/remappedSrc/dan200/computercraft/core/apis/http/websocket/Websocket.java new file mode 100644 index 000000000..fa1399bb6 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/websocket/Websocket.java @@ -0,0 +1,235 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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, 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( NetworkUtils.toFriendlyError( c.cause() ) ); + } ); + + // Do an additional check for cancellation + checkClosed(); + } + catch( HTTPRequestException e ) + { + failure( e.getMessage() ); + } + catch( Exception e ) + { + failure( NetworkUtils.toFriendlyError( e ) ); + 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/remappedSrc/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java b/remappedSrc/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java new file mode 100644 index 000000000..0c74c0c9f --- /dev/null +++ b/remappedSrc/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-2021. 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/remappedSrc/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java b/remappedSrc/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java new file mode 100644 index 000000000..4916f5fbd --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java @@ -0,0 +1,106 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.apis.http.websocket; + +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.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.websocketx.*; +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 = NetworkUtils.toFriendlyError( cause ); + if( handshaker.isHandshakeComplete() ) + { + websocket.close( -1, message ); + } + else + { + websocket.failure( message ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/core/asm/DeclaringClassLoader.java b/remappedSrc/dan200/computercraft/core/asm/DeclaringClassLoader.java new file mode 100644 index 000000000..34e9be132 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/asm/DeclaringClassLoader.java @@ -0,0 +1,23 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/asm/Generator.java b/remappedSrc/dan200/computercraft/core/asm/Generator.java new file mode 100644 index 000000000..e68732f95 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/asm/Generator.java @@ -0,0 +1,379 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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( catching( this::build, Collections.emptyList() ) ) ); + + private final LoadingCache> methodCache = CacheBuilder + .newBuilder() + .build( CacheLoader.from( catching( this::build, Optional.empty() ) ) ); + + Generator( Class base, List> context, Function wrap ) + { + this.base = base; + this.context = context; + 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( GenericMethod method : 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; + } + + @SuppressWarnings( "Guava" ) + private static com.google.common.base.Function catching( Function function, U def ) + { + return x -> { + try + { + return function.apply( x ); + } + catch( Exception | LinkageError e ) + { + // LinkageError due to possible codegen bugs and NoClassDefFoundError. The latter occurs when fetching + // methods on a class which references non-existent (i.e. client-only) types. + ComputerCraft.log.error( "Error generating @LuaFunctions", e ); + return def; + } + }; + } +} diff --git a/remappedSrc/dan200/computercraft/core/asm/GenericMethod.java b/remappedSrc/dan200/computercraft/core/asm/GenericMethod.java new file mode 100644 index 000000000..e0c916c2e --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/asm/GenericMethod.java @@ -0,0 +1,90 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.asm; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.GenericSource; +import dan200.computercraft.api.lua.LuaFunction; + +import javax.annotation.Nonnull; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A generic method is a method belonging to a {@link GenericSource} with a known target. + */ +public class GenericMethod +{ + final Method method; + final LuaFunction annotation; + final Class target; + + private static final List sources = new ArrayList<>(); + private static List cache; + + GenericMethod( Method method, LuaFunction annotation, Class target ) + { + this.method = method; + this.annotation = annotation; + this.target = target; + } + + /** + * Find all public static methods annotated with {@link LuaFunction} which belong to a {@link GenericSource}. + * + * @return All available generic methods. + */ + static List all() + { + if( cache != null ) return cache; + return cache = sources.stream() + .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() ); + } + + + public static synchronized void register( @Nonnull GenericSource source ) + { + Objects.requireNonNull( source, "Source cannot be null" ); + + if( cache != null ) + { + ComputerCraft.log.warn( "Registering a generic source {} after cache has been built. This source will be ignored.", cache ); + } + + sources.add( source ); + } +} diff --git a/remappedSrc/dan200/computercraft/core/asm/IntCache.java b/remappedSrc/dan200/computercraft/core/asm/IntCache.java new file mode 100644 index 000000000..ec634b3cb --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/asm/IntCache.java @@ -0,0 +1,40 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/asm/LuaMethod.java b/remappedSrc/dan200/computercraft/core/asm/LuaMethod.java new file mode 100644 index 000000000..b0562835c --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/asm/LuaMethod.java @@ -0,0 +1,27 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/asm/NamedMethod.java b/remappedSrc/dan200/computercraft/core/asm/NamedMethod.java new file mode 100644 index 000000000..ea72bb7a4 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/asm/NamedMethod.java @@ -0,0 +1,39 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/asm/ObjectSource.java b/remappedSrc/dan200/computercraft/core/asm/ObjectSource.java new file mode 100644 index 000000000..06ecbaf03 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/asm/ObjectSource.java @@ -0,0 +1,32 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/asm/PeripheralMethod.java b/remappedSrc/dan200/computercraft/core/asm/PeripheralMethod.java new file mode 100644 index 000000000..38618442f --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/asm/PeripheralMethod.java @@ -0,0 +1,30 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/asm/Reflect.java b/remappedSrc/dan200/computercraft/core/asm/Reflect.java new file mode 100644 index 000000000..5504f3bc8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/asm/Reflect.java @@ -0,0 +1,94 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 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( 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/remappedSrc/dan200/computercraft/core/asm/TaskCallback.java b/remappedSrc/dan200/computercraft/core/asm/TaskCallback.java new file mode 100644 index 000000000..1298391e4 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/asm/TaskCallback.java @@ -0,0 +1,67 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/computer/ApiWrapper.java b/remappedSrc/dan200/computercraft/core/computer/ApiWrapper.java new file mode 100644 index 000000000..6265b127d --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/ApiWrapper.java @@ -0,0 +1,53 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/computer/Computer.java b/remappedSrc/dan200/computercraft/core/computer/Computer.java new file mode 100644 index 000000000..005625230 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/Computer.java @@ -0,0 +1,219 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 id; + private String label = null; + + // Read-only fields about the computer + private final IComputerEnvironment environment; + private final Terminal terminal; + private final ComputerExecutor executor; + private final MainThreadExecutor serverExecutor; + + // Additional state about the computer and its environment. + private boolean blinking = false; + private final Environment internalEnvironment = new Environment( this ); + private final AtomicBoolean externalOutputChanged = new AtomicBoolean(); + + private boolean startRequested; + private int ticksSinceStart = -1; + + public Computer( IComputerEnvironment environment, Terminal terminal, int id ) + { + this.id = id; + this.environment = environment; + this.terminal = terminal; + + executor = new ComputerExecutor( this ); + serverExecutor = new MainThreadExecutor( this ); + } + + IComputerEnvironment getComputerEnvironment() + { + return environment; + } + + FileSystem getFileSystem() + { + return executor.getFileSystem(); + } + + Terminal getTerminal() + { + return 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 id; + } + + public int assignID() + { + if( id < 0 ) + { + id = environment.assignNewID(); + } + return id; + } + + public void setID( int id ) + { + this.id = id; + } + + public String getLabel() + { + return label; + } + + public void setLabel( String label ) + { + if( !Objects.equal( label, this.label ) ) + { + this.label = label; + externalOutputChanged.set( true ); + } + } + + public void tick() + { + // We keep track of the number of ticks since the last start, only + if( ticksSinceStart >= 0 && ticksSinceStart <= START_DELAY ) ticksSinceStart++; + + if( startRequested && (ticksSinceStart < 0 || ticksSinceStart > START_DELAY) ) + { + startRequested = false; + if( !executor.isOn() ) + { + 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 = terminal.getCursorBlink() && + terminal.getCursorX() >= 0 && terminal.getCursorX() < terminal.getWidth() && + terminal.getCursorY() >= 0 && terminal.getCursorY() < terminal.getHeight(); + if( blinking != this.blinking ) + { + this.blinking = blinking; + externalOutputChanged.set( true ); + } + } + + void markChanged() + { + externalOutputChanged.set( true ); + } + + public boolean pollAndResetChanged() + { + return externalOutputChanged.getAndSet( false ); + } + + public boolean isBlinking() + { + return isOn() && blinking; + } + + public void addApi( ILuaAPI api ) + { + executor.addApi( api ); + } +} diff --git a/remappedSrc/dan200/computercraft/core/computer/ComputerExecutor.java b/remappedSrc/dan200/computercraft/core/computer/ComputerExecutor.java new file mode 100644 index 000000000..7ee1b742a --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/ComputerExecutor.java @@ -0,0 +1,681 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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() + { + immediateFail( StateCommand.ABORT ); + } + + /** + * Abort this whole computer due to an internal error. This will immediately destroy the Lua machine, + * and then schedule a shutdown. + */ + void fastFail() + { + immediateFail( StateCommand.ERROR ); + } + + private void immediateFail( StateCommand command ) + { + ILuaMachine machine = this.machine; + if( machine != null ) machine.close(); + + synchronized( queueLock ) + { + if( closed ) return; + this.command = command; + 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; + + case ERROR: + if( !isOn ) return; + displayFailure( "Error running computer", "An internal error occurred, see logs." ); + 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, + ERROR, + } + + 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/remappedSrc/dan200/computercraft/core/computer/ComputerSide.java b/remappedSrc/dan200/computercraft/core/computer/ComputerSide.java new file mode 100644 index 000000000..dd925b324 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/ComputerSide.java @@ -0,0 +1,60 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.computer; + +import net.minecraft.util.math.Direction; + +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/remappedSrc/dan200/computercraft/core/computer/ComputerSystem.java b/remappedSrc/dan200/computercraft/core/computer/ComputerSystem.java new file mode 100644 index 000000000..9747f27e4 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/ComputerSystem.java @@ -0,0 +1,75 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/computer/ComputerThread.java b/remappedSrc/dan200/computercraft/core/computer/ComputerThread.java new file mode 100644 index 000000000..d190dfefb --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/ComputerThread.java @@ -0,0 +1,542 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 ); + // Tear down the computer immediately. There's no guarantee it's well behaved from now on. + executor.fastFail(); + } + 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/remappedSrc/dan200/computercraft/core/computer/Environment.java b/remappedSrc/dan200/computercraft/core/computer/Environment.java new file mode 100644 index 000000000..3931629f1 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/Environment.java @@ -0,0 +1,377 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/computer/IComputerEnvironment.java b/remappedSrc/dan200/computercraft/core/computer/IComputerEnvironment.java new file mode 100644 index 000000000..3d928ffc2 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/IComputerEnvironment.java @@ -0,0 +1,41 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/computer/MainThread.java b/remappedSrc/dan200/computercraft/core/computer/MainThread.java new file mode 100644 index 000000000..8f53541e2 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/MainThread.java @@ -0,0 +1,194 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/computer/MainThreadExecutor.java b/remappedSrc/dan200/computercraft/core/computer/MainThreadExecutor.java new file mode 100644 index 000000000..07737c4c2 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/MainThreadExecutor.java @@ -0,0 +1,249 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 net.minecraft.block.entity.BlockEntity; + +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 BlockEntity}) 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/remappedSrc/dan200/computercraft/core/computer/TimeoutState.java b/remappedSrc/dan200/computercraft/core/computer/TimeoutState.java new file mode 100644 index 000000000..5f2d99265 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/computer/TimeoutState.java @@ -0,0 +1,171 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/filesystem/ChannelWrapper.java b/remappedSrc/dan200/computercraft/core/filesystem/ChannelWrapper.java new file mode 100644 index 000000000..f5de836bc --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/ChannelWrapper.java @@ -0,0 +1,49 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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(); + } + } + + T get() + { + return wrapper; + } +} diff --git a/remappedSrc/dan200/computercraft/core/filesystem/ComboMount.java b/remappedSrc/dan200/computercraft/core/filesystem/ComboMount.java new file mode 100644 index 000000000..46e63e9ac --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/ComboMount.java @@ -0,0 +1,145 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 final IMount[] parts; + + public ComboMount( IMount[] parts ) + { + this.parts = parts; + } + + // IMount implementation + + @Override + public boolean exists( @Nonnull String path ) throws IOException + { + for( int i = parts.length - 1; i >= 0; --i ) + { + IMount part = parts[i]; + if( part.exists( path ) ) + { + return true; + } + } + return false; + } + + @Override + public boolean isDirectory( @Nonnull String path ) throws IOException + { + for( int i = parts.length - 1; i >= 0; --i ) + { + IMount part = 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 = parts.length - 1; i >= 0; --i ) + { + IMount part = 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 = parts.length - 1; i >= 0; --i ) + { + IMount part = 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 = parts.length - 1; i >= 0; --i ) + { + IMount part = 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 = parts.length - 1; i >= 0; --i ) + { + IMount part = parts[i]; + if( part.exists( path ) && !part.isDirectory( path ) ) + { + return part.getAttributes( path ); + } + } + throw new FileOperationException( path, "No such file" ); + } +} diff --git a/remappedSrc/dan200/computercraft/core/filesystem/EmptyMount.java b/remappedSrc/dan200/computercraft/core/filesystem/EmptyMount.java new file mode 100644 index 000000000..ab89d3fb8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/EmptyMount.java @@ -0,0 +1,47 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/filesystem/FileMount.java b/remappedSrc/dan200/computercraft/core/filesystem/FileMount.java new file mode 100644 index 000000000..bcd19adbe --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/FileMount.java @@ -0,0 +1,419 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 inner; + long ignoredBytesLeft; + + WritableCountingChannel( WritableByteChannel inner, long bytesToIgnore ) + { + this.inner = inner; + ignoredBytesLeft = bytesToIgnore; + } + + @Override + public int write( @Nonnull ByteBuffer b ) throws IOException + { + count( b.remaining() ); + return inner.write( b ); + } + + void count( long n ) throws IOException + { + ignoredBytesLeft -= n; + if( ignoredBytesLeft < 0 ) + { + long newBytes = -ignoredBytesLeft; + ignoredBytesLeft = 0; + + long bytesLeft = capacity - usedSpace; + if( newBytes > bytesLeft ) throw new IOException( "Out of space" ); + usedSpace += newBytes; + } + } + + @Override + public boolean isOpen() + { + return inner.isOpen(); + } + + @Override + public void close() throws IOException + { + inner.close(); + } + } + + private class SeekableCountingChannel extends WritableCountingChannel implements SeekableByteChannel + { + private final SeekableByteChannel inner; + + SeekableCountingChannel( SeekableByteChannel inner, long bytesToIgnore ) + { + super( inner, bytesToIgnore ); + this.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 - inner.position(); + if( delta < 0 ) + { + ignoredBytesLeft -= delta; + } + else + { + count( delta ); + } + + return 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( !inner.isOpen() ) throw new ClosedChannelException(); + throw new NonReadableChannelException(); + } + + @Override + public long position() throws IOException + { + return inner.position(); + } + + @Override + public long size() throws IOException + { + return inner.size(); + } + } + + private final File rootPath; + private final long capacity; + private long usedSpace; + + public FileMount( File rootPath, long capacity ) + { + this.rootPath = rootPath; + this.capacity = capacity + MINIMUM_FILE_SIZE; + usedSpace = created() ? measureUsedSpace( this.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() ) + { + 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 ) + { + 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() ) + { + usedSpace -= Math.max( file.length(), MINIMUM_FILE_SIZE ); + } + else if( getRemainingSpace() < MINIMUM_FILE_SIZE ) + { + throw new FileOperationException( path, "Out of space" ); + } + 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( capacity - usedSpace, 0 ); + } + + @Nonnull + @Override + public OptionalLong getCapacity() + { + return OptionalLong.of( capacity - MINIMUM_FILE_SIZE ); + } + + private File getRealPath( String path ) + { + return new File( rootPath, path ); + } + + private boolean created() + { + return rootPath.exists(); + } + + private void create() throws IOException + { + if( !rootPath.exists() ) + { + boolean success = 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/remappedSrc/dan200/computercraft/core/filesystem/FileSystem.java b/remappedSrc/dan200/computercraft/core/filesystem/FileSystem.java new file mode 100644 index 000000000..cfbc9d92e --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/FileSystem.java @@ -0,0 +1,620 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 wrapper = new FileSystemWrapperMount( this ); + private final Map mounts = new HashMap<>(); + + private final HashMap>, ChannelWrapper> openFiles = new HashMap<>(); + private final ReferenceQueue> 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( openFiles ) + { + for( Closeable file : openFiles.values() ) IoUtil.closeQuietly( file ); + openFiles.clear(); + while( 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 ) + { + MountWrapper mount = mounts.remove( sanitizePath( path ) ); + if( mount == null ) return; + + cleanup(); + + // Close any files which belong to this mount - don't want people writing to a disk after it's been ejected! + // There's no point storing a Mount -> Wrapper[] map, as openFiles is small and unmount isn't called very + // often. + synchronized( openFiles ) + { + for( Iterator>> iterator = openFiles.keySet().iterator(); iterator.hasNext(); ) + { + WeakReference> reference = iterator.next(); + FileSystemWrapper wrapper = reference.get(); + if( wrapper == null ) continue; + + if( wrapper.mount == mount ) + { + wrapper.closeExternally(); + iterator.remove(); + } + } + } + } + + public 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( openFiles ) + { + Reference ref; + while( (ref = openFileQueue.poll()) != null ) + { + IoUtil.closeQuietly( openFiles.remove( ref ) ); + } + } + } + + private synchronized FileSystemWrapper openFile( @Nonnull MountWrapper mount, @Nonnull Channel channel, @Nonnull T file ) throws FileSystemException + { + synchronized( openFiles ) + { + if( ComputerCraft.maximumFilesOpen > 0 && + 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, mount, channelWrapper, openFileQueue ); + openFiles.put( fsWrapper.self, channelWrapper ); + return fsWrapper; + } + } + + void removeFile( FileSystemWrapper handle ) + { + synchronized( openFiles ) + { + 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 ); + return channel != null ? openFile( mount, channel, open.apply( channel ) ) : 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 ); + return channel != null ? openFile( mount, channel, open.apply( channel ) ) : 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 wrapper; + } + + private static String sanitizePath( String path ) + { + return sanitizePath( path, false ); + } + + private static final Pattern threeDotsPattern = Pattern.compile( "^\\.{3,}$" ); + + public 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/remappedSrc/dan200/computercraft/core/filesystem/FileSystemException.java b/remappedSrc/dan200/computercraft/core/filesystem/FileSystemException.java new file mode 100644 index 000000000..482d0d080 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/FileSystemException.java @@ -0,0 +1,16 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.filesystem; + +public class FileSystemException extends Exception +{ + private static final long serialVersionUID = -2500631644868104029L; + + FileSystemException( String s ) + { + super( s ); + } +} diff --git a/remappedSrc/dan200/computercraft/core/filesystem/FileSystemWrapper.java b/remappedSrc/dan200/computercraft/core/filesystem/FileSystemWrapper.java new file mode 100644 index 000000000..a65e04326 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/FileSystemWrapper.java @@ -0,0 +1,70 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.filesystem; + +import dan200.computercraft.shared.util.IoUtil; + +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 TrackingCloseable +{ + private final FileSystem fileSystem; + final MountWrapper mount; + private final ChannelWrapper closeable; + final WeakReference> self; + private boolean isOpen = true; + + FileSystemWrapper( FileSystem fileSystem, MountWrapper mount, ChannelWrapper closeable, ReferenceQueue> queue ) + { + this.fileSystem = fileSystem; + this.mount = mount; + this.closeable = closeable; + self = new WeakReference<>( this, queue ); + } + + @Override + public void close() throws IOException + { + isOpen = false; + fileSystem.removeFile( this ); + closeable.close(); + } + + void closeExternally() + { + isOpen = false; + IoUtil.closeQuietly( closeable ); + } + + @Override + public boolean isOpen() + { + return isOpen; + } + + @Nonnull + public T get() + { + return closeable.get(); + } +} diff --git a/remappedSrc/dan200/computercraft/core/filesystem/FileSystemWrapperMount.java b/remappedSrc/dan200/computercraft/core/filesystem/FileSystemWrapperMount.java new file mode 100644 index 000000000..e0082dcbe --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/FileSystemWrapperMount.java @@ -0,0 +1,192 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 filesystem; + + public FileSystemWrapperMount( FileSystem filesystem ) + { + this.filesystem = filesystem; + } + + @Override + public void makeDirectory( @Nonnull String path ) throws IOException + { + try + { + filesystem.makeDir( path ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public void delete( @Nonnull String path ) throws IOException + { + try + { + 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 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 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 filesystem.openForWrite( path, true, Function.identity() ).get(); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public long getRemainingSpace() throws IOException + { + try + { + return filesystem.getFreeSpace( "/" ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public boolean exists( @Nonnull String path ) throws IOException + { + try + { + return filesystem.exists( path ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public boolean isDirectory( @Nonnull String path ) throws IOException + { + try + { + return filesystem.isDir( 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, filesystem.list( path ) ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public long getSize( @Nonnull String path ) throws IOException + { + try + { + return filesystem.getSize( path ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public String combine( String path, String child ) + { + return filesystem.combine( path, child ); + } + + @Override + public void copy( String from, String to ) throws IOException + { + try + { + filesystem.copy( from, to ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } + + @Override + public void move( String from, String to ) throws IOException + { + try + { + filesystem.move( from, to ); + } + catch( FileSystemException e ) + { + throw new IOException( e.getMessage() ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/core/filesystem/JarMount.java b/remappedSrc/dan200/computercraft/core/filesystem/JarMount.java new file mode 100644 index 000000000..f902261a0 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/JarMount.java @@ -0,0 +1,345 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/filesystem/MountWrapper.java b/remappedSrc/dan200/computercraft/core/filesystem/MountWrapper.java new file mode 100644 index 000000000..96b11f24f --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/MountWrapper.java @@ -0,0 +1,323 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/filesystem/ResourceMount.java b/remappedSrc/dan200/computercraft/core/filesystem/ResourceMount.java new file mode 100644 index 000000000..b31350f6b --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/ResourceMount.java @@ -0,0 +1,332 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 dan200.computercraft.shared.util.IoUtil; +import net.minecraft.resource.ReloadableResourceManager; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceReloader; +import net.minecraft.util.Identifier; +import net.minecraft.util.InvalidIdentifierException; +import net.minecraft.util.profiler.Profiler; + +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.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +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; + String existingNamespace = null; + + FileEntry newRoot = new FileEntry( new Identifier( namespace, subPath ) ); + for( Identifier file : manager.findResources( subPath, s -> true ) ) + { + existingNamespace = file.getNamespace(); + + if( !file.getNamespace().equals( namespace ) ) continue; + + String localPath = FileSystem.toLocal( file.getPath(), subPath ); + create( newRoot, localPath ); + hasAny = true; + } + + root = hasAny ? newRoot : null; + + if( !hasAny ) + { + ComputerCraft.log.warn( "Cannot find any files under /data/{}/{} for resource mount.", namespace, subPath ); + if( existingNamespace != null ) + { + ComputerCraft.log.warn( "There are files under /data/{}/{} though. Did you get the wrong namespace?", existingNamespace, subPath ); + } + } + } + + 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 ); + + try + { + contents = ByteStreams.toByteArray( stream ); + } + finally + { + IoUtil.closeQuietly( 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 ResourceReloader} 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 ResourceReloader + { + 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 CompletableFuture reload( Synchronizer synchronizer, ResourceManager manager, Profiler prepareProfiler, Profiler applyProfiler, Executor prepareExecutor, Executor applyExecutor ) + { + return CompletableFuture.runAsync( () -> { + prepareProfiler.push( "Mount reloading" ); + try + { + for( ResourceMount mount : mounts ) mount.load(); + } + finally + { + prepareProfiler.pop(); + } + }, prepareExecutor ); + } + + synchronized void add( ReloadableResourceManager manager, ResourceMount mount ) + { + if( managers.add( manager ) ) manager.registerReloader( this ); + mounts.add( mount ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/core/filesystem/SubMount.java b/remappedSrc/dan200/computercraft/core/filesystem/SubMount.java new file mode 100644 index 000000000..b596896c8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/SubMount.java @@ -0,0 +1,69 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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 final IMount parent; + private final 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/remappedSrc/dan200/computercraft/core/filesystem/TrackingCloseable.java b/remappedSrc/dan200/computercraft/core/filesystem/TrackingCloseable.java new file mode 100644 index 000000000..19ffc978f --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/filesystem/TrackingCloseable.java @@ -0,0 +1,44 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.filesystem; + +import java.io.Closeable; +import java.io.IOException; + +/** + * A {@link Closeable} which knows when it has been closed. + * + * This is a quick (though racey) way of providing more friendly (and more similar to Lua) + * error messages to the user. + */ +public interface TrackingCloseable extends Closeable +{ + boolean isOpen(); + + class Impl implements TrackingCloseable + { + private final Closeable object; + private boolean isOpen = true; + + public Impl( Closeable object ) + { + this.object = object; + } + + @Override + public boolean isOpen() + { + return isOpen; + } + + @Override + public void close() throws IOException + { + isOpen = false; + object.close(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/core/lua/BasicFunction.java b/remappedSrc/dan200/computercraft/core/lua/BasicFunction.java new file mode 100644 index 000000000..f515cd455 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/lua/BasicFunction.java @@ -0,0 +1,74 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/lua/CobaltLuaMachine.java b/remappedSrc/dan200/computercraft/core/lua/CobaltLuaMachine.java new file mode 100644 index 000000000..82093db4e --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/lua/CobaltLuaMachine.java @@ -0,0 +1,521 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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.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 computer; + private final TimeoutState timeout; + private final TimeoutDebugHandler debug; + private final ILuaContext context; + + private LuaState state; + private LuaTable globals; + + private LuaThread mainRoutine = null; + private String eventFilter = null; + + public CobaltLuaMachine( Computer computer, TimeoutState timeout ) + { + this.computer = computer; + this.timeout = timeout; + context = new LuaContext( computer ); + debug = new TimeoutDebugHandler(); + + // Create an environment to run in + LuaState state = this.state = LuaState.builder() + .resourceManipulator( new VoidResourceManipulator() ) + .debug( debug ) + .coroutineExecutor( command -> { + Tracking.addValue( this.computer, TrackingField.COROUTINES_CREATED, 1 ); + COROUTINES.execute( () -> { + try + { + command.run(); + } + finally + { + Tracking.addValue( this.computer, TrackingField.COROUTINES_DISPOSED, 1 ); + } + } ); + } ) + .build(); + + globals = new LuaTable(); + state.setupThread( globals ); + + // Add basic libraries + globals.load( state, new BaseLib() ); + globals.load( state, new TableLib() ); + globals.load( state, new StringLib() ); + globals.load( state, new MathLib() ); + globals.load( state, new CoroutineLib() ); + globals.load( state, new Bit32Lib() ); + globals.load( state, new Utf8Lib() ); + if( ComputerCraft.debugEnable ) globals.load( state, new DebugLib() ); + + // Remove globals we don't want to expose + globals.rawset( "collectgarbage", Constants.NIL ); + globals.rawset( "dofile", Constants.NIL ); + globals.rawset( "loadfile", Constants.NIL ); + globals.rawset( "print", Constants.NIL ); + + // Add version globals + globals.rawset( "_VERSION", valueOf( "Lua 5.1" ) ); + globals.rawset( "_HOST", valueOf( computer.getAPIEnvironment().getComputerEnvironment().getHostString() ) ); + globals.rawset( "_CC_DEFAULT_SETTINGS", valueOf( ComputerCraft.defaultComputerSettings ) ); + if( ComputerCraft.disableLua51Features ) + { + 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 ) globals.rawset( name, table ); + } + + @Override + public MachineResult loadBios( @Nonnull InputStream bios ) + { + // Begin executing a file (ie, the bios) + if( mainRoutine != null ) return MachineResult.OK; + + try + { + LuaFunction value = LoadState.load( state, bios, "@bios.lua", globals ); + mainRoutine = new LuaThread( state, value, 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( mainRoutine == null ) return MachineResult.OK; + + if( eventFilter != null && eventName != null && !eventName.equals( 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 = state.getCurrentThread(); + if( thread == null || thread == state.getMainThread() ) thread = mainRoutine; + + Varargs results = LuaThread.run( thread, resumeArgs ); + if( timeout.isHardAborted() ) throw HardAbortError.INSTANCE; + if( results == null ) return MachineResult.PAUSE; + + LuaValue filter = results.first(); + eventFilter = filter.isString() ? filter.toString() : null; + + if( 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 = this.state; + if( state == null ) return; + + state.abandon(); + mainRoutine = null; + this.state = null; + 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() || 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 = CobaltLuaMachine.this.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 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/remappedSrc/dan200/computercraft/core/lua/ILuaMachine.java b/remappedSrc/dan200/computercraft/core/lua/ILuaMachine.java new file mode 100644 index 000000000..3d62c2c43 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/lua/ILuaMachine.java @@ -0,0 +1,66 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/lua/LuaContext.java b/remappedSrc/dan200/computercraft/core/lua/LuaContext.java new file mode 100644 index 000000000..626d15fad --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/lua/LuaContext.java @@ -0,0 +1,69 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.lua; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.ILuaTask; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.core.computer.Computer; +import dan200.computercraft.core.computer.MainThread; + +import javax.annotation.Nonnull; + +class LuaContext implements ILuaContext +{ + private final Computer computer; + + LuaContext( Computer computer ) + { + this.computer = computer; + } + + @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 ); + computer.queueEvent( "task_complete", eventArguments ); + } + else + { + computer.queueEvent( "task_complete", new Object[] { taskID, true } ); + } + } + catch( LuaException e ) + { + computer.queueEvent( "task_complete", new Object[] { taskID, false, e.getMessage() } ); + } + catch( Exception t ) + { + if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error running task", t ); + computer.queueEvent( "task_complete", new Object[] { + taskID, false, "Java Exception Thrown: " + t, + } ); + } + }; + if( computer.queueMainThread( iTask ) ) + { + return taskID; + } + else + { + throw new LuaException( "Task limit exceeded" ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/core/lua/MachineResult.java b/remappedSrc/dan200/computercraft/core/lua/MachineResult.java new file mode 100644 index 000000000..167ec9298 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/lua/MachineResult.java @@ -0,0 +1,80 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/lua/ResultInterpreterFunction.java b/remappedSrc/dan200/computercraft/core/lua/ResultInterpreterFunction.java new file mode 100644 index 000000000..e1af2e170 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/lua/ResultInterpreterFunction.java @@ -0,0 +1,121 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/lua/VarargArguments.java b/remappedSrc/dan200/computercraft/core/lua/VarargArguments.java new file mode 100644 index 000000000..0079e41c7 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/lua/VarargArguments.java @@ -0,0 +1,101 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/terminal/Terminal.java b/remappedSrc/dan200/computercraft/core/terminal/Terminal.java new file mode 100644 index 000000000..8215e4d19 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/terminal/Terminal.java @@ -0,0 +1,424 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.terminal; + +import dan200.computercraft.shared.util.Colour; +import dan200.computercraft.shared.util.Palette; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.PacketByteBuf; + +import javax.annotation.Nonnull; + +public class Terminal +{ + private static final String base16 = "0123456789abcdef"; + + private int cursorX = 0; + private int cursorY = 0; + private boolean cursorBlink = false; + private int cursorColour = 0; + private int cursorBackgroundColour = 15; + + private int width; + private int height; + + private TextBuffer[] text; + private TextBuffer[] textColour; + private TextBuffer[] backgroundColour; + + private final Palette 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 ) + { + this.width = width; + this.height = height; + onChanged = changedCallback; + + text = new TextBuffer[height]; + textColour = new TextBuffer[height]; + backgroundColour = new TextBuffer[height]; + for( int i = 0; i < this.height; i++ ) + { + text[i] = new TextBuffer( ' ', this.width ); + textColour[i] = new TextBuffer( base16.charAt( cursorColour ), this.width ); + backgroundColour[i] = new TextBuffer( base16.charAt( cursorBackgroundColour ), this.width ); + } + } + + public synchronized void reset() + { + cursorColour = 0; + cursorBackgroundColour = 15; + cursorX = 0; + cursorY = 0; + cursorBlink = false; + clear(); + setChanged(); + palette.resetColours(); + } + + public int getWidth() + { + return width; + } + + public int getHeight() + { + return height; + } + + public synchronized void resize( int width, int height ) + { + if( width == this.width && height == this.height ) + { + return; + } + + int oldHeight = this.height; + int oldWidth = this.width; + TextBuffer[] oldText = text; + TextBuffer[] oldTextColour = textColour; + TextBuffer[] oldBackgroundColour = backgroundColour; + + this.width = width; + this.height = height; + + text = new TextBuffer[height]; + textColour = new TextBuffer[height]; + backgroundColour = new TextBuffer[height]; + for( int i = 0; i < this.height; i++ ) + { + if( i >= oldHeight ) + { + text[i] = new TextBuffer( ' ', this.width ); + textColour[i] = new TextBuffer( base16.charAt( cursorColour ), this.width ); + backgroundColour[i] = new TextBuffer( base16.charAt( cursorBackgroundColour ), this.width ); + } + else if( this.width == oldWidth ) + { + text[i] = oldText[i]; + textColour[i] = oldTextColour[i]; + backgroundColour[i] = oldBackgroundColour[i]; + } + else + { + text[i] = new TextBuffer( ' ', this.width ); + textColour[i] = new TextBuffer( base16.charAt( cursorColour ), this.width ); + backgroundColour[i] = new TextBuffer( base16.charAt( cursorBackgroundColour ), this.width ); + text[i].write( oldText[i] ); + textColour[i].write( oldTextColour[i] ); + backgroundColour[i].write( oldBackgroundColour[i] ); + } + } + setChanged(); + } + + public void setCursorPos( int x, int y ) + { + if( cursorX != x || cursorY != y ) + { + cursorX = x; + cursorY = y; + setChanged(); + } + } + + public void setCursorBlink( boolean blink ) + { + if( cursorBlink != blink ) + { + cursorBlink = blink; + setChanged(); + } + } + + public void setTextColour( int colour ) + { + if( cursorColour != colour ) + { + cursorColour = colour; + setChanged(); + } + } + + public void setBackgroundColour( int colour ) + { + if( cursorBackgroundColour != colour ) + { + cursorBackgroundColour = colour; + setChanged(); + } + } + + public int getCursorX() + { + return cursorX; + } + + public int getCursorY() + { + return cursorY; + } + + public boolean getCursorBlink() + { + return cursorBlink; + } + + public int getTextColour() + { + return cursorColour; + } + + public int getBackgroundColour() + { + return cursorBackgroundColour; + } + + @Nonnull + public Palette getPalette() + { + return palette; + } + + public synchronized void blit( String text, String textColour, String backgroundColour ) + { + int x = cursorX; + int y = cursorY; + if( y >= 0 && y < height ) + { + this.text[y].write( text, x ); + this.textColour[y].write( textColour, x ); + this.backgroundColour[y].write( backgroundColour, x ); + setChanged(); + } + } + + public synchronized void write( String text ) + { + int x = cursorX; + int y = cursorY; + if( y >= 0 && y < height ) + { + this.text[y].write( text, x ); + textColour[y].fill( base16.charAt( cursorColour ), x, x + text.length() ); + backgroundColour[y].fill( base16.charAt( cursorBackgroundColour ), x, x + text.length() ); + setChanged(); + } + } + + public synchronized void scroll( int yDiff ) + { + if( yDiff != 0 ) + { + TextBuffer[] newText = new TextBuffer[height]; + TextBuffer[] newTextColour = new TextBuffer[height]; + TextBuffer[] newBackgroundColour = new TextBuffer[height]; + for( int y = 0; y < height; y++ ) + { + int oldY = y + yDiff; + if( oldY >= 0 && oldY < height ) + { + newText[y] = text[oldY]; + newTextColour[y] = textColour[oldY]; + newBackgroundColour[y] = backgroundColour[oldY]; + } + else + { + newText[y] = new TextBuffer( ' ', width ); + newTextColour[y] = new TextBuffer( base16.charAt( cursorColour ), width ); + newBackgroundColour[y] = new TextBuffer( base16.charAt( cursorBackgroundColour ), width ); + } + } + text = newText; + textColour = newTextColour; + backgroundColour = newBackgroundColour; + setChanged(); + } + } + + public synchronized void clear() + { + for( int y = 0; y < height; y++ ) + { + text[y].fill( ' ' ); + textColour[y].fill( base16.charAt( cursorColour ) ); + backgroundColour[y].fill( base16.charAt( cursorBackgroundColour ) ); + } + setChanged(); + } + + public synchronized void clearLine() + { + int y = cursorY; + if( y >= 0 && y < height ) + { + text[y].fill( ' ' ); + textColour[y].fill( base16.charAt( cursorColour ) ); + backgroundColour[y].fill( base16.charAt( cursorBackgroundColour ) ); + setChanged(); + } + } + + public synchronized TextBuffer getLine( int y ) + { + if( y >= 0 && y < height ) + { + return text[y]; + } + return null; + } + + public synchronized void setLine( int y, String text, String textColour, String backgroundColour ) + { + this.text[y].write( text ); + this.textColour[y].write( textColour ); + this.backgroundColour[y].write( backgroundColour ); + setChanged(); + } + + public synchronized TextBuffer getTextColourLine( int y ) + { + if( y >= 0 && y < height ) + { + return textColour[y]; + } + return null; + } + + public synchronized TextBuffer getBackgroundColourLine( int y ) + { + if( y >= 0 && y < height ) + { + return backgroundColour[y]; + } + return null; + } + + public final void setChanged() + { + if( onChanged != null ) onChanged.run(); + } + + public synchronized void write( PacketByteBuf buffer ) + { + buffer.writeInt( cursorX ); + buffer.writeInt( cursorY ); + buffer.writeBoolean( cursorBlink ); + buffer.writeByte( cursorBackgroundColour << 4 | cursorColour ); + + for( int y = 0; y < height; y++ ) + { + TextBuffer text = this.text[y]; + TextBuffer textColour = this.textColour[y]; + TextBuffer backColour = backgroundColour[y]; + + for( int x = 0; x < width; x++ ) + { + buffer.writeByte( text.charAt( x ) & 0xFF ); + buffer.writeByte( getColour( + backColour.charAt( x ), Colour.BLACK ) << 4 | + getColour( textColour.charAt( x ), Colour.WHITE ) + ); + } + } + + palette.write( buffer ); + } + + public synchronized void read( PacketByteBuf buffer ) + { + cursorX = buffer.readInt(); + cursorY = buffer.readInt(); + cursorBlink = buffer.readBoolean(); + + byte cursorColour = buffer.readByte(); + cursorBackgroundColour = (cursorColour >> 4) & 0xF; + this.cursorColour = cursorColour & 0xF; + + for( int y = 0; y < height; y++ ) + { + TextBuffer text = this.text[y]; + TextBuffer textColour = this.textColour[y]; + TextBuffer backColour = backgroundColour[y]; + + for( int x = 0; x < 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 ) ); + } + } + + palette.read( buffer ); + setChanged(); + } + + public synchronized NbtCompound writeToNBT( NbtCompound nbt ) + { + nbt.putInt( "term_cursorX", cursorX ); + nbt.putInt( "term_cursorY", cursorY ); + nbt.putBoolean( "term_cursorBlink", cursorBlink ); + nbt.putInt( "term_textColour", cursorColour ); + nbt.putInt( "term_bgColour", cursorBackgroundColour ); + for( int n = 0; n < height; n++ ) + { + nbt.putString( "term_text_" + n, text[n].toString() ); + nbt.putString( "term_textColour_" + n, textColour[n].toString() ); + nbt.putString( "term_textBgColour_" + n, backgroundColour[n].toString() ); + } + + palette.writeToNBT( nbt ); + return nbt; + } + + public synchronized void readFromNBT( NbtCompound nbt ) + { + cursorX = nbt.getInt( "term_cursorX" ); + cursorY = nbt.getInt( "term_cursorY" ); + cursorBlink = nbt.getBoolean( "term_cursorBlink" ); + cursorColour = nbt.getInt( "term_textColour" ); + cursorBackgroundColour = nbt.getInt( "term_bgColour" ); + + for( int n = 0; n < height; n++ ) + { + text[n].fill( ' ' ); + if( nbt.contains( "term_text_" + n ) ) + { + text[n].write( nbt.getString( "term_text_" + n ) ); + } + textColour[n].fill( base16.charAt( cursorColour ) ); + if( nbt.contains( "term_textColour_" + n ) ) + { + textColour[n].write( nbt.getString( "term_textColour_" + n ) ); + } + backgroundColour[n].fill( base16.charAt( cursorBackgroundColour ) ); + if( nbt.contains( "term_textBgColour_" + n ) ) + { + backgroundColour[n].write( nbt.getString( "term_textBgColour_" + n ) ); + } + } + + 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/remappedSrc/dan200/computercraft/core/terminal/TextBuffer.java b/remappedSrc/dan200/computercraft/core/terminal/TextBuffer.java new file mode 100644 index 000000000..1c399764b --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/terminal/TextBuffer.java @@ -0,0 +1,86 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.terminal; + +public class TextBuffer +{ + private final char[] text; + + public TextBuffer( char c, int length ) + { + text = new char[length]; + fill( c ); + } + + public TextBuffer( String text ) + { + this.text = text.toCharArray(); + } + + public int length() + { + return text.length; + } + + public void write( String text ) + { + write( text, 0 ); + } + + public void write( String text, int start ) + { + int pos = start; + start = Math.max( start, 0 ); + int end = Math.min( start + text.length(), pos + text.length() ); + end = Math.min( end, this.text.length ); + for( int i = start; i < end; i++ ) + { + this.text[i] = text.charAt( i - pos ); + } + } + + public void write( TextBuffer text ) + { + int end = Math.min( text.length(), this.text.length ); + for( int i = 0; i < end; i++ ) + { + this.text[i] = text.charAt( i ); + } + } + + public void fill( char c ) + { + fill( c, 0, text.length ); + } + + public void fill( char c, int start, int end ) + { + start = Math.max( start, 0 ); + end = Math.min( end, text.length ); + for( int i = start; i < end; i++ ) + { + text[i] = c; + } + } + + public char charAt( int i ) + { + return text[i]; + } + + public void setChar( int i, char c ) + { + if( i >= 0 && i < text.length ) + { + text[i] = c; + } + } + + public String toString() + { + return new String( text ); + } +} diff --git a/remappedSrc/dan200/computercraft/core/tracking/ComputerTracker.java b/remappedSrc/dan200/computercraft/core/tracking/ComputerTracker.java new file mode 100644 index 000000000..0bc7787ca --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/tracking/ComputerTracker.java @@ -0,0 +1,122 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/tracking/Tracker.java b/remappedSrc/dan200/computercraft/core/tracking/Tracker.java new file mode 100644 index 000000000..979f5863f --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/tracking/Tracker.java @@ -0,0 +1,47 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/tracking/Tracking.java b/remappedSrc/dan200/computercraft/core/tracking/Tracking.java new file mode 100644 index 000000000..1097e7d8a --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/tracking/Tracking.java @@ -0,0 +1,87 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/tracking/TrackingContext.java b/remappedSrc/dan200/computercraft/core/tracking/TrackingContext.java new file mode 100644 index 000000000..95621da4d --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/tracking/TrackingContext.java @@ -0,0 +1,116 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/core/tracking/TrackingField.java b/remappedSrc/dan200/computercraft/core/tracking/TrackingField.java new file mode 100644 index 000000000..c8e13f479 --- /dev/null +++ b/remappedSrc/dan200/computercraft/core/tracking/TrackingField.java @@ -0,0 +1,96 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.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/remappedSrc/dan200/computercraft/fabric/events/ClientUnloadWorldEvent.java b/remappedSrc/dan200/computercraft/fabric/events/ClientUnloadWorldEvent.java new file mode 100644 index 000000000..ce687d947 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/events/ClientUnloadWorldEvent.java @@ -0,0 +1,23 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.events; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +@FunctionalInterface +public interface ClientUnloadWorldEvent +{ + Event EVENT = EventFactory.createArrayBacked( ClientUnloadWorldEvent.class, + callbacks -> () -> { + for( ClientUnloadWorldEvent callback : callbacks ) + { + callback.onClientUnloadWorld(); + } + } ); + + void onClientUnloadWorld(); +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/AffineTransformationAccess.java b/remappedSrc/dan200/computercraft/fabric/mixin/AffineTransformationAccess.java new file mode 100644 index 000000000..ced81eaaa --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/AffineTransformationAccess.java @@ -0,0 +1,25 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import net.minecraft.util.math.AffineTransformation; +import net.minecraft.util.math.Quaternion; +import net.minecraft.util.math.Vec3f; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin( AffineTransformation.class ) +public interface AffineTransformationAccess +{ + @Accessor + Vec3f getTranslation(); + + @Accessor + Vec3f getScale(); + + @Accessor + Quaternion getRotation1(); +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/BakedQuadAccess.java b/remappedSrc/dan200/computercraft/fabric/mixin/BakedQuadAccess.java new file mode 100644 index 000000000..fa7541eca --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/BakedQuadAccess.java @@ -0,0 +1,18 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import net.minecraft.client.render.model.BakedQuad; +import net.minecraft.client.texture.Sprite; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin( BakedQuad.class ) +public interface BakedQuadAccess +{ + @Accessor + Sprite getSprite(); +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/ChatHudAccess.java b/remappedSrc/dan200/computercraft/fabric/mixin/ChatHudAccess.java new file mode 100644 index 000000000..9d86ea657 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/ChatHudAccess.java @@ -0,0 +1,21 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import net.minecraft.client.gui.hud.ChatHud; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin( ChatHud.class ) +public interface ChatHudAccess +{ + @Invoker + void callAddMessage( Text text, int messageId ); + + @Invoker + void callRemoveMessage( int messageId ); +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/HeldItemRendererAccess.java b/remappedSrc/dan200/computercraft/fabric/mixin/HeldItemRendererAccess.java new file mode 100644 index 000000000..a799fdf3a --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/HeldItemRendererAccess.java @@ -0,0 +1,26 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.item.HeldItemRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Arm; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin( HeldItemRenderer.class ) +public interface HeldItemRendererAccess +{ + @Invoker + float callGetMapAngle( float tickDelta ); + + @Invoker + void callRenderArm( MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, Arm arm ); + + @Invoker + void callRenderArmHoldingItem( MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, float equipProgress, float swingProgress, Arm arm ); +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MinecraftServerAccess.java b/remappedSrc/dan200/computercraft/fabric/mixin/MinecraftServerAccess.java new file mode 100644 index 000000000..8ce40b911 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MinecraftServerAccess.java @@ -0,0 +1,18 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import net.minecraft.resource.ServerResourceManager; +import net.minecraft.server.MinecraftServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin( MinecraftServer.class ) +public interface MinecraftServerAccess +{ + @Accessor + ServerResourceManager getServerResourceManager(); +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinBlock.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinBlock.java new file mode 100644 index 000000000..05ed3bde3 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinBlock.java @@ -0,0 +1,36 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import dan200.computercraft.shared.util.DropConsumer; +import net.minecraft.block.Block; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Captures block drops. + * + * @see Block#dropStack(World, BlockPos, ItemStack) + */ +@Mixin( Block.class ) +public class MixinBlock +{ + @Inject( method = "dropStack", + at = @At( value = "INVOKE", target = "Lnet/minecraft/world/World;spawnEntity(Lnet/minecraft/entity/Entity;)Z" ), + cancellable = true ) + private static void dropStack( World world, BlockPos pos, ItemStack stack, CallbackInfo callbackInfo ) + { + if( DropConsumer.onHarvestDrops( world, pos, stack ) ) + { + callbackInfo.cancel(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinEntity.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinEntity.java new file mode 100644 index 000000000..5bb1671b9 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinEntity.java @@ -0,0 +1,35 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import dan200.computercraft.shared.util.DropConsumer; +import net.minecraft.entity.Entity; +import net.minecraft.entity.ItemEntity; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Captures entities. + * + * @see Entity#dropStack(ItemStack, float) + */ +@Mixin( Entity.class ) +public class MixinEntity +{ + @Inject( method = "dropStack(Lnet/minecraft/item/ItemStack;F)Lnet/minecraft/entity/ItemEntity;", + at = @At( value = "INVOKE", target = "Lnet/minecraft/world/World;spawnEntity(Lnet/minecraft/entity/Entity;)Z" ), + cancellable = true ) + public void dropStack( ItemStack stack, float height, CallbackInfoReturnable callbackInfo ) + { + if( DropConsumer.onLivingDrops( (Entity) (Object) this, stack ) ) + { + callbackInfo.setReturnValue( null ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinHeldItemRenderer.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinHeldItemRenderer.java new file mode 100644 index 000000000..4573b1bd2 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinHeldItemRenderer.java @@ -0,0 +1,65 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import dan200.computercraft.client.render.ItemPocketRenderer; +import dan200.computercraft.client.render.ItemPrintoutRenderer; +import dan200.computercraft.shared.media.items.ItemPrintout; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.item.HeldItemRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Arm; +import net.minecraft.util.Hand; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin( HeldItemRenderer.class ) +@Environment( EnvType.CLIENT ) +public class MixinHeldItemRenderer +{ + @Shadow + private void renderArmHoldingItem( MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, float equipProgress, float swingProgress, + Arm arm ) + { + } + + @Shadow + private float getMapAngle( float pitch ) + { + return 0; + } + + @Inject( method = "Lnet/minecraft/client/render/item/HeldItemRenderer;renderFirstPersonItem(Lnet/minecraft/client/network/AbstractClientPlayerEntity;" + + "FFLnet/minecraft/util/Hand;FLnet/minecraft/item/ItemStack;FLnet/minecraft/client/util/math/MatrixStack;" + + "Lnet/minecraft/client/render/VertexConsumerProvider;I)V", + at = @At( "HEAD" ), + cancellable = true ) + public void renderFirstPersonItem( + AbstractClientPlayerEntity player, float var2, float pitch, Hand hand, float swingProgress, + ItemStack stack, float equipProgress, MatrixStack matrixStack, VertexConsumerProvider provider, int light, + CallbackInfo callback + ) + { + if( stack.getItem() instanceof ItemPrintout ) + { + ItemPrintoutRenderer.INSTANCE.renderItemFirstPerson( matrixStack, provider, light, hand, pitch, equipProgress, swingProgress, stack ); + callback.cancel(); + } + else if( stack.getItem() instanceof ItemPocketComputer ) + { + ItemPocketRenderer.INSTANCE.renderItemFirstPerson( matrixStack, provider, light, hand, pitch, equipProgress, swingProgress, stack ); + callback.cancel(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinItemFrameEntityRenderer.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinItemFrameEntityRenderer.java new file mode 100644 index 000000000..47364d858 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinItemFrameEntityRenderer.java @@ -0,0 +1,39 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import dan200.computercraft.client.render.ItemPrintoutRenderer; +import dan200.computercraft.shared.media.items.ItemPrintout; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.ItemFrameEntityRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.decoration.ItemFrameEntity; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin( ItemFrameEntityRenderer.class ) +@Environment( EnvType.CLIENT ) +public class MixinItemFrameEntityRenderer +{ + @Inject( method = "render", at = @At( "HEAD" ), cancellable = true ) + private void renderItem( + ItemFrameEntity itemFrameEntity, float f, float g, MatrixStack matrixStack, + VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo info + ) + { + ItemStack stack = itemFrameEntity.getHeldItemStack(); + if( stack.getItem() instanceof ItemPrintout ) + { + ItemPrintoutRenderer.INSTANCE.renderInFrame( matrixStack, vertexConsumerProvider, stack ); + info.cancel(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinLanguage.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinLanguage.java new file mode 100644 index 000000000..fdd5eeeb3 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinLanguage.java @@ -0,0 +1,75 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.fabric.mixin; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonParseException; +import dan200.computercraft.shared.peripheral.generic.data.ItemData; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.util.Language; +import org.apache.logging.log4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.function.BiConsumer; + +/** + * Loads all mods en_us lang file into the default Language instance on the dedicated server. + * Needed so that lua code running on the server can access the display name of items. + * + * @see ItemData#fill + */ +@Mixin( Language.class ) +public class MixinLanguage +{ + @Shadow + private static Logger LOGGER; + + @Shadow + public static void load( InputStream inputStream, BiConsumer entryConsumer ) + { + } + + private static void loadModLangFile( String modId, BiConsumer biConsumer ) + { + String path = "/assets/" + modId + "/lang/en_us.json"; + + try ( InputStream inputStream = Language.class.getResourceAsStream( path ) ) + { + if ( inputStream == null ) return; + load( inputStream, biConsumer ); + } + catch ( JsonParseException | IOException e ) + { + LOGGER.error( "Couldn't read strings from " + path, e ); + } + } + + @Inject( method = "create", locals = LocalCapture.CAPTURE_FAILSOFT, at = @At( value = "INVOKE", remap = false, target = "Lcom/google/common/collect/ImmutableMap$Builder;build()Lcom/google/common/collect/ImmutableMap;" ) ) + private static void create( CallbackInfoReturnable cir, ImmutableMap.Builder builder ) + { + /* We must ensure that the keys are de-duplicated because we can't catch the error that might otherwise + * occur when the injected function calls build() on the ImmutableMap builder. So we use our own hash map and + * exclude "minecraft", as the injected function has already loaded those keys at this point. + */ + HashMap translations = new HashMap<>(); + + FabricLoader.getInstance().getAllMods().stream().map( modContainer -> modContainer.getMetadata().getId() ) + .filter( id -> !id.equals( "minecraft" ) ).forEach( id -> { + loadModLangFile( id, translations::put ); + } ); + + builder.putAll( translations ); + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinMinecraftClient.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinMinecraftClient.java new file mode 100644 index 000000000..8539fc6b9 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinMinecraftClient.java @@ -0,0 +1,38 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import dan200.computercraft.client.FrameInfo; +import dan200.computercraft.fabric.events.ClientUnloadWorldEvent; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.world.ClientWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin( MinecraftClient.class ) +public abstract class MixinMinecraftClient +{ + @Inject( method = "render", at = @At( "HEAD" ) ) + private void onRender( CallbackInfo info ) + { + FrameInfo.onRenderFrame(); + } + + @Inject( method = "disconnect(Lnet/minecraft/client/gui/screen/Screen;)V", at = @At( "RETURN" ) ) + private void disconnectAfter( Screen screen, CallbackInfo info ) + { + ClientUnloadWorldEvent.EVENT.invoker().onClientUnloadWorld(); + } + + @Inject( method = "joinWorld", at = @At( "RETURN" ) ) + private void joinWorldAfter( ClientWorld world, CallbackInfo info ) + { + ClientUnloadWorldEvent.EVENT.invoker().onClientUnloadWorld(); + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinScreen.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinScreen.java new file mode 100644 index 000000000..d53ba9939 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinScreen.java @@ -0,0 +1,29 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import dan200.computercraft.shared.command.ClientCommands; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.screen.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin( Screen.class ) +@Environment( EnvType.CLIENT ) +public class MixinScreen +{ + @Inject( method = "sendMessage(Ljava/lang/String;Z)V", at = @At( "HEAD" ), cancellable = true ) + public void sendClientCommand( String message, boolean add, CallbackInfo info ) + { + if( ClientCommands.onClientSendMessage( message ) ) + { + info.cancel(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinServerPlayerInteractionManager.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinServerPlayerInteractionManager.java new file mode 100644 index 000000000..3ca7e4620 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinServerPlayerInteractionManager.java @@ -0,0 +1,42 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import dan200.computercraft.shared.ComputerCraftRegistry; +import net.minecraft.advancement.criterion.Criteria; +import net.minecraft.block.BlockState; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.network.ServerPlayerInteractionManager; +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.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin( ServerPlayerInteractionManager.class ) +public class MixinServerPlayerInteractionManager +{ + @Inject( at = @At( value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;copy()Lnet/minecraft/item/ItemStack;", ordinal = 0 ), method = "interactBlock(Lnet/minecraft/server/network/ServerPlayerEntity;Lnet/minecraft/world/World;Lnet/minecraft/item/ItemStack;Lnet/minecraft/util/Hand;Lnet/minecraft/util/hit/BlockHitResult;)Lnet/minecraft/util/ActionResult;", cancellable = true ) + private void interact( ServerPlayerEntity player, World world, ItemStack stack, Hand hand, BlockHitResult hitResult, CallbackInfoReturnable cir ) + { + BlockPos pos = hitResult.getBlockPos(); + BlockState state = world.getBlockState( pos ); + if( player.getMainHandStack().getItem() == ComputerCraftRegistry.ModItems.DISK && state.getBlock() == ComputerCraftRegistry.ModBlocks.DISK_DRIVE ) + { + ActionResult actionResult = state.onUse( world, player, hand, hitResult ); + if( actionResult.isAccepted() ) + { + Criteria.ITEM_USED_ON_BLOCK.test( player, pos, stack ); + cir.setReturnValue( actionResult ); + } + } + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinServerWorld.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinServerWorld.java new file mode 100644 index 000000000..8d1d79e37 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinServerWorld.java @@ -0,0 +1,32 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import dan200.computercraft.shared.util.DropConsumer; +import net.minecraft.entity.Entity; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Captures item stacks spawned into the world. + * + * @see ServerWorld#spawnEntity(Entity) + */ +@Mixin( ServerWorld.class ) +public class MixinServerWorld +{ + @Inject( method = "spawnEntity", at = @At( "HEAD" ), cancellable = true ) + public void spawnEntity( Entity entity, CallbackInfoReturnable callbackInfo ) + { + if( DropConsumer.onEntitySpawn( entity ) ) + { + callbackInfo.setReturnValue( false ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinWorld.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinWorld.java new file mode 100644 index 000000000..4d8c34bf0 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinWorld.java @@ -0,0 +1,60 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import dan200.computercraft.shared.common.TileGeneric; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.annotation.Nullable; +import java.util.Collection; + +/** + * Horrible bodge to ensure a {@link BlockEntity}'s world is always present when setting a TE during another TE's tick. + * + * Forge does this, this is just a bodge to get Fabric in line with that behaviour. + */ +@Mixin( World.class ) +public class MixinWorld +{ + @Shadow + protected boolean iteratingTickingBlockEntities; + + @Inject( method = "setBlockEntity", at = @At( "HEAD" ) ) + public void setBlockEntity( BlockPos pos, @Nullable BlockEntity entity, CallbackInfo info ) + { + if( !World.isOutOfBuildLimitVertically( pos ) && entity != null && !entity.isRemoved() && iteratingTickingBlockEntities ) + { + setWorld( entity, this ); + } + } + + private static void setWorld( BlockEntity entity, Object world ) + { + if( entity.getWorld() != world && entity instanceof TileGeneric ) + { + entity.setLocation( (World) world, entity.getPos() ); + } + } + + @Inject( method = "addBlockEntities", at = @At( "HEAD" ) ) + public void addBlockEntities( Collection entities, CallbackInfo info ) + { + if( iteratingTickingBlockEntities ) + { + for( BlockEntity entity : entities ) + { + setWorld( entity, this ); + } + } + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MixinWorldRenderer.java b/remappedSrc/dan200/computercraft/fabric/mixin/MixinWorldRenderer.java new file mode 100644 index 000000000..fc16eb218 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MixinWorldRenderer.java @@ -0,0 +1,50 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import dan200.computercraft.client.render.CableHighlightRenderer; +import dan200.computercraft.client.render.MonitorHighlightRenderer; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin( WorldRenderer.class ) +@Environment( EnvType.CLIENT ) +public class MixinWorldRenderer +{ + @Inject( method = "drawBlockOutline", cancellable = true, at = @At( "HEAD" ) ) + public void drawBlockOutline( MatrixStack matrixStack, VertexConsumer vertexConsumer, Entity entity, double d, double e, double f, BlockPos blockPos, + BlockState blockState, CallbackInfo info ) + { + if( CableHighlightRenderer.drawHighlight( matrixStack, + vertexConsumer, + entity, + d, + e, + f, + blockPos, + blockState ) || MonitorHighlightRenderer.drawHighlight( matrixStack, + vertexConsumer, + entity, + d, + e, + f, + blockPos, + blockState ) ) + { + info.cancel(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/MusicDiscItemAccessor.java b/remappedSrc/dan200/computercraft/fabric/mixin/MusicDiscItemAccessor.java new file mode 100644 index 000000000..58a9fc4c8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/MusicDiscItemAccessor.java @@ -0,0 +1,18 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import net.minecraft.item.MusicDiscItem; +import net.minecraft.sound.SoundEvent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin( MusicDiscItem.class ) +public interface MusicDiscItemAccessor +{ + @Accessor + SoundEvent getSound(); +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/SignBlockEntityAccess.java b/remappedSrc/dan200/computercraft/fabric/mixin/SignBlockEntityAccess.java new file mode 100644 index 000000000..e1aa351da --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/SignBlockEntityAccess.java @@ -0,0 +1,18 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import net.minecraft.block.entity.SignBlockEntity; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin( SignBlockEntity.class ) +public interface SignBlockEntityAccess +{ + @Accessor + Text[] getText(); +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/SoundEventAccess.java b/remappedSrc/dan200/computercraft/fabric/mixin/SoundEventAccess.java new file mode 100644 index 000000000..87e9c7896 --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/SoundEventAccess.java @@ -0,0 +1,18 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin( SoundEvent.class ) +public interface SoundEventAccess +{ + @Accessor + Identifier getId(); +} diff --git a/remappedSrc/dan200/computercraft/fabric/mixin/WorldSavePathAccess.java b/remappedSrc/dan200/computercraft/fabric/mixin/WorldSavePathAccess.java new file mode 100644 index 000000000..fcaff65cf --- /dev/null +++ b/remappedSrc/dan200/computercraft/fabric/mixin/WorldSavePathAccess.java @@ -0,0 +1,20 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.fabric.mixin; + +import net.minecraft.util.WorldSavePath; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin( WorldSavePath.class ) +public interface WorldSavePathAccess +{ + @Invoker( "" ) + static WorldSavePath createWorldSavePath( String relativePath ) + { + throw new UnsupportedOperationException(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/BundledRedstone.java b/remappedSrc/dan200/computercraft/shared/BundledRedstone.java new file mode 100644 index 000000000..8c8be9b79 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/BundledRedstone.java @@ -0,0 +1,71 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared; + +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.isInBuildLimit( pos ) ? DefaultBundledRedstoneProvider.getDefaultBundledRedstoneOutput( world, pos, side ) : -1; + } + + public static int getOutput( World world, BlockPos pos, Direction side ) + { + int signal = getUnmaskedOutput( world, pos, side ); + return signal >= 0 ? signal : 0; + } + + private static int getUnmaskedOutput( World world, BlockPos pos, Direction side ) + { + if( !World.isInBuildLimit( 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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/ComputerCraftRegistry.java b/remappedSrc/dan200/computercraft/shared/ComputerCraftRegistry.java new file mode 100644 index 000000000..3e45b482f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/ComputerCraftRegistry.java @@ -0,0 +1,322 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +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.media.items.ItemDisk; +import dan200.computercraft.shared.media.items.ItemPrintout; +import dan200.computercraft.shared.media.items.ItemTreasureDisk; +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.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.upgrades.*; +import dan200.computercraft.shared.util.FixedPointTileEntityType; +import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings; +import net.fabricmc.fabric.api.screenhandler.v1.ScreenHandlerRegistry; +import net.minecraft.block.AbstractBlock; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +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.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.sound.BlockSoundGroup; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import static net.minecraft.util.registry.Registry.BLOCK_ENTITY_TYPE; + +public final class ComputerCraftRegistry +{ + public static final String MOD_ID = ComputerCraft.MOD_ID; + + public static void init() + { + Object[] o = { + ModTiles.CABLE, + ModBlocks.CABLE, + ModItems.CABLE, + ModEntities.TURTLE_PLAYER, + ModContainers.COMPUTER, + }; + + TurtleUpgrades.registerTurtleUpgrades(); + PocketUpgrades.registerPocketUpgrades(); + } + + public static final class ModBlocks + { + public static final BlockComputer COMPUTER_NORMAL = register( "computer_normal", + new BlockComputer( properties(), ComputerFamily.NORMAL, ModTiles.COMPUTER_NORMAL ) ); + public static final BlockComputer COMPUTER_ADVANCED = register( "computer_advanced", + new BlockComputer( properties(), + ComputerFamily.ADVANCED, + ModTiles.COMPUTER_ADVANCED ) ); + public static final BlockComputer COMPUTER_COMMAND = register( "computer_command", + new BlockComputer( FabricBlockSettings.copyOf( Blocks.STONE ) + .strength( -1, 6000000.0F ), + ComputerFamily.COMMAND, + ModTiles.COMPUTER_COMMAND ) ); + public static final BlockTurtle TURTLE_NORMAL = register( "turtle_normal", + new BlockTurtle( turtleProperties(), ComputerFamily.NORMAL, ModTiles.TURTLE_NORMAL ) ); + public static final BlockTurtle TURTLE_ADVANCED = register( "turtle_advanced", + new BlockTurtle( turtleProperties(), ComputerFamily.ADVANCED, ModTiles.TURTLE_ADVANCED ) ); + public static final BlockSpeaker SPEAKER = register( "speaker", new BlockSpeaker( properties() ) ); + public static final BlockDiskDrive DISK_DRIVE = register( "disk_drive", new BlockDiskDrive( properties() ) ); + public static final BlockPrinter PRINTER = register( "printer", new BlockPrinter( properties() ) ); + public static final BlockMonitor MONITOR_NORMAL = register( "monitor_normal", new BlockMonitor( properties(), ModTiles.MONITOR_NORMAL ) ); + public static final BlockMonitor MONITOR_ADVANCED = register( "monitor_advanced", new BlockMonitor( properties(), ModTiles.MONITOR_ADVANCED ) ); + public static final BlockWirelessModem WIRELESS_MODEM_NORMAL = register( "wireless_modem_normal", + new BlockWirelessModem( properties(), ModTiles.WIRELESS_MODEM_NORMAL ) ); + public static final BlockWirelessModem WIRELESS_MODEM_ADVANCED = register( "wireless_modem_advanced", + new BlockWirelessModem( properties(), ModTiles.WIRELESS_MODEM_ADVANCED ) ); + public static final BlockWiredModemFull WIRED_MODEM_FULL = register( "wired_modem_full", + new BlockWiredModemFull( emProperties(), ModTiles.WIRED_MODEM_FULL ) ); + public static final BlockCable CABLE = register( "cable", new BlockCable( emProperties() ) ); + + private static Block.Settings properties() + { + //return FabricBlockSettings.copyOf(Blocks.GLASS) + // .strength(2); + return AbstractBlock.Settings.of( Material.GLASS ) + .strength( 2F ) + .sounds( BlockSoundGroup.STONE ) + .nonOpaque(); + } + + private static Block.Settings turtleProperties() + { + return FabricBlockSettings.copyOf( Blocks.STONE ) + .strength( 2.5f ); + } + + private static Block.Settings emProperties() + { + return FabricBlockSettings.copyOf( Blocks.STONE ) + .strength( 1.5f ); + } + + public static T register( String id, T value ) + { + return Registry.register( Registry.BLOCK, new Identifier( MOD_ID, id ), value ); + } + } + + public static class ModTiles + { + + public static final BlockEntityType MONITOR_NORMAL = ofBlock( () -> ModBlocks.MONITOR_NORMAL, + "monitor_normal", + f -> new TileMonitor( f, false ) ); + public static final BlockEntityType MONITOR_ADVANCED = ofBlock( () -> ModBlocks.MONITOR_ADVANCED, + "monitor_advanced", + f -> new TileMonitor( f, true ) ); + public static final BlockEntityType COMPUTER_NORMAL = ofBlock( () -> ModBlocks.COMPUTER_NORMAL, + "computer_normal", + f -> new TileComputer( ComputerFamily.NORMAL, f ) ); + public static final BlockEntityType COMPUTER_ADVANCED = ofBlock( () -> ModBlocks.COMPUTER_ADVANCED, + "computer_advanced", + f -> new TileComputer( ComputerFamily.ADVANCED, f ) ); + public static final BlockEntityType COMPUTER_COMMAND = ofBlock( () -> ModBlocks.COMPUTER_COMMAND, + "computer_command", + f -> new TileCommandComputer( ComputerFamily.COMMAND, f ) ); + public static final BlockEntityType TURTLE_NORMAL = ofBlock( () -> ModBlocks.TURTLE_NORMAL, + "turtle_normal", + f -> new TileTurtle( f, ComputerFamily.NORMAL ) ); + public static final BlockEntityType TURTLE_ADVANCED = ofBlock( () -> ModBlocks.TURTLE_ADVANCED, + "turtle_advanced", + f -> new TileTurtle( f, ComputerFamily.ADVANCED ) ); + public static final BlockEntityType SPEAKER = ofBlock( () -> ModBlocks.SPEAKER, "speaker", TileSpeaker::new ); + public static final BlockEntityType DISK_DRIVE = ofBlock( () -> ModBlocks.DISK_DRIVE, "disk_drive", TileDiskDrive::new ); + public static final BlockEntityType PRINTER = ofBlock( () -> ModBlocks.PRINTER, "printer", TilePrinter::new ); + public static final BlockEntityType WIRED_MODEM_FULL = ofBlock( () -> ModBlocks.WIRED_MODEM_FULL, + "wired_modem_full", + TileWiredModemFull::new ); + public static final BlockEntityType CABLE = ofBlock( () -> ModBlocks.CABLE, "cable", TileCable::new ); + public static final BlockEntityType WIRELESS_MODEM_NORMAL = ofBlock( () -> ModBlocks.WIRELESS_MODEM_NORMAL, + "wireless_modem_normal", + f -> new TileWirelessModem( f, false ) ); + public static final BlockEntityType WIRELESS_MODEM_ADVANCED = ofBlock( () -> ModBlocks.WIRELESS_MODEM_ADVANCED, + "wireless_modem_advanced", + f -> new TileWirelessModem( f, true ) ); + + private static BlockEntityType ofBlock( Supplier block, String id, Function, T> factory ) + { + return Registry.register( BLOCK_ENTITY_TYPE, + new Identifier( MOD_ID, id ), + FixedPointTileEntityType.create( Objects.requireNonNull( block ), factory ) ); + } + } + + public static final class ModItems + { + private static final ItemGroup mainItemGroup = ComputerCraft.MAIN_GROUP; + public static final ItemComputer COMPUTER_NORMAL = ofBlock( ModBlocks.COMPUTER_NORMAL, ItemComputer::new ); + public static final ItemComputer COMPUTER_ADVANCED = ofBlock( ModBlocks.COMPUTER_ADVANCED, ItemComputer::new ); + public static final ItemComputer COMPUTER_COMMAND = ofBlock( ModBlocks.COMPUTER_COMMAND, ItemComputer::new ); + public static final ItemPocketComputer POCKET_COMPUTER_NORMAL = register( "pocket_computer_normal", + new ItemPocketComputer( properties().maxCount( 1 ), ComputerFamily.NORMAL ) ); + public static final ItemPocketComputer POCKET_COMPUTER_ADVANCED = register( "pocket_computer_advanced", + new ItemPocketComputer( properties().maxCount( 1 ), + ComputerFamily.ADVANCED ) ); + public static final ItemTurtle TURTLE_NORMAL = ofBlock( ModBlocks.TURTLE_NORMAL, ItemTurtle::new ); + public static final ItemTurtle TURTLE_ADVANCED = ofBlock( ModBlocks.TURTLE_ADVANCED, ItemTurtle::new ); + public static final ItemDisk DISK = register( "disk", new ItemDisk( properties().maxCount( 1 ) ) ); + public static final ItemTreasureDisk TREASURE_DISK = register( "treasure_disk", new ItemTreasureDisk( properties().maxCount( 1 ) ) ); + public static final ItemPrintout PRINTED_PAGE = register( "printed_page", new ItemPrintout( properties().maxCount( 1 ), ItemPrintout.Type.PAGE ) ); + public static final ItemPrintout PRINTED_PAGES = register( "printed_pages", new ItemPrintout( properties().maxCount( 1 ), ItemPrintout.Type.PAGES ) ); + public static final ItemPrintout PRINTED_BOOK = register( "printed_book", new ItemPrintout( properties().maxCount( 1 ), ItemPrintout.Type.BOOK ) ); + public static final BlockItem SPEAKER = ofBlock( ModBlocks.SPEAKER, BlockItem::new ); + public static final BlockItem DISK_DRIVE = ofBlock( ModBlocks.DISK_DRIVE, BlockItem::new ); + public static final BlockItem PRINTER = ofBlock( ModBlocks.PRINTER, BlockItem::new ); + public static final BlockItem MONITOR_NORMAL = ofBlock( ModBlocks.MONITOR_NORMAL, BlockItem::new ); + public static final BlockItem MONITOR_ADVANCED = ofBlock( ModBlocks.MONITOR_ADVANCED, BlockItem::new ); + public static final BlockItem WIRELESS_MODEM_NORMAL = ofBlock( ModBlocks.WIRELESS_MODEM_NORMAL, BlockItem::new ); + public static final BlockItem WIRELESS_MODEM_ADVANCED = ofBlock( ModBlocks.WIRELESS_MODEM_ADVANCED, BlockItem::new ); + public static final BlockItem WIRED_MODEM_FULL = ofBlock( ModBlocks.WIRED_MODEM_FULL, BlockItem::new ); + public static final ItemBlockCable.Cable CABLE = register( "cable", new ItemBlockCable.Cable( ModBlocks.CABLE, properties() ) ); + public static final ItemBlockCable.WiredModem WIRED_MODEM = register( "wired_modem", new ItemBlockCable.WiredModem( ModBlocks.CABLE, properties() ) ); + + private static I ofBlock( B parent, BiFunction supplier ) + { + return Registry.register( Registry.ITEM, Registry.BLOCK.getId( parent ), supplier.apply( parent, properties() ) ); + } + + private static Item.Settings properties() + { + return new Item.Settings().group( mainItemGroup ); + } + + private static T register( String id, T item ) + { + return Registry.register( Registry.ITEM, new Identifier( MOD_ID, id ), item ); + } + } + + public static class ModEntities + { + public static final EntityType TURTLE_PLAYER = Registry.register( Registry.ENTITY_TYPE, + new Identifier( MOD_ID, "turtle_player" ), + EntityType.Builder.create( SpawnGroup.MISC ).disableSaving() + .disableSummon() + .setDimensions( + 0, + 0 ) + .build( + ComputerCraft.MOD_ID + ":turtle_player" ) ); + } + + public static class ModContainers + { + public static final ScreenHandlerType COMPUTER = registerExtended( "computer", ContainerComputer::new ); + public static final ScreenHandlerType POCKET_COMPUTER = registerExtended( "pocket_computer", ContainerPocketComputer::new ); + public static final ScreenHandlerType TURTLE = registerExtended( "turtle", ContainerTurtle::new ); + public static final ScreenHandlerType DISK_DRIVE = registerSimple( "disk_drive", ContainerDiskDrive::new ); + public static final ScreenHandlerType PRINTER = registerSimple( "printer", ContainerPrinter::new ); + public static final ScreenHandlerType PRINTOUT = registerExtended( "printout", ContainerHeldItem::createPrintout ); + public static final ScreenHandlerType VIEW_COMPUTER = registerExtended( "view_computer", ContainerViewComputer::new ); + + private static ScreenHandlerType registerSimple( String id, + ScreenHandlerRegistry.SimpleClientHandlerFactory function ) + { + return ScreenHandlerRegistry.registerSimple( new Identifier( MOD_ID, id ), function ); + } + + private static ScreenHandlerType registerExtended( String id, ScreenHandlerRegistry.ExtendedClientHandlerFactory function ) + { + return ScreenHandlerRegistry.registerExtended( new Identifier( MOD_ID, id ), function ); + } + } + + public static final class TurtleUpgrades + { + public static TurtleModem wirelessModemNormal = new TurtleModem( false, new Identifier( ComputerCraft.MOD_ID, "wireless_modem_normal" ) ); + public static TurtleModem wirelessModemAdvanced = new TurtleModem( true, new Identifier( ComputerCraft.MOD_ID, "wireless_modem_advanced" ) ); + public static TurtleSpeaker speaker = new TurtleSpeaker( new Identifier( ComputerCraft.MOD_ID, "speaker" ) ); + + public static TurtleCraftingTable craftingTable = new TurtleCraftingTable( new Identifier( "minecraft", "crafting_table" ) ); + public static TurtleSword diamondSword = new TurtleSword( new Identifier( "minecraft", "diamond_sword" ), Items.DIAMOND_SWORD ); + public static TurtleShovel diamondShovel = new TurtleShovel( new Identifier( "minecraft", "diamond_shovel" ), Items.DIAMOND_SHOVEL ); + public static TurtleTool diamondPickaxe = new TurtleTool( new Identifier( "minecraft", "diamond_pickaxe" ), Items.DIAMOND_PICKAXE ); + public static TurtleAxe diamondAxe = new TurtleAxe( new Identifier( "minecraft", "diamond_axe" ), Items.DIAMOND_AXE ); + public static TurtleHoe diamondHoe = new TurtleHoe( new Identifier( "minecraft", "diamond_hoe" ), Items.DIAMOND_HOE ); + + public static TurtleTool netheritePickaxe = new TurtleTool( new Identifier( "minecraft", "netherite_pickaxe" ), Items.NETHERITE_PICKAXE ); + + public static void registerTurtleUpgrades() + { + ComputerCraftAPI.registerTurtleUpgrade( wirelessModemNormal ); + ComputerCraftAPI.registerTurtleUpgrade( wirelessModemAdvanced ); + ComputerCraftAPI.registerTurtleUpgrade( speaker ); + + ComputerCraftAPI.registerTurtleUpgrade( craftingTable ); + ComputerCraftAPI.registerTurtleUpgrade( diamondSword ); + ComputerCraftAPI.registerTurtleUpgrade( diamondShovel ); + ComputerCraftAPI.registerTurtleUpgrade( diamondPickaxe ); + ComputerCraftAPI.registerTurtleUpgrade( diamondAxe ); + ComputerCraftAPI.registerTurtleUpgrade( diamondHoe ); + + ComputerCraftAPI.registerTurtleUpgrade( netheritePickaxe ); + } + } + + public static final class PocketUpgrades + { + public static PocketModem wirelessModemNormal = new PocketModem( false ); + public static PocketModem wirelessModemAdvanced = new PocketModem( true ); + public static PocketSpeaker speaker = new PocketSpeaker(); + + public static void registerPocketUpgrades() + { + ComputerCraftAPI.registerPocketUpgrade( wirelessModemNormal ); + ComputerCraftAPI.registerPocketUpgrade( wirelessModemAdvanced ); + ComputerCraftAPI.registerPocketUpgrade( speaker ); + } + } + + +} diff --git a/remappedSrc/dan200/computercraft/shared/MediaProviders.java b/remappedSrc/dan200/computercraft/shared/MediaProviders.java new file mode 100644 index 000000000..a76eef0be --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/MediaProviders.java @@ -0,0 +1,57 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared; + +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/remappedSrc/dan200/computercraft/shared/Peripherals.java b/remappedSrc/dan200/computercraft/shared/Peripherals.java new file mode 100644 index 000000000..38fc5676e --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/Peripherals.java @@ -0,0 +1,64 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IPeripheralProvider; +import dan200.computercraft.shared.peripheral.generic.GenericPeripheralProvider; +import net.minecraft.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.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; + +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 ) + { + return World.isInBuildLimit( pos ) && !world.isClient ? getPeripheralAt( world, pos, side ) : null; + } + + @Nullable + private static IPeripheral getPeripheralAt( World world, BlockPos pos, Direction side ) + { + // Try the handlers in order: + for( IPeripheralProvider peripheralProvider : providers ) + { + try + { + IPeripheral peripheral = peripheralProvider.getPeripheral( world, pos, side ); + if( peripheral != null ) + { + return peripheral; + } + } + catch( Exception e ) + { + ComputerCraft.log.error( "Peripheral provider " + peripheralProvider + " errored.", e ); + } + } + + return GenericPeripheralProvider.getPeripheral( world, pos, side ); + } + +} diff --git a/remappedSrc/dan200/computercraft/shared/PocketUpgrades.java b/remappedSrc/dan200/computercraft/shared/PocketUpgrades.java new file mode 100644 index 000000000..0c5820bea --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/PocketUpgrades.java @@ -0,0 +1,88 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared; + +import dan200.computercraft.api.pocket.IPocketUpgrade; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenCustomHashMap; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Util; + +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 Map upgradeOwners = new Object2ObjectLinkedOpenCustomHashMap<>( Util.identityHashStrategy() ); + + 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 ); + + // Infer the mod id by the identifier of the upgrade. This is not how the forge api works, so it may break peripheral mods using the api. + // TODO: get the mod id of the mod that is currently being loaded. + ModContainer mc = FabricLoader.getInstance().getModContainer( upgrade.getUpgradeID().getNamespace() ).orElseGet( null ); + if( mc != null && mc.getMetadata().getId() != null ) upgradeOwners.put( upgrade, mc.getMetadata().getId() ); + } + + 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() && craftingStack.getItem() == stack.getItem() && upgrade.isItemSuitable( stack ) ) + { + 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( ComputerCraftRegistry.PocketUpgrades.wirelessModemNormal ); + vanilla.add( ComputerCraftRegistry.PocketUpgrades.wirelessModemAdvanced ); + vanilla.add( ComputerCraftRegistry.PocketUpgrades.speaker ); + return vanilla; + } + + public static Iterable getUpgrades() + { + return Collections.unmodifiableCollection( upgrades.values() ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/TurtlePermissions.java b/remappedSrc/dan200/computercraft/shared/TurtlePermissions.java new file mode 100644 index 000000000..f6236a770 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/TurtlePermissions.java @@ -0,0 +1,39 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared; + +import com.google.common.eventbus.Subscribe; +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; + +public final class TurtlePermissions +{ + public static boolean isBlockEditable( World world, BlockPos pos, PlayerEntity player ) + { + return isBlockEnterable( world, pos, player ); + } + + 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 )); + } + + @Subscribe + public void onTurtleAction( TurtleActionEvent event ) + { + if( ComputerCraft.turtleDisabledActions.contains( event.getAction() ) ) + { + event.setCanceled( true, "Action has been disabled" ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/TurtleUpgrades.java b/remappedSrc/dan200/computercraft/shared/TurtleUpgrades.java new file mode 100644 index 000000000..f59b40577 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/TurtleUpgrades.java @@ -0,0 +1,177 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import net.minecraft.item.ItemStack; + +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; + id = upgrade.getUpgradeID() + .toString(); + // TODO This should be the mod id of the mod the peripheral comes from + modId = ComputerCraft.MOD_ID; + 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() && craftingStack.getItem() == stack.getItem() && wrapper.upgrade.isItemSuitable( stack ) ) + { + return wrapper.upgrade; + } + } + + return null; + } + + public static Stream getVanillaUpgrades() + { + if( vanilla == null ) + { + vanilla = new ITurtleUpgrade[] { + // ComputerCraft upgrades + ComputerCraftRegistry.TurtleUpgrades.wirelessModemNormal, + ComputerCraftRegistry.TurtleUpgrades.wirelessModemAdvanced, + ComputerCraftRegistry.TurtleUpgrades.speaker, + + // Vanilla Minecraft upgrades + ComputerCraftRegistry.TurtleUpgrades.diamondPickaxe, + ComputerCraftRegistry.TurtleUpgrades.diamondAxe, + ComputerCraftRegistry.TurtleUpgrades.diamondSword, + ComputerCraftRegistry.TurtleUpgrades.diamondShovel, + ComputerCraftRegistry.TurtleUpgrades.diamondHoe, + ComputerCraftRegistry.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/remappedSrc/dan200/computercraft/shared/command/ClientCommands.java b/remappedSrc/dan200/computercraft/shared/command/ClientCommands.java new file mode 100644 index 000000000..6845f2815 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/ClientCommands.java @@ -0,0 +1,50 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.command; + +import dan200.computercraft.shared.util.IDAssigner; +import net.minecraft.util.Util; + +import java.io.File; + +/** + * Basic client-side commands. + * + * Simply hooks into client chat messages and intercepts matching strings. + */ +public final class ClientCommands +{ + public static final String OPEN_COMPUTER = "/computercraft open-computer "; + + private ClientCommands() + { + } + + public static boolean onClientSendMessage( String message ) + { + // Emulate the command on the client side + if( message.startsWith( OPEN_COMPUTER ) ) + { + String idStr = message.substring( OPEN_COMPUTER.length() ).trim(); + int id; + try + { + id = Integer.parseInt( idStr ); + } + catch( NumberFormatException ignore ) + { + return true; + } + + File file = new File( IDAssigner.getDir(), "computer/" + id ); + if( !file.isDirectory() ) return true; + + Util.getOperatingSystem().open( file ); + return true; + } + return false; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/command/CommandComputerCraft.java b/remappedSrc/dan200/computercraft/shared/command/CommandComputerCraft.java new file mode 100644 index 000000000..e10a31cc7 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -0,0 +1,422 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.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 dan200.computercraft.shared.util.IDAssigner; +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket; +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.TranslatableText; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import java.io.File; +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, Boolean dedicated ) + { + 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.requestTeleport( + 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" ); + computer.sendTerminalState( player ); + ViewComputerContainerData container = new ViewComputerContainerData( computer ); + container.open( player, new ExtendedScreenHandlerFactory() + { + @Override + public void writeScreenOpeningData( ServerPlayerEntity player, PacketByteBuf buf ) + { + container.toBytes( buf ); + } + + @Nonnull + @Override + public MutableText 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 MutableText 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" ) + ) ); + } + + if( UserLevel.OWNER.test( source ) && isPlayer( source ) ) + { + MutableText linkPath = linkStorage( computerId ); + if( linkPath != null ) out.append( " " ).append( linkPath ); + } + + return out; + } + + private static MutableText 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() ); + } + } + + private static MutableText linkStorage( int id ) + { + File file = new File( IDAssigner.getDir(), "computer/" + id ); + if( !file.isDirectory() ) return null; + + return link( + text( "\u270E" ), + ClientCommands.OPEN_COMPUTER + id, + translate( "commands.computercraft.dump.open_path" ) + ); + } + + @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() ); + + MutableText[] headers = new MutableText[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 ); + + MutableText computerComponent = linkComputer( source, serverComputer, entry.getComputerId() ); + + MutableText[] row = new MutableText[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/remappedSrc/dan200/computercraft/shared/command/CommandUtils.java b/remappedSrc/dan200/computercraft/shared/command/CommandUtils.java new file mode 100644 index 000000000..d64a7935d --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/CommandUtils.java @@ -0,0 +1,74 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.command; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import dan200.computercraft.api.turtle.FakePlayer; +import net.minecraft.command.CommandSource; +import net.minecraft.entity.Entity; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; + +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, T[] candidates, Function toString ) + { + return suggest( builder, Arrays.asList( candidates ), toString ); + } + + 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(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/command/Exceptions.java b/remappedSrc/dan200/computercraft/shared/command/Exceptions.java new file mode 100644 index 000000000..88f173093 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/Exceptions.java @@ -0,0 +1,40 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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" ); + public static final SimpleCommandExceptionType ARGUMENT_EXPECTED = translated( "argument.computercraft.argument_expected" ); + 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" ); + + 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/remappedSrc/dan200/computercraft/shared/command/UserLevel.java b/remappedSrc/dan200/computercraft/shared/command/UserLevel.java new file mode 100644 index 000000000..ec1994247 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/UserLevel.java @@ -0,0 +1,73 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.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; + + if( this == OWNER || this == OWNER_OP ) + { + MinecraftServer server = source.getServer(); + Entity sender = source.getEntity(); + if( server.isSinglePlayer() && sender instanceof PlayerEntity && + ((PlayerEntity) sender).getGameProfile().getName().equalsIgnoreCase( server.getServerModName() ) ) + { + return true; + } + } + + return source.hasPermissionLevel( toLevel() ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java b/remappedSrc/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java new file mode 100644 index 000000000..a8520734d --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java @@ -0,0 +1,41 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 +{ + 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() ); + } + + private static > void register( Identifier id, T instance ) + { + registerUnsafe( id, instance.getClass(), new ConstantArgumentSerializer<>( () -> instance ) ); + } + + private static > void register( Identifier id, Class type, ArgumentSerializer serializer ) + { + ArgumentTypes.register( id.toString(), type, serializer ); + } + + @SuppressWarnings( "unchecked" ) + private static > void registerUnsafe( Identifier id, Class type, ArgumentSerializer serializer ) + { + ArgumentTypes.register( id.toString(), type, (ArgumentSerializer) serializer ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/command/arguments/ChoiceArgumentType.java b/remappedSrc/dan200/computercraft/shared/command/arguments/ChoiceArgumentType.java new file mode 100644 index 000000000..642b5af10 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/arguments/ChoiceArgumentType.java @@ -0,0 +1,86 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java b/remappedSrc/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java new file mode 100644 index 000000000..06840b154 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java @@ -0,0 +1,103 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 net.minecraft.server.command.ServerCommandSource; + +import java.util.Collection; +import java.util.concurrent.CompletableFuture; + +import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_MANY; + +public final class ComputerArgumentType implements ArgumentType +{ + private static final ComputerArgumentType INSTANCE = new ComputerArgumentType(); + + private 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() ); + } + + @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/remappedSrc/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java b/remappedSrc/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java new file mode 100644 index 000000000..db7900c2f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java @@ -0,0 +1,216 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 net.minecraft.command.argument.serialize.ArgumentSerializer; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.server.command.ServerCommandSource; + +import javax.annotation.Nonnull; +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" ); + private final boolean requireSome; + + private ComputersArgumentType( boolean requireSome ) + { + this.requireSome = requireSome; + } + + 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() ); + } + + 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; + } + + @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() ) ); + } + + @FunctionalInterface + public interface ComputersSupplier + { + Collection unwrap( ServerCommandSource source ) throws CommandSyntaxException; + } + + public static class Serializer implements ArgumentSerializer + { + + @Override + public void toPacket( @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 toJson( @Nonnull ComputersArgumentType arg, @Nonnull JsonObject json ) + { + json.addProperty( "requireSome", arg.requireSome ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java b/remappedSrc/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java new file mode 100644 index 000000000..d38c6b51c --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java @@ -0,0 +1,182 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 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 javax.annotation.Nonnull; +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, false, 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 toPacket( @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 toJson( @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/remappedSrc/dan200/computercraft/shared/command/arguments/TrackingFieldArgumentType.java b/remappedSrc/dan200/computercraft/shared/command/arguments/TrackingFieldArgumentType.java new file mode 100644 index 000000000..06f2330fd --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/arguments/TrackingFieldArgumentType.java @@ -0,0 +1,28 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/command/builder/ArgCommand.java b/remappedSrc/dan200/computercraft/shared/command/builder/ArgCommand.java new file mode 100644 index 000000000..ea834c4c8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/builder/ArgCommand.java @@ -0,0 +1,23 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/command/builder/CommandBuilder.java b/remappedSrc/dan200/computercraft/shared/command/builder/CommandBuilder.java new file mode 100644 index 000000000..72233bd69 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/builder/CommandBuilder.java @@ -0,0 +1,138 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 net.minecraft.server.command.ServerCommandSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; + +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, T defaultValue ) + { + return argManyValue( name, type, Collections.singletonList( defaultValue ) ); + } + + public CommandNodeBuilder>> argManyValue( String name, ArgumentType type, List empty ) + { + return argMany( name, type, () -> empty ); + } + + public CommandNodeBuilder>> argMany( String name, ArgumentType type, Supplier> empty ) + { + return argMany( name, RepeatArgumentType.some( 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 ); + }; + } + + private ArgumentBuilder tail( Command command ) + { + ArgumentBuilder defaultTail = args.get( args.size() - 1 ); + defaultTail.executes( command ); + if( requires != null ) + { + defaultTail.requires( requires ); + } + return defaultTail; + } + + @SuppressWarnings( "unchecked" ) + private static List getList( CommandContext context, String name ) + { + return (List) context.getArgument( name, List.class ); + } + + private CommandNode link( ArgumentBuilder tail ) + { + for( int i = args.size() - 2; i >= 0; i-- ) + { + tail = args.get( i ) + .then( tail ); + } + return tail.build(); + } + + public CommandNodeBuilder>> argManyFlatten( String name, ArgumentType> type, Supplier> empty ) + { + return argMany( name, RepeatArgumentType.someFlat( type, ARGUMENT_EXPECTED ), empty ); + } + + @Override + public CommandNode executes( Command command ) + { + if( args.isEmpty() ) + { + throw new IllegalStateException( "Cannot have empty arg chain builder" ); + } + + return link( tail( command ) ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/command/builder/CommandNodeBuilder.java b/remappedSrc/dan200/computercraft/shared/command/builder/CommandNodeBuilder.java new file mode 100644 index 000000000..dc10a5eea --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/builder/CommandNodeBuilder.java @@ -0,0 +1,27 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java b/remappedSrc/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java new file mode 100644 index 000000000..c7bd1248e --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java @@ -0,0 +1,221 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 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 javax.annotation.Nonnull; +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 static final Formatting HEADER = Formatting.LIGHT_PURPLE; + private static final Formatting SYNOPSIS = Formatting.AQUA; + private static final Formatting NAME = Formatting.GREEN; + private final Collection children = new ArrayList<>(); + + private HelpingArgumentBuilder( String literal ) + { + super( literal ); + } + + public static HelpingArgumentBuilder choice( String literal ) + { + return new HelpingArgumentBuilder( literal ); + } + + 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() + .getServer() + .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; + } + + @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 LiteralArgumentBuilder executes( final Command command ) + { + throw new IllegalStateException( "Cannot use executes on a HelpingArgumentBuilder" ); + } + + @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 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; + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/command/text/ChatHelpers.java b/remappedSrc/dan200/computercraft/shared/command/text/ChatHelpers.java new file mode 100644 index 000000000..03a8deae0 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/text/ChatHelpers.java @@ -0,0 +1,103 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.command.text; + +import net.minecraft.text.*; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.BlockPos; + +/** + * 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 ) + { + MutableText component = new LiteralText( text == null ? "" : text ); + component.setStyle( component.getStyle().withColor( colour ) ); + return component; + } + + public static T coloured( T component, Formatting colour ) + { + component.setStyle( component.getStyle().withColor( 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( MutableText... children ) + { + MutableText component = new LiteralText( "" ); + for( MutableText 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, MutableText toolTip ) + { + return link( component, new ClickEvent( ClickEvent.Action.RUN_COMMAND, command ), toolTip ); + } + + public static MutableText link( MutableText component, ClickEvent click, MutableText toolTip ) + { + Style style = component.getStyle(); + + if( style.getColor() == null ) style = style.withColor( Formatting.YELLOW ); + style = style.withClickEvent( click ); + style = style.withHoverEvent( new HoverEvent( HoverEvent.Action.SHOW_TEXT, toolTip ) ); + + component.setStyle( style ); + + return component; + } + + public static MutableText header( String text ) + { + return coloured( text, HEADER ); + } + + public static MutableText copy( String text ) + { + LiteralText name = new LiteralText( text ); + name.setStyle( name.getStyle() + .withClickEvent( new ClickEvent( ClickEvent.Action.COPY_TO_CLIPBOARD, text ) ) + .withHoverEvent( new HoverEvent( HoverEvent.Action.SHOW_TEXT, new TranslatableText( "gui.computercraft.tooltip.copy" ) ) ) ); + return name; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/command/text/ServerTableFormatter.java b/remappedSrc/dan200/computercraft/shared/command/text/ServerTableFormatter.java new file mode 100644 index 000000000..fe8ee5b7c --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/text/ServerTableFormatter.java @@ -0,0 +1,55 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.command.text; + +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Nullable; + +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/remappedSrc/dan200/computercraft/shared/command/text/TableBuilder.java b/remappedSrc/dan200/computercraft/shared/command/text/TableBuilder.java new file mode 100644 index 000000000..c65c58294 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/text/TableBuilder.java @@ -0,0 +1,153 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.command.text; + +import dan200.computercraft.shared.command.CommandUtils; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.client.ChatTableClientMessage; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +public class TableBuilder +{ + private final int id; + private final Text[] headers; + private final ArrayList rows = new ArrayList<>(); + private int columns = -1; + 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; + } + + 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 ); + } + } + + /** + * 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(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/command/text/TableFormatter.java b/remappedSrc/dan200/computercraft/shared/command/text/TableFormatter.java new file mode 100644 index 000000000..3c942e2d8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/command/text/TableFormatter.java @@ -0,0 +1,141 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.command.text; + +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Nullable; + +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 ); + + 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(); + } + + int getWidth( Text component ); + + /** + * Get the minimum padding between each column. + * + * @return The minimum padding. + */ + int getColumnPadding(); + + /** + * 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 ); + + void writeLine( int id, Text component ); +} diff --git a/remappedSrc/dan200/computercraft/shared/common/BlockGeneric.java b/remappedSrc/dan200/computercraft/shared/common/BlockGeneric.java new file mode 100644 index 000000000..4781320ac --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/common/BlockGeneric.java @@ -0,0 +1,101 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.common; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockRenderType; +import net.minecraft.block.BlockState; +import net.minecraft.block.BlockWithEntity; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Random; + +public abstract class BlockGeneric extends BlockWithEntity +{ + private final BlockEntityType type; + + public BlockGeneric( Settings settings, BlockEntityType type ) + { + super( settings ); + this.type = type; + } + + @Override + public BlockRenderType getRenderType( BlockState state ) + { + return BlockRenderType.MODEL; + } + + @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 + @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 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(); + } + } + + @Nullable + @Override + public BlockEntity createBlockEntity( @Nonnull BlockView world ) + { + return type.instantiate(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/common/ClientTerminal.java b/remappedSrc/dan200/computercraft/shared/common/ClientTerminal.java new file mode 100644 index 000000000..45601a8bc --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/common/ClientTerminal.java @@ -0,0 +1,97 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.common; + +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.network.client.TerminalState; +import net.minecraft.nbt.NbtCompound; + +public class ClientTerminal implements ITerminal +{ + private boolean colour; + private Terminal terminal; + private boolean terminalChanged; + + public ClientTerminal( boolean colour ) + { + this.colour = colour; + terminal = null; + terminalChanged = false; + } + + public boolean pollTerminalChanged() + { + boolean changed = terminalChanged; + terminalChanged = false; + return changed; + } + + // ITerminal implementation + + @Override + public Terminal getTerminal() + { + return terminal; + } + + @Override + public boolean isColour() + { + return colour; + } + + public void read( TerminalState state ) + { + colour = state.colour; + if( state.hasTerminal() ) + { + resizeTerminal( state.width, state.height ); + state.apply( terminal ); + } + else + { + deleteTerminal(); + } + } + + private void resizeTerminal( int width, int height ) + { + if( terminal == null ) + { + terminal = new Terminal( width, height, () -> terminalChanged = true ); + terminalChanged = true; + } + else + { + terminal.resize( width, height ); + } + } + + private void deleteTerminal() + { + if( terminal != null ) + { + terminal = null; + terminalChanged = true; + } + } + + public void readDescription( NbtCompound nbt ) + { + colour = nbt.getBoolean( "colour" ); + if( nbt.contains( "terminal" ) ) + { + NbtCompound terminal = nbt.getCompound( "terminal" ); + resizeTerminal( terminal.getInt( "term_width" ), terminal.getInt( "term_height" ) ); + this.terminal.readFromNBT( terminal ); + } + else + { + deleteTerminal(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/common/ColourableRecipe.java b/remappedSrc/dan200/computercraft/shared/common/ColourableRecipe.java new file mode 100644 index 000000000..251bb104b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/common/ColourableRecipe.java @@ -0,0 +1,107 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.common; + +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 +{ + public static final RecipeSerializer SERIALIZER = new SpecialRecipeSerializer<>( ColourableRecipe::new ); + + 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 craft( @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; + } + else + { + DyeColor dye = ColourUtils.getStackColour( stack ); + if( dye != null ) tracker.addColour( dye ); + } + } + + if( colourable.isEmpty() ) return ItemStack.EMPTY; + + ItemStack stack = ((IColouredItem) colourable.getItem()).withColour( colourable, tracker.getColour() ); + stack.setCount( 1 ); + return stack; + } + + @Override + public boolean fits( int x, int y ) + { + return x >= 2 && y >= 2; + } + + @Override + @Nonnull + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/common/ContainerHeldItem.java b/remappedSrc/dan200/computercraft/shared/common/ContainerHeldItem.java new file mode 100644 index 000000000..64a5fe91f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/common/ContainerHeldItem.java @@ -0,0 +1,100 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.common; + +import dan200.computercraft.shared.ComputerCraftRegistry; +import dan200.computercraft.shared.network.container.HeldItemContainerData; +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.network.ServerPlayerEntity; +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, PacketByteBuf data ) + { + return createPrintout( id, inventory, new HeldItemContainerData( data ) ); + } + + public static ContainerHeldItem createPrintout( int id, PlayerInventory inventory, HeldItemContainerData data ) + { + return new ContainerHeldItem( ComputerCraftRegistry.ModContainers.PRINTOUT, 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 ExtendedScreenHandlerFactory + { + private final ScreenHandlerType type; + private final Text name; + private final Hand hand; + + public Factory( ScreenHandlerType type, ItemStack stack, Hand hand ) + { + this.type = type; + 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 ); + } + + @Override + public void writeScreenOpeningData( ServerPlayerEntity serverPlayerEntity, PacketByteBuf packetByteBuf ) + { + packetByteBuf.writeEnumConstant( hand ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/common/DefaultBundledRedstoneProvider.java b/remappedSrc/dan200/computercraft/shared/common/DefaultBundledRedstoneProvider.java new file mode 100644 index 000000000..b1642f413 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/common/DefaultBundledRedstoneProvider.java @@ -0,0 +1,39 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/common/IBundledRedstoneBlock.java b/remappedSrc/dan200/computercraft/shared/common/IBundledRedstoneBlock.java new file mode 100644 index 000000000..0be19a1ff --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/common/IBundledRedstoneBlock.java @@ -0,0 +1,18 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/common/IColouredItem.java b/remappedSrc/dan200/computercraft/shared/common/IColouredItem.java new file mode 100644 index 000000000..d3e5041ec --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/common/IColouredItem.java @@ -0,0 +1,50 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.common; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; + +public interface IColouredItem +{ + String NBT_COLOUR = "Color"; + + default int getColour( ItemStack stack ) + { + return getColourBasic( stack ); + } + + static int getColourBasic( ItemStack stack ) + { + NbtCompound tag = stack.getNbt(); + return tag != null && tag.contains( NBT_COLOUR ) ? tag.getInt( NBT_COLOUR ) : -1; + } + + default ItemStack withColour( ItemStack stack, int colour ) + { + ItemStack copy = stack.copy(); + setColourBasic( copy, colour ); + return copy; + } + + static void setColourBasic( ItemStack stack, int colour ) + { + if( colour == -1 ) + { + NbtCompound tag = stack.getNbt(); + if( tag != null ) + { + tag.remove( NBT_COLOUR ); + } + } + else + { + stack.getOrCreateNbt() + .putInt( NBT_COLOUR, colour ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/common/ITerminal.java b/remappedSrc/dan200/computercraft/shared/common/ITerminal.java new file mode 100644 index 000000000..2a6a9a9e0 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/common/ITerminal.java @@ -0,0 +1,16 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.common; + +import dan200.computercraft.core.terminal.Terminal; + +public interface ITerminal +{ + Terminal getTerminal(); + + boolean isColour(); +} diff --git a/remappedSrc/dan200/computercraft/shared/common/ServerTerminal.java b/remappedSrc/dan200/computercraft/shared/common/ServerTerminal.java new file mode 100644 index 000000000..e3573ae15 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/common/ServerTerminal.java @@ -0,0 +1,99 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.common; + +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.network.client.TerminalState; +import java.util.concurrent.atomic.AtomicBoolean; +import net.minecraft.nbt.NbtCompound; + +public class ServerTerminal implements ITerminal +{ + private final boolean colour; + private final AtomicBoolean terminalChanged = new AtomicBoolean( false ); + private Terminal terminal; + private boolean terminalChangedLastFrame = false; + + public ServerTerminal( boolean colour ) + { + this.colour = colour; + terminal = null; + } + + public ServerTerminal( boolean colour, int terminalWidth, int terminalHeight ) + { + this.colour = colour; + terminal = new Terminal( terminalWidth, terminalHeight, this::markTerminalChanged ); + } + + protected void markTerminalChanged() + { + terminalChanged.set( true ); + } + + protected void resize( int width, int height ) + { + if( terminal == null ) + { + terminal = new Terminal( width, height, this::markTerminalChanged ); + markTerminalChanged(); + } + else + { + terminal.resize( width, height ); + } + } + + public void delete() + { + if( terminal != null ) + { + terminal = null; + markTerminalChanged(); + } + } + + public void update() + { + terminalChangedLastFrame = terminalChanged.getAndSet( false ); + } + + public boolean hasTerminalChanged() + { + return terminalChangedLastFrame; + } + + @Override + public Terminal getTerminal() + { + return terminal; + } + + @Override + public boolean isColour() + { + return colour; + } + + public TerminalState write() + { + return new TerminalState( colour, terminal ); + } + + public void writeDescription( NbtCompound nbt ) + { + nbt.putBoolean( "colour", colour ); + if( terminal != null ) + { + NbtCompound terminal = new NbtCompound(); + terminal.putInt( "term_width", this.terminal.getWidth() ); + terminal.putInt( "term_height", this.terminal.getHeight() ); + this.terminal.writeToNBT( terminal ); + nbt.put( "terminal", terminal ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/common/TileGeneric.java b/remappedSrc/dan200/computercraft/shared/common/TileGeneric.java new file mode 100644 index 000000000..941cc3893 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/common/TileGeneric.java @@ -0,0 +1,104 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.common; + +import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable; +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.NbtCompound; +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 implements BlockEntityClientSerializable +{ + public TileGeneric( BlockEntityType type ) + { + super( type ); + } + + public void destroy() + { + } + + public void onChunkUnloaded() + { + } + + 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() + { + } + + 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 double getInteractRange( PlayerEntity player ) + { + return 8.0; + } + + @Override + public void fromClientTag( NbtCompound compoundTag ) + { + readDescription( compoundTag ); + } + + protected void readDescription( @Nonnull NbtCompound nbt ) + { + } + + @Override + public NbtCompound toClientTag( NbtCompound compoundTag ) + { + writeDescription( compoundTag ); + return compoundTag; + } + + protected void writeDescription( @Nonnull NbtCompound nbt ) + { + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/apis/CommandAPI.java b/remappedSrc/dan200/computercraft/shared/computer/apis/CommandAPI.java new file mode 100644 index 000000000..079133e89 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/apis/CommandAPI.java @@ -0,0 +1,301 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.apis; + +import com.google.common.collect.ImmutableMap; +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.util.NBTUtil; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.state.property.Property; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.registry.Registry; +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" }; + } + + /** + * 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 ); + } + + 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 Object createOutput( String output ) + { + return new Object[] { output }; + } + + /** + * 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.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.isInBuildLimit( min ) || !World.isInBuildLimit( 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; + } + + private static Map getBlockInfo( World world, BlockPos pos ) + { + // Get the details of the block + BlockState state = world.getBlockState( pos ); + Block block = state.getBlock(); + + Map table = new HashMap<>(); + table.put( "name", Registry.BLOCK.getId( block ).toString() ); + table.put( "world", world.getRegistryKey() ); + + Map stateTable = new HashMap<>(); + for( ImmutableMap.Entry, Comparable> entry : state.getEntries().entrySet() ) + { + Property property = entry.getKey(); + stateTable.put( property.getName(), getPropertyValue( property, entry.getValue() ) ); + } + table.put( "state", stateTable ); + + BlockEntity tile = world.getBlockEntity( pos ); + if( tile != null ) + { + table.put( "nbt", NBTUtil.toLua( tile.writeNbt( new NbtCompound() ) ) ); + } + + return table; + } + + @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 ); + } + + /** + * 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.isInBuildLimit( position ) ) + { + return getBlockInfo( world, position ); + } + else + { + throw new LuaException( "Co-ordinates out of range" ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/blocks/BlockComputer.java b/remappedSrc/dan200/computercraft/shared/computer/blocks/BlockComputer.java new file mode 100644 index 000000000..f597a84e2 --- /dev/null +++ b/remappedSrc/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-2021. 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 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, BlockEntityType type ) + { + super( settings, family, type ); + setDefaultState( getDefaultState().with( FACING, Direction.NORTH ) + .with( STATE, ComputerState.OFF ) ); + } + + @Nullable + @Override + public BlockState getPlacementState( ItemPlacementContext placement ) + { + return getDefaultState().with( FACING, + placement.getPlayerFacing() + .getOpposite() ); + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( FACING, STATE ); + } + + @Nonnull + @Override + protected ItemStack getItem( TileComputerBase tile ) + { + return tile instanceof TileComputer ? ComputerItemFactory.create( (TileComputer) tile ) : ItemStack.EMPTY; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/blocks/BlockComputerBase.java b/remappedSrc/dan200/computercraft/shared/computer/blocks/BlockComputerBase.java new file mode 100644 index 000000000..4f98a1e54 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/blocks/BlockComputerBase.java @@ -0,0 +1,214 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.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 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, BlockEntityType 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 getWeakRedstonePower( @Nonnull BlockState state, @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction incomingSide ) + { + return getStrongRedstonePower( state, world, pos, incomingSide ); + } + + @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 ); + } + + public ComputerFamily getFamily() + { + return family; + } + + @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 ); + } + + @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 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 ); + } + } + } + + @Nonnull + @Override + public ItemStack getPickStack( BlockView world, BlockPos pos, BlockState state ) + { + BlockEntity tile = world.getBlockEntity( pos ); + if( tile instanceof TileComputerBase ) + { + ItemStack result = getItem( (TileComputerBase) tile ); + if( !result.isEmpty() ) + { + return result; + } + } + + return super.getPickStack( world, pos, state ); + } + + @Nonnull + protected abstract ItemStack getItem( TileComputerBase tile ); + + @Override + public void onBreak( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState state, @Nonnull PlayerEntity player ) + { + // Call super as it is what provides sound and block break particles. Does not do anything else. + super.onBreak( world, pos, state, 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() ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java b/remappedSrc/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java new file mode 100644 index 000000000..23b1cd9d7 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java @@ -0,0 +1,117 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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; + } + + @Nonnull + @Override + public Object getTarget() + { + return computer.getTile(); + } + + @Override + public boolean equals( IPeripheral other ) + { + return other instanceof ComputerPeripheral && computer == ((ComputerPeripheral) other).computer; + } + + /** + * 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(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/blocks/ComputerProxy.java b/remappedSrc/dan200/computercraft/shared/computer/blocks/ComputerProxy.java new file mode 100644 index 000000000..2eee9af0d --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/blocks/ComputerProxy.java @@ -0,0 +1,92 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 class ComputerProxy +{ + private final Supplier get; + + public ComputerProxy( Supplier get ) + { + this.get = get; + } + + public void turnOn() + { + TileComputerBase tile = getTile(); + ServerComputer computer = tile.getServerComputer(); + if( computer == null ) + { + tile.startOn = true; + } + else + { + computer.turnOn(); + } + } + + protected TileComputerBase getTile() + { + return get.get(); + } + + public void shutdown() + { + TileComputerBase tile = getTile(); + ServerComputer computer = tile.getServerComputer(); + if( computer == null ) + { + tile.startOn = false; + } + else + { + computer.shutdown(); + } + } + + public void reboot() + { + TileComputerBase tile = getTile(); + ServerComputer computer = tile.getServerComputer(); + if( computer == null ) + { + tile.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/remappedSrc/dan200/computercraft/shared/computer/blocks/IComputerTile.java b/remappedSrc/dan200/computercraft/shared/computer/blocks/IComputerTile.java new file mode 100644 index 000000000..ba3e4fc0d --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/blocks/IComputerTile.java @@ -0,0 +1,22 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java b/remappedSrc/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java new file mode 100644 index 000000000..60dd39182 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java @@ -0,0 +1,147 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 +{ + 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; + } + + 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 ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/blocks/TileComputer.java b/remappedSrc/dan200/computercraft/shared/computer/blocks/TileComputer.java new file mode 100644 index 000000000..078a9925a --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/blocks/TileComputer.java @@ -0,0 +1,109 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.blocks; + +import dan200.computercraft.ComputerCraft; +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 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 javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class TileComputer extends TileComputerBase +{ + private ComputerProxy proxy; + + public TileComputer( ComputerFamily family, BlockEntityType type ) + { + super( type, family ); + } + + public boolean isUsableByPlayer( PlayerEntity player ) + { + return isUsable( player, false ); + } + + @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 + public Direction getDirection() + { + return getCachedState().get( BlockComputer.FACING ); + } + + @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; + } + + @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; + } + + @Override + public ComputerProxy createProxy() + { + if( proxy == null ) + { + proxy = new ComputerProxy( () -> this ) + { + @Override + protected TileComputerBase getTile() + { + return TileComputer.this; + } + }; + } + return proxy; + } + + @Nullable + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory inventory, @Nonnull PlayerEntity player ) + { + return new ContainerComputer( id, this ); + } + +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/blocks/TileComputerBase.java b/remappedSrc/dan200/computercraft/shared/computer/blocks/TileComputerBase.java new file mode 100644 index 000000000..b8ddfbc60 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/blocks/TileComputerBase.java @@ -0,0 +1,518 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.blocks; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IPeripheralTile; +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.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory; +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.NbtCompound; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.server.network.ServerPlayerEntity; +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, IPeripheralTile, Nameable, + ExtendedScreenHandlerFactory +{ + private static final String NBT_ID = "ComputerId"; + private static final String NBT_LABEL = "Label"; + private static final String NBT_ON = "On"; + private final ComputerFamily family; + protected String label = null; + boolean startOn = false; + private int instanceID = -1; + private int computerID = -1; + private boolean on = false; + private boolean fresh = false; + + public TileComputerBase( BlockEntityType type, ComputerFamily family ) + { + super( type ); + this.family = family; + } + + @Override + public void destroy() + { + unload(); + for( Direction dir : DirectionUtil.FACINGS ) + { + RedstoneUtil.propagateRedstoneOutput( getWorld(), getPos(), dir ); + } + } + + @Override + public void onChunkUnloaded() + { + unload(); + } + + protected void unload() + { + if( instanceID >= 0 ) + { + if( !getWorld().isClient ) + { + ComputerCraft.serverComputerRegistry.remove( instanceID ); + } + instanceID = -1; + } + } + + @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(); + createServerComputer().sendTerminalState( player ); + new ComputerContainerData( createServerComputer() ).open( player, this ); + } + return ActionResult.SUCCESS; + } + return ActionResult.PASS; + } + + protected boolean canNameWithTag( PlayerEntity player ) + { + return false; + } + + public ServerComputer createServerComputer() + { + if( getWorld().isClient ) + { + return null; + } + + boolean changed = false; + if( instanceID < 0 ) + { + instanceID = ComputerCraft.serverComputerRegistry.getUnusedInstanceID(); + changed = true; + } + if( !ComputerCraft.serverComputerRegistry.contains( instanceID ) ) + { + ServerComputer computer = createComputer( instanceID, computerID ); + ComputerCraft.serverComputerRegistry.add( instanceID, computer ); + fresh = true; + changed = true; + } + if( changed ) + { + updateBlock(); + updateInput(); + } + return ComputerCraft.serverComputerRegistry.get( instanceID ); + } + + public ServerComputer getServerComputer() + { + return getWorld().isClient ? null : ComputerCraft.serverComputerRegistry.get( instanceID ); + } + + protected abstract ServerComputer createComputer( int instanceID, int id ); + + 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 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 ); + computer.setPeripheral( localDir, peripheral ); + } + } + + protected ComputerSide remapToLocalSide( Direction globalSide ) + { + return remapLocalSide( DirectionUtil.toLocal( getDirection(), globalSide ) ); + } + + /** + * 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 + */ + 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; + } + + protected boolean isPeripheralBlockedOnSide( ComputerSide localSide ) + { + return false; + } + + protected ComputerSide remapLocalSide( ComputerSide localSide ) + { + return localSide; + } + + protected abstract Direction getDirection(); + + @Override + public void onNeighbourChange( @Nonnull BlockPos neighbour ) + { + updateInput( neighbour ); + } + + @Override + public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour ) + { + updateInput( neighbour ); + } + + @Override + protected void readDescription( @Nonnull NbtCompound nbt ) + { + super.readDescription( nbt ); + label = nbt.contains( NBT_LABEL ) ? nbt.getString( NBT_LABEL ) : null; + computerID = nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1; + } + + @Override + protected void writeDescription( @Nonnull NbtCompound nbt ) + { + super.writeDescription( nbt ); + if( label != null ) + { + nbt.putString( NBT_LABEL, label ); + } + if( computerID >= 0 ) + { + nbt.putInt( NBT_ID, computerID ); + } + } + + @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( startOn || (fresh && on) ) + { + computer.turnOn(); + startOn = false; + } + + computer.keepAlive(); + + fresh = false; + computerID = computer.getID(); + label = computer.getLabel(); + 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(); + } + } + } + + public void updateOutput() + { + // Update redstone + updateBlock(); + for( Direction dir : DirectionUtil.FACINGS ) + { + RedstoneUtil.propagateRedstoneOutput( getWorld(), getPos(), dir ); + } + } + + protected abstract void updateBlockState( ComputerState newState ); + + @Override + public void readNbt( @Nonnull BlockState state, @Nonnull NbtCompound nbt ) + { + super.readNbt( state, nbt ); + + // Load ID, label and power state + computerID = nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1; + label = nbt.contains( NBT_LABEL ) ? nbt.getString( NBT_LABEL ) : null; + on = startOn = nbt.getBoolean( NBT_ON ); + } + + @Nonnull + @Override + public NbtCompound writeNbt( @Nonnull NbtCompound nbt ) + { + // Save ID, label and power state + if( computerID >= 0 ) + { + nbt.putInt( NBT_ID, computerID ); + } + if( label != null ) + { + nbt.putString( NBT_LABEL, label ); + } + nbt.putBoolean( NBT_ON, on ); + + return super.writeNbt( nbt ); + } + + @Override + public void markRemoved() + { + unload(); + super.markRemoved(); + } + + 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 ) ); + } + + @Override + public final int getComputerID() + { + return computerID; + } + + @Override + public final void setComputerID( int id ) + { + if( getWorld().isClient || computerID == id ) + { + return; + } + + computerID = id; + ServerComputer computer = getServerComputer(); + if( computer != null ) + { + computer.setID( computerID ); + } + markDirty(); + } + + @Override + public final String getLabel() + { + return label; + } + + // Networking stuff + + @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; + } + + protected void transferStateFrom( TileComputerBase copy ) + { + if( copy.computerID != computerID || copy.instanceID != instanceID ) + { + unload(); + instanceID = copy.instanceID; + computerID = copy.computerID; + label = copy.label; + on = copy.on; + startOn = copy.startOn; + updateBlock(); + } + copy.instanceID = -1; + } + + @Nonnull + @Override + public IPeripheral getPeripheral( Direction side ) + { + return new ComputerPeripheral( "computer", createProxy() ); + } + + public abstract ComputerProxy createProxy(); + + @Nonnull + @Override + public Text getName() + { + return hasCustomName() ? new LiteralText( label ) : new TranslatableText( getCachedState().getBlock() + .getTranslationKey() ); + } + + @Override + public boolean hasCustomName() + { + return !Strings.isNullOrEmpty( label ); + } + + @Nonnull + @Override + public Text getDisplayName() + { + return Nameable.super.getDisplayName(); + } + + @Nullable + @Override + public Text getCustomName() + { + return hasCustomName() ? new LiteralText( label ) : null; + } + + @Override + public void writeScreenOpeningData( ServerPlayerEntity serverPlayerEntity, PacketByteBuf packetByteBuf ) + { + packetByteBuf.writeInt( getServerComputer().getInstanceID() ); + packetByteBuf.writeEnumConstant( getServerComputer().getFamily() ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/core/ClientComputer.java b/remappedSrc/dan200/computercraft/shared/computer/core/ClientComputer.java new file mode 100644 index 000000000..615b65eef --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/core/ClientComputer.java @@ -0,0 +1,132 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.core; + +import dan200.computercraft.shared.common.ClientTerminal; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.server.*; +import net.minecraft.nbt.NbtCompound; + +public class ClientComputer extends ClientTerminal implements IComputer +{ + private final int instanceID; + + private boolean on = false; + private boolean blinking = false; + private NbtCompound userData = null; + + + public ClientComputer( int instanceID ) + { + super( false ); + this.instanceID = instanceID; + } + + public NbtCompound getUserData() + { + return userData; + } + + public void requestState() + { + // Request state from server + NetworkHandler.sendToServer( new RequestComputerMessage( getInstanceID() ) ); + } + + // IComputer + + @Override + public int getInstanceID() + { + return instanceID; + } + + @Override + public void turnOn() + { + // Send turnOn to server + NetworkHandler.sendToServer( new ComputerActionServerMessage( instanceID, ComputerActionServerMessage.Action.TURN_ON ) ); + } + + @Override + public void shutdown() + { + // Send shutdown to server + NetworkHandler.sendToServer( new ComputerActionServerMessage( instanceID, ComputerActionServerMessage.Action.SHUTDOWN ) ); + } + + @Override + public void reboot() + { + // Send reboot to server + NetworkHandler.sendToServer( new ComputerActionServerMessage( instanceID, ComputerActionServerMessage.Action.REBOOT ) ); + } + + @Override + public void queueEvent( String event, Object[] arguments ) + { + // Send event to server + NetworkHandler.sendToServer( new QueueEventServerMessage( instanceID, event, arguments ) ); + } + + @Override + public boolean isOn() + { + return on; + } + + @Override + public boolean isCursorDisplayed() + { + return on && blinking; + } + + @Override + public void keyDown( int key, boolean repeat ) + { + NetworkHandler.sendToServer( new KeyEventServerMessage( instanceID, + repeat ? KeyEventServerMessage.TYPE_REPEAT : KeyEventServerMessage.TYPE_DOWN, + key ) ); + } + + @Override + public void keyUp( int key ) + { + NetworkHandler.sendToServer( new KeyEventServerMessage( instanceID, KeyEventServerMessage.TYPE_UP, key ) ); + } + + @Override + public void mouseClick( int button, int x, int y ) + { + NetworkHandler.sendToServer( new MouseEventServerMessage( instanceID, MouseEventServerMessage.TYPE_CLICK, button, x, y ) ); + } + + @Override + public void mouseUp( int button, int x, int y ) + { + NetworkHandler.sendToServer( new MouseEventServerMessage( instanceID, MouseEventServerMessage.TYPE_UP, button, x, y ) ); + } + + @Override + public void mouseDrag( int button, int x, int y ) + { + NetworkHandler.sendToServer( new MouseEventServerMessage( instanceID, MouseEventServerMessage.TYPE_DRAG, button, x, y ) ); + } + + @Override + public void mouseScroll( int direction, int x, int y ) + { + NetworkHandler.sendToServer( new MouseEventServerMessage( instanceID, MouseEventServerMessage.TYPE_SCROLL, direction, x, y ) ); + } + + public void setState( ComputerState state, NbtCompound userData ) + { + on = state != ComputerState.OFF; + blinking = state == ComputerState.BLINKING; + this.userData = userData; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/core/ClientComputerRegistry.java b/remappedSrc/dan200/computercraft/shared/computer/core/ClientComputerRegistry.java new file mode 100644 index 000000000..725a8c7a5 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/core/ClientComputerRegistry.java @@ -0,0 +1,17 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.core; + +public class ClientComputerRegistry extends ComputerRegistry +{ + @Override + public void add( int instanceID, ClientComputer computer ) + { + super.add( instanceID, computer ); + computer.requestState(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/core/ComputerFamily.java b/remappedSrc/dan200/computercraft/shared/computer/core/ComputerFamily.java new file mode 100644 index 000000000..5964b3dbd --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/core/ComputerFamily.java @@ -0,0 +1,12 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.core; + +public enum ComputerFamily +{ + NORMAL, ADVANCED, COMMAND +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/core/ComputerRegistry.java b/remappedSrc/dan200/computercraft/shared/computer/core/ComputerRegistry.java new file mode 100644 index 000000000..29451df8f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/core/ComputerRegistry.java @@ -0,0 +1,79 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.core; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class ComputerRegistry +{ + private final Map computers; + private int nextUnusedInstanceID; + private int sessionID; + + protected ComputerRegistry() + { + computers = new HashMap<>(); + reset(); + } + + public void reset() + { + computers.clear(); + nextUnusedInstanceID = 0; + sessionID = new Random().nextInt(); + } + + public int getSessionID() + { + return sessionID; + } + + public int getUnusedInstanceID() + { + return nextUnusedInstanceID++; + } + + public Collection getComputers() + { + return computers.values(); + } + + public T get( int instanceID ) + { + if( instanceID >= 0 ) + { + if( computers.containsKey( instanceID ) ) + { + return computers.get( instanceID ); + } + } + return null; + } + + public boolean contains( int instanceID ) + { + return computers.containsKey( instanceID ); + } + + public void add( int instanceID, T computer ) + { + if( computers.containsKey( instanceID ) ) + { + remove( instanceID ); + } + computers.put( instanceID, computer ); + nextUnusedInstanceID = Math.max( nextUnusedInstanceID, instanceID + 1 ); + } + + public void remove( int instanceID ) + { + computers.remove( instanceID ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/core/ComputerState.java b/remappedSrc/dan200/computercraft/shared/computer/core/ComputerState.java new file mode 100644 index 000000000..c88169a2c --- /dev/null +++ b/remappedSrc/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-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.core; + +import net.minecraft.util.StringIdentifiable; + +import javax.annotation.Nonnull; + +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/remappedSrc/dan200/computercraft/shared/computer/core/IComputer.java b/remappedSrc/dan200/computercraft/shared/computer/core/IComputer.java new file mode 100644 index 000000000..149e3a057 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/core/IComputer.java @@ -0,0 +1,41 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.core; + +import dan200.computercraft.shared.common.ITerminal; + +public interface IComputer extends ITerminal, InputHandler +{ + int getInstanceID(); + + void turnOn(); + + void shutdown(); + + void reboot(); + + default void queueEvent( String event ) + { + queueEvent( event, null ); + } + + @Override + void queueEvent( String event, Object[] arguments ); + + default ComputerState getState() + { + if( !isOn() ) + { + return ComputerState.OFF; + } + return isCursorDisplayed() ? ComputerState.BLINKING : ComputerState.ON; + } + + boolean isOn(); + + boolean isCursorDisplayed(); +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/core/IContainerComputer.java b/remappedSrc/dan200/computercraft/shared/computer/core/IContainerComputer.java new file mode 100644 index 000000000..b25eae0a0 --- /dev/null +++ b/remappedSrc/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-2021. 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/remappedSrc/dan200/computercraft/shared/computer/core/InputHandler.java b/remappedSrc/dan200/computercraft/shared/computer/core/InputHandler.java new file mode 100644 index 000000000..c7b630720 --- /dev/null +++ b/remappedSrc/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-2021. 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/remappedSrc/dan200/computercraft/shared/computer/core/InputState.java b/remappedSrc/dan200/computercraft/shared/computer/core/InputState.java new file mode 100644 index 000000000..111faee60 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/core/InputState.java @@ -0,0 +1,137 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/computer/core/ServerComputer.java b/remappedSrc/dan200/computercraft/shared/computer/core/ServerComputer.java new file mode 100644 index 000000000..8204424c9 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -0,0 +1,407 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 me.shedaniel.cloth.api.utils.v1.GameInstanceUtils; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.InputStream; + +public class ServerComputer extends ServerTerminal implements IComputer, IComputerEnvironment +{ + private final int instanceID; + private final ComputerFamily family; + private final Computer computer; + private World world; + private BlockPos position; + private NbtCompound userData; + private boolean changed; + + private boolean changedLastFrame; + private int ticksSincePing; + + public ServerComputer( World world, int computerID, String label, int instanceID, ComputerFamily family, int terminalWidth, int terminalHeight ) + { + super( family != ComputerFamily.NORMAL, terminalWidth, terminalHeight ); + this.instanceID = instanceID; + + this.world = world; + position = null; + + this.family = family; + computer = new Computer( this, getTerminal(), computerID ); + computer.setLabel( label ); + userData = null; + changed = false; + + changedLastFrame = false; + ticksSincePing = 0; + } + + public ComputerFamily getFamily() + { + return family; + } + + public World getWorld() + { + return world; + } + + public void setWorld( World world ) + { + this.world = world; + } + + public BlockPos getPosition() + { + return position; + } + + public void setPosition( BlockPos pos ) + { + position = new BlockPos( pos ); + } + + public IAPIEnvironment getAPIEnvironment() + { + return computer.getAPIEnvironment(); + } + + public Computer getComputer() + { + return computer; + } + + @Override + public void update() + { + super.update(); + computer.tick(); + + changedLastFrame = computer.pollAndResetChanged() || changed; + changed = false; + + ticksSincePing++; + } + + public void keepAlive() + { + ticksSincePing = 0; + } + + public boolean hasTimedOut() + { + return ticksSincePing > 100; + } + + public void unload() + { + computer.unload(); + } + + public NbtCompound getUserData() + { + if( userData == null ) + { + userData = new NbtCompound(); + } + return userData; + } + + public void updateUserData() + { + changed = true; + } + + public void broadcastState( boolean force ) + { + if( hasOutputChanged() || force ) + { + // Send computer state to all clients + MinecraftServer server = GameInstanceUtils.getServer(); + if( server != null ) + { + NetworkHandler.sendToAllPlayers( server, createComputerPacket() ); + } + } + + if( hasTerminalChanged() || force ) + { + MinecraftServer server = GameInstanceUtils.getServer(); + if( server != null ) + { + // Send terminal state to clients who are currently interacting with the computer. + + NetworkMessage packet = null; + for( PlayerEntity player : server.getPlayerManager() + .getPlayerList() ) + { + if( isInteracting( player ) ) + { + if( packet == null ) + { + packet = createTerminalPacket(); + } + NetworkHandler.sendToPlayer( player, packet ); + } + } + } + } + } + + public boolean hasOutputChanged() + { + return changedLastFrame; + } + + private NetworkMessage createComputerPacket() + { + return new ComputerDataClientMessage( this ); + } + + protected boolean isInteracting( PlayerEntity player ) + { + return getContainer( player ) != null; + } + + protected NetworkMessage createTerminalPacket() + { + return new ComputerTerminalClientMessage( getInstanceID(), write() ); + } + + @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; + } + + @Override + public int getInstanceID() + { + return instanceID; + } + + @Override + public void turnOn() + { + // Turn on + computer.turnOn(); + } + + // IComputer + + @Override + public void shutdown() + { + // Shutdown + computer.shutdown(); + } + + @Override + public void reboot() + { + // Reboot + computer.reboot(); + } + + @Override + public void queueEvent( String event, Object[] arguments ) + { + // Queue event + computer.queueEvent( event, arguments ); + } + + @Override + public boolean isOn() + { + return computer.isOn(); + } + + @Override + public boolean isCursorDisplayed() + { + return computer.isOn() && computer.isBlinking(); + } + + 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 + MinecraftServer server = GameInstanceUtils.getServer(); + if( server != null ) + { + NetworkHandler.sendToAllPlayers( server, new ComputerDeletedClientMessage( getInstanceID() ) ); + } + } + + public int getID() + { + return computer.getID(); + } + + public void setID( int id ) + { + computer.setID( id ); + } + + public String getLabel() + { + return computer.getLabel(); + } + + public void setLabel( String label ) + { + computer.setLabel( label ); + } + + public int getRedstoneOutput( ComputerSide side ) + { + return computer.getEnvironment() + .getExternalRedstoneOutput( side ); + } + + public void setRedstoneInput( ComputerSide side, int level ) + { + computer.getEnvironment() + .setRedstoneInput( side, level ); + } + + public int getBundledRedstoneOutput( ComputerSide side ) + { + return computer.getEnvironment() + .getExternalBundledRedstoneOutput( side ); + } + + public void setBundledRedstoneInput( ComputerSide side, int combination ) + { + computer.getEnvironment() + .setBundledRedstoneInput( side, combination ); + } + + public void addAPI( ILuaAPI api ) + { + computer.addApi( api ); + } + + // IComputerEnvironment implementation + + public void setPeripheral( ComputerSide side, IPeripheral peripheral ) + { + computer.getEnvironment() + .setPeripheral( side, peripheral ); + } + + public IPeripheral getPeripheral( ComputerSide side ) + { + return computer.getEnvironment() + .getPeripheral( side ); + } + + @Override + public int getDay() + { + return (int) ((world.getTimeOfDay() + 6000) / 24000) + 1; + } + + @Override + public double getTimeOfDay() + { + return (world.getTimeOfDay() + 6000) % 24000 / 1000.0; + } + + @Override + public long getComputerSpaceLimit() + { + return ComputerCraft.computerSpaceLimit; + } + + @Nonnull + @Override + public String getHostString() + { + return String.format( "ComputerCraft %s (Minecraft %s)", ComputerCraftAPI.getInstalledVersion(), "1.16.4" ); + } + + @Nonnull + @Override + public String getUserAgent() + { + return ComputerCraft.MOD_ID + "/" + ComputerCraftAPI.getInstalledVersion(); + } + + @Override + public int assignNewID() + { + return ComputerCraftAPI.createUniqueNumberedSaveDir( world, "computer" ); + } + + @Override + public IWritableMount createSaveDirMount( String subPath, long capacity ) + { + return ComputerCraftAPI.createSaveDirMount( 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 ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java b/remappedSrc/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java new file mode 100644 index 000000000..448a43571 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java @@ -0,0 +1,89 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 reset() + { + //System.out.println( "RESET SERVER COMPUTERS" ); + for( ServerComputer computer : getComputers() ) + { + computer.unload(); + } + super.reset(); + //System.out.println( getComputers().size() + " SERVER COMPUTERS" ); + } + + @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" ); + } + + 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/remappedSrc/dan200/computercraft/shared/computer/inventory/ContainerComputer.java b/remappedSrc/dan200/computercraft/shared/computer/inventory/ContainerComputer.java new file mode 100644 index 000000000..f4202bbe1 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/inventory/ContainerComputer.java @@ -0,0 +1,25 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.inventory; + +import dan200.computercraft.shared.ComputerCraftRegistry; +import dan200.computercraft.shared.computer.blocks.TileComputer; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.network.PacketByteBuf; + +public class ContainerComputer extends ContainerComputerBase +{ + public ContainerComputer( int id, TileComputer tile ) + { + super( ComputerCraftRegistry.ModContainers.COMPUTER, id, tile::isUsableByPlayer, tile.createServerComputer(), tile.getFamily() ); + } + + public ContainerComputer( int i, PlayerInventory playerInventory, PacketByteBuf packetByteBuf ) + { + super( ComputerCraftRegistry.ModContainers.COMPUTER, i, playerInventory, packetByteBuf ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java b/remappedSrc/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java new file mode 100644 index 000000000..a696c8618 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java @@ -0,0 +1,96 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.network.PacketByteBuf; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; +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, PlayerInventory player, PacketByteBuf packetByteBuf ) + { + this( type, + id, + x -> true, + getComputer( player, new ComputerContainerData( new PacketByteBuf( packetByteBuf.copy() ) ) ), + new ComputerContainerData( new PacketByteBuf( packetByteBuf.copy() ) ).getFamily() ); + } + + protected ContainerComputerBase( ScreenHandlerType type, int id, Predicate canUse, IComputer computer, + ComputerFamily family ) + { + super( type, id ); + this.canUse = canUse; + this.computer = Objects.requireNonNull( computer ); + this.family = family; + } + + 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; + } + + @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(); + } + + @Override + public boolean canUse( @Nonnull PlayerEntity player ) + { + return canUse.test( player ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java b/remappedSrc/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java new file mode 100644 index 000000000..c2fa650c6 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java @@ -0,0 +1,61 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.inventory; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.ComputerCraftRegistry; +import dan200.computercraft.shared.computer.blocks.TileCommandComputer; +import dan200.computercraft.shared.computer.core.ComputerFamily; +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 net.minecraft.network.PacketByteBuf; + +import javax.annotation.Nonnull; + +public class ContainerViewComputer extends ContainerComputerBase +{ + private final int width; + private final int height; + + public ContainerViewComputer( int id, ServerComputer computer ) + { + super( ComputerCraftRegistry.ModContainers.VIEW_COMPUTER, id, player -> canInteractWith( computer, player ), computer, computer.getFamily() ); + width = height = 0; + } + + public ContainerViewComputer( int id, PlayerInventory player, PacketByteBuf packetByteBuf ) + { + super( ComputerCraftRegistry.ModContainers.VIEW_COMPUTER, id, player, packetByteBuf ); + ViewComputerContainerData data = new ViewComputerContainerData( new PacketByteBuf( packetByteBuf.copy() ) ); + width = data.getWidth(); + 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 + return computer.getFamily() != ComputerFamily.COMMAND || TileCommandComputer.isUsable( player ); + } + + public int getWidth() + { + return width; + } + + public int getHeight() + { + return height; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/items/ComputerItemFactory.java b/remappedSrc/dan200/computercraft/shared/computer/items/ComputerItemFactory.java new file mode 100644 index 000000000..b267240c5 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/items/ComputerItemFactory.java @@ -0,0 +1,41 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.items; + +import dan200.computercraft.shared.ComputerCraftRegistry; +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 ComputerCraftRegistry.ModItems.COMPUTER_NORMAL.create( id, label ); + case ADVANCED: + return ComputerCraftRegistry.ModItems.COMPUTER_ADVANCED.create( id, label ); + case COMMAND: + return ComputerCraftRegistry.ModItems.COMPUTER_COMMAND.create( id, label ); + default: + return ItemStack.EMPTY; + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/items/IComputerItem.java b/remappedSrc/dan200/computercraft/shared/computer/items/IComputerItem.java new file mode 100644 index 000000000..326b2adac --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/items/IComputerItem.java @@ -0,0 +1,33 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.items; + +import dan200.computercraft.shared.computer.core.ComputerFamily; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import javax.annotation.Nonnull; + +public interface IComputerItem +{ + String NBT_ID = "ComputerId"; + + default int getComputerID( @Nonnull ItemStack stack ) + { + NbtCompound nbt = stack.getNbt(); + 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/remappedSrc/dan200/computercraft/shared/computer/items/ItemComputer.java b/remappedSrc/dan200/computercraft/shared/computer/items/ItemComputer.java new file mode 100644 index 000000000..3fc0370e8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/items/ItemComputer.java @@ -0,0 +1,48 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.getOrCreateNbt() + .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/remappedSrc/dan200/computercraft/shared/computer/items/ItemComputerBase.java b/remappedSrc/dan200/computercraft/shared/computer/items/ItemComputerBase.java new file mode 100644 index 000000000..482e2916b --- /dev/null +++ b/remappedSrc/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-2021. 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/remappedSrc/dan200/computercraft/shared/computer/recipe/ComputerConvertRecipe.java b/remappedSrc/dan200/computercraft/shared/computer/recipe/ComputerConvertRecipe.java new file mode 100644 index 000000000..8b4cfaa58 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/recipe/ComputerConvertRecipe.java @@ -0,0 +1,79 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 + @Override + public String getGroup() + { + return group; + } + + @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 + protected abstract ItemStack convert( @Nonnull IComputerItem item, @Nonnull ItemStack stack ); +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/recipe/ComputerFamilyRecipe.java b/remappedSrc/dan200/computercraft/shared/computer/recipe/ComputerFamilyRecipe.java new file mode 100644 index 000000000..9971dd31b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/recipe/ComputerFamilyRecipe.java @@ -0,0 +1,89 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.computer.recipe; + +import com.google.gson.JsonObject; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.util.RecipeUtil; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.RecipeSerializer; +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 implements RecipeSerializer + { + @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 = getItem( JsonHelper.getObject( json, "result" ) ); + + return create( identifier, group, template.width, template.height, template.ingredients, result, family ); + } + + 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 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.getIngredients() ) + { + ingredient.write( buf ); + } + buf.writeItemStack( recipe.getOutput() ); + buf.writeEnumConstant( recipe.getFamily() ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/computer/recipe/ComputerUpgradeRecipe.java b/remappedSrc/dan200/computercraft/shared/computer/recipe/ComputerUpgradeRecipe.java new file mode 100644 index 000000000..b22c10b14 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/computer/recipe/ComputerUpgradeRecipe.java @@ -0,0 +1,51 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 static final RecipeSerializer SERIALIZER = + new 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 ); + } + }; + + 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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/data/BlockNamedEntityLootCondition.java b/remappedSrc/dan200/computercraft/shared/data/BlockNamedEntityLootCondition.java new file mode 100644 index 000000000..ee98295f1 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/data/BlockNamedEntityLootCondition.java @@ -0,0 +1,54 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.data; + +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 javax.annotation.Nonnull; +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/remappedSrc/dan200/computercraft/shared/data/ConstantLootConditionSerializer.java b/remappedSrc/dan200/computercraft/shared/data/ConstantLootConditionSerializer.java new file mode 100644 index 000000000..8a45e575b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/data/ConstantLootConditionSerializer.java @@ -0,0 +1,43 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.data; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import net.minecraft.loot.condition.LootCondition; +import net.minecraft.loot.condition.LootConditionType; +import net.minecraft.util.JsonSerializer; + +import javax.annotation.Nonnull; + +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 toJson( @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/remappedSrc/dan200/computercraft/shared/data/HasComputerIdLootCondition.java b/remappedSrc/dan200/computercraft/shared/data/HasComputerIdLootCondition.java new file mode 100644 index 000000000..7b2717d29 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/data/HasComputerIdLootCondition.java @@ -0,0 +1,54 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.data; + +import dan200.computercraft.shared.computer.blocks.IComputerTile; +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 javax.annotation.Nonnull; +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/remappedSrc/dan200/computercraft/shared/data/PlayerCreativeLootCondition.java b/remappedSrc/dan200/computercraft/shared/data/PlayerCreativeLootCondition.java new file mode 100644 index 000000000..e52690853 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/data/PlayerCreativeLootCondition.java @@ -0,0 +1,54 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/integration/ModMenuIntegration.java b/remappedSrc/dan200/computercraft/shared/integration/ModMenuIntegration.java new file mode 100644 index 000000000..f973cf5a5 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/integration/ModMenuIntegration.java @@ -0,0 +1,52 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.integration; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; +import dan200.computercraft.shared.util.Config; +import me.shedaniel.clothconfig2.api.ConfigBuilder; +import me.shedaniel.clothconfig2.api.ConfigCategory; +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; + +// A poor mod menu integration just for testing the monitor rendering changes we've been making :) + +@Environment( EnvType.CLIENT ) +public class ModMenuIntegration implements ModMenuApi +{ + @Override + public ConfigScreenFactory getModConfigScreenFactory() + { + return parent -> { + ConfigBuilder builder = ConfigBuilder.create().setParentScreen( parent ) + .setTitle( new LiteralText( "Computer Craft" ) ) + .setSavingRunnable( () -> { + Config.clientSpec.correct( Config.clientConfig ); + Config.sync(); + Config.save(); + ComputerCraft.log.info( "Monitor renderer: {}", ComputerCraft.monitorRenderer ); + } ); + + ConfigCategory client = builder.getOrCreateCategory( new LiteralText( "Client" ) ); + + ConfigEntryBuilder entryBuilder = builder.entryBuilder(); + + client.addEntry( entryBuilder.startEnumSelector( new LiteralText( "Monitor Renderer" ), MonitorRenderer.class, ComputerCraft.monitorRenderer ) + .setDefaultValue( MonitorRenderer.BEST ) + .setSaveConsumer( renderer -> { Config.clientConfig.set( "monitor_renderer", renderer ); } ) + .setTooltip( Text.of( Config.clientConfig.getComment( "monitor_renderer" ) ) ) + .build() ); + + return builder.build(); + }; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/media/items/ItemDisk.java b/remappedSrc/dan200/computercraft/shared/media/items/ItemDisk.java new file mode 100644 index 000000000..3d656c87a --- /dev/null +++ b/remappedSrc/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-2021. 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.ComputerCraftRegistry; +import dan200.computercraft.shared.common.IColouredItem; +import dan200.computercraft.shared.util.Colour; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +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.world.World; + +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 ); + } + + @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 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() ) ); + } + } + + @Nonnull + public static ItemStack createFromIDAndColour( int id, String label, int colour ) + { + ItemStack stack = new ItemStack( ComputerCraftRegistry.ModItems.DISK ); + setDiskID( stack, id ); + ComputerCraftRegistry.ModItems.DISK.setLabel( stack, label ); + IColouredItem.setColourBasic( stack, colour ); + return stack; + } + + private static void setDiskID( @Nonnull ItemStack stack, int id ) + { + if( id >= 0 ) + { + stack.getOrCreateNbt() + .putInt( NBT_ID, id ); + } + } + + public static int getDiskID( @Nonnull ItemStack stack ) + { + NbtCompound nbt = stack.getNbt(); + return nbt != null && nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1; + } + + @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 ); + } + + @Override + public int getColour( @Nonnull ItemStack stack ) + { + int colour = IColouredItem.getColourBasic( stack ); + return colour == -1 ? Colour.WHITE.getHex() : colour; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/media/items/ItemPrintout.java b/remappedSrc/dan200/computercraft/shared/media/items/ItemPrintout.java new file mode 100644 index 000000000..07ecffb99 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/media/items/ItemPrintout.java @@ -0,0 +1,168 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.media.items; + +import dan200.computercraft.shared.ComputerCraftRegistry; +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.NbtCompound; +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 +{ + public static final int LINES_PER_PAGE = 21; + public static final int LINE_MAX_LENGTH = 25; + public static final int MAX_PAGES = 16; + 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"; + private final Type type; + + public ItemPrintout( Settings settings, Type type ) + { + super( settings ); + this.type = type; + } + + @Nonnull + public static ItemStack createSingleFromTitleAndText( String title, String[] text, String[] colours ) + { + return ComputerCraftRegistry.ModItems.PRINTED_PAGE.createFromTitleAndText( title, text, colours ); + } + + @Nonnull + private ItemStack createFromTitleAndText( String title, String[] text, String[] colours ) + { + ItemStack stack = new ItemStack( this ); + + // Build NBT + if( title != null ) + { + stack.getOrCreateNbt() + .putString( NBT_TITLE, title ); + } + if( text != null ) + { + NbtCompound tag = stack.getOrCreateNbt(); + 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 ) + { + NbtCompound tag = stack.getOrCreateNbt(); + 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 createMultipleFromTitleAndText( String title, String[] text, String[] colours ) + { + return ComputerCraftRegistry.ModItems.PRINTED_PAGES.createFromTitleAndText( title, text, colours ); + } + + @Nonnull + public static ItemStack createBookFromTitleAndText( String title, String[] text, String[] colours ) + { + return ComputerCraftRegistry.ModItems.PRINTED_BOOK.createFromTitleAndText( title, text, colours ); + } + + public static String[] getText( @Nonnull ItemStack stack ) + { + return getLines( stack, NBT_LINE_TEXT ); + } + + private static String[] getLines( @Nonnull ItemStack stack, String prefix ) + { + NbtCompound nbt = stack.getNbt(); + 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; + } + + public static int getPageCount( @Nonnull ItemStack stack ) + { + NbtCompound nbt = stack.getNbt(); + return nbt != null && nbt.contains( NBT_PAGES ) ? nbt.getInt( NBT_PAGES ) : 1; + } + + public static String[] getColours( @Nonnull ItemStack stack ) + { + return getLines( stack, NBT_LINE_COLOUR ); + } + + @Nonnull + @Override + public TypedActionResult use( World world, @Nonnull PlayerEntity player, @Nonnull Hand hand ) + { + if( !world.isClient ) + { + new HeldItemContainerData( hand ).open( player, + new ContainerHeldItem.Factory( ComputerCraftRegistry.ModContainers.PRINTOUT, + player.getStackInHand( hand ), + hand ) ); + } + return new TypedActionResult<>( ActionResult.SUCCESS, player.getStackInHand( hand ) ); + } + + @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 ) ); + } + } + + public static String getTitle( @Nonnull ItemStack stack ) + { + NbtCompound nbt = stack.getNbt(); + return nbt != null && nbt.contains( NBT_TITLE ) ? nbt.getString( NBT_TITLE ) : null; + } + + public Type getType() + { + return type; + } + + public enum Type + { + PAGE, PAGES, BOOK + } +} diff --git a/remappedSrc/dan200/computercraft/shared/media/items/ItemTreasureDisk.java b/remappedSrc/dan200/computercraft/shared/media/items/ItemTreasureDisk.java new file mode 100644 index 000000000..ae4e6e990 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/media/items/ItemTreasureDisk.java @@ -0,0 +1,134 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.ComputerCraftRegistry; +import dan200.computercraft.shared.util.Colour; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; + +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 ); + } + + public static ItemStack create( String subPath, int colourIndex ) + { + ItemStack result = new ItemStack( ComputerCraftRegistry.ModItems.TREASURE_DISK ); + NbtCompound nbt = result.getOrCreateNbt(); + 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; + } + + public static int getColour( @Nonnull ItemStack stack ) + { + NbtCompound nbt = stack.getNbt(); + return nbt != null && nbt.contains( NBT_COLOUR ) ? nbt.getInt( NBT_COLOUR ) : Colour.BLUE.getHex(); + } + + @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 void appendStacks( @Nonnull ItemGroup group, @Nonnull DefaultedList stacks ) + { + } + + @Nonnull + private static String getTitle( @Nonnull ItemStack stack ) + { + NbtCompound nbt = stack.getNbt(); + return nbt != null && nbt.contains( NBT_TITLE ) ? nbt.getString( NBT_TITLE ) : "'alongtimeago' by dan200"; + } + + @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; + } + } + + private static IMount getTreasureMount() + { + return ComputerCraftAPI.createResourceMount( "computercraft", "lua/treasure" ); + } + + @Nonnull + private static String getSubPath( @Nonnull ItemStack stack ) + { + NbtCompound nbt = stack.getNbt(); + return nbt != null && nbt.contains( NBT_SUB_PATH ) ? nbt.getString( NBT_SUB_PATH ) : "dan200/alongtimeago"; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/media/items/RecordMedia.java b/remappedSrc/dan200/computercraft/shared/media/items/RecordMedia.java new file mode 100644 index 000000000..730f8584f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/media/items/RecordMedia.java @@ -0,0 +1,58 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.media.items; + +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.fabric.mixin.MusicDiscItemAccessor; +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 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; + } + return ((MusicDiscItemAccessor) item).getSound(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/media/recipes/DiskRecipe.java b/remappedSrc/dan200/computercraft/shared/media/recipes/DiskRecipe.java new file mode 100644 index 000000000..748e61f91 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/media/recipes/DiskRecipe.java @@ -0,0 +1,120 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 +{ + public static final RecipeSerializer SERIALIZER = new SpecialRecipeSerializer<>( DiskRecipe::new ); + private final Ingredient paper = Ingredient.ofItems( Items.PAPER ); + // TODO: Ingredient.fromTag( Tags.Items.DUSTS_REDSTONE ); + private final Ingredient redstone = Ingredient.ofItems( Items.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 craft( @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 ) tracker.addColour( dye ); + } + } + + 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 RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + @Nonnull + @Override + public ItemStack getOutput() + { + return ItemDisk.createFromIDAndColour( -1, null, Colour.BLUE.getHex() ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java b/remappedSrc/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java new file mode 100644 index 000000000..421af7edf --- /dev/null +++ b/remappedSrc/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-2021. 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 +{ + public static final RecipeSerializer SERIALIZER = new SpecialRecipeSerializer<>( PrintoutRecipe::new ); + private final Ingredient paper = Ingredient.ofItems( Items.PAPER ); + private final Ingredient leather = Ingredient.ofItems( Items.LEATHER ); + private final Ingredient string = Ingredient.ofItems( Items.STRING ); + + private PrintoutRecipe( Identifier id ) + { + super( id ); + } + + @Nonnull + @Override + public ItemStack getOutput() + { + return ItemPrintout.createMultipleFromTitleAndText( null, null, null ); + } + + @Override + public boolean matches( @Nonnull CraftingInventory inventory, @Nonnull World world ) + { + return !craft( inventory ).isEmpty(); + } + + @Nonnull + @Override + public ItemStack craft( @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; + } + + @Override + public boolean fits( int x, int y ) + { + return x >= 3 && y >= 3; + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/network/NetworkHandler.java b/remappedSrc/dan200/computercraft/shared/network/NetworkHandler.java new file mode 100644 index 000000000..91b2e92b0 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/NetworkHandler.java @@ -0,0 +1,152 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.network.client.*; +import dan200.computercraft.shared.network.server.*; +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.network.ClientSidePacketRegistry; +import net.fabricmc.fabric.api.network.PacketContext; +import net.fabricmc.fabric.api.network.ServerSidePacketRegistry; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket; +import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public final class NetworkHandler +{ + private static final Int2ObjectMap> packetReaders = new Int2ObjectOpenHashMap<>(); + private static final Object2IntMap> packetIds = new Object2IntOpenHashMap<>(); + + private static final Identifier ID = new Identifier( ComputerCraft.MOD_ID, "main" ); + + private NetworkHandler() + { + } + + public static void setup() + { + ServerSidePacketRegistry.INSTANCE.register( ID, NetworkHandler::receive ); + if( FabricLoader.getInstance() + .getEnvironmentType() == EnvType.CLIENT ) + { + ClientSidePacketRegistry.INSTANCE.register( ID, NetworkHandler::receive ); + } + + // Server messages + registerMainThread( 0, ComputerActionServerMessage::new ); + registerMainThread( 1, QueueEventServerMessage::new ); + registerMainThread( 2, RequestComputerMessage::new ); + registerMainThread( 3, KeyEventServerMessage::new ); + registerMainThread( 4, MouseEventServerMessage::new ); + + // Client messages + registerMainThread( 10, ChatTableClientMessage::new ); + registerMainThread( 11, ComputerDataClientMessage::new ); + registerMainThread( 12, ComputerDeletedClientMessage::new ); + registerMainThread( 13, ComputerTerminalClientMessage::new ); + registerMainThread( 14, PlayRecordClientMessage.class, PlayRecordClientMessage::new ); + } + + private static void receive( PacketContext context, PacketByteBuf buffer ) + { + int type = buffer.readByte(); + packetReaders.get( type ) + .accept( context, buffer ); + } + + /** + * /** 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 factory The factory for this type of packet. + */ + private static void registerMainThread( int id, Supplier factory ) + { + registerMainThread( id, 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 decoder The factory for this type of packet. + */ + private static void registerMainThread( int id, Class type, Function decoder ) + { + packetIds.put( type, id ); + packetReaders.put( id, ( context, buf ) -> { + T result = decoder.apply( buf ); + context.getTaskQueue() + .execute( () -> result.handle( context ) ); + } ); + } + + @SuppressWarnings( "unchecked" ) + private static Class getType( Supplier supplier ) + { + return (Class) supplier.get() + .getClass(); + } + + public static void sendToPlayer( PlayerEntity player, NetworkMessage packet ) + { + ((ServerPlayerEntity) player).networkHandler.sendPacket( new CustomPayloadS2CPacket( ID, encode( packet ) ) ); + } + + private static PacketByteBuf encode( NetworkMessage message ) + { + PacketByteBuf buf = new PacketByteBuf( Unpooled.buffer() ); + buf.writeByte( packetIds.getInt( message.getClass() ) ); + message.toBytes( buf ); + return buf; + } + + public static void sendToAllPlayers( MinecraftServer server, NetworkMessage packet ) + { + server.getPlayerManager() + .sendToAll( new CustomPayloadS2CPacket( ID, encode( packet ) ) ); + } + + @Environment( EnvType.CLIENT ) + public static void sendToServer( NetworkMessage packet ) + { + MinecraftClient.getInstance().player.networkHandler.sendPacket( new CustomPayloadC2SPacket( ID, encode( packet ) ) ); + } + + public static void sendToAllAround( NetworkMessage packet, World world, Vec3d pos, double range ) + { + world.getServer() + .getPlayerManager() + .sendToAround( null, pos.x, pos.y, pos.z, range, world.getRegistryKey(), new CustomPayloadS2CPacket( ID, encode( packet ) ) ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/network/NetworkMessage.java b/remappedSrc/dan200/computercraft/shared/network/NetworkMessage.java new file mode 100644 index 000000000..911014907 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/NetworkMessage.java @@ -0,0 +1,46 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network; + +import net.fabricmc.fabric.api.network.PacketContext; +import net.minecraft.network.PacketByteBuf; + +import javax.annotation.Nonnull; + +/** + * The base interface for any message which will be sent to the client or 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( PacketContext context ); +} diff --git a/remappedSrc/dan200/computercraft/shared/network/client/ChatTableClientMessage.java b/remappedSrc/dan200/computercraft/shared/network/client/ChatTableClientMessage.java new file mode 100644 index 000000000..0933ecdb7 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/client/ChatTableClientMessage.java @@ -0,0 +1,105 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.fabricmc.fabric.api.network.PacketContext; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.text.Text; + +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( PacketContext context ) + { + ClientTableFormatter.INSTANCE.display( table ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/network/client/ComputerClientMessage.java b/remappedSrc/dan200/computercraft/shared/network/client/ComputerClientMessage.java new file mode 100644 index 000000000..792de6845 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/client/ComputerClientMessage.java @@ -0,0 +1,58 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.client; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.ClientComputer; +import dan200.computercraft.shared.network.NetworkMessage; +import net.minecraft.network.PacketByteBuf; + +import javax.annotation.Nonnull; + +/** + * 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/remappedSrc/dan200/computercraft/shared/network/client/ComputerDataClientMessage.java b/remappedSrc/dan200/computercraft/shared/network/client/ComputerDataClientMessage.java new file mode 100644 index 000000000..568338fe1 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/client/ComputerDataClientMessage.java @@ -0,0 +1,57 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.client; + +import dan200.computercraft.shared.computer.core.ComputerState; +import dan200.computercraft.shared.computer.core.ServerComputer; +import net.fabricmc.fabric.api.network.PacketContext; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.PacketByteBuf; + +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 NbtCompound 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.writeNbt( userData ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + super.fromBytes( buf ); + state = buf.readEnumConstant( ComputerState.class ); + userData = buf.readNbt(); + } + + @Override + public void handle( PacketContext context ) + { + getComputer().setState( state, userData ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/network/client/ComputerDeletedClientMessage.java b/remappedSrc/dan200/computercraft/shared/network/client/ComputerDeletedClientMessage.java new file mode 100644 index 000000000..4883e6610 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/client/ComputerDeletedClientMessage.java @@ -0,0 +1,28 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.client; + +import dan200.computercraft.ComputerCraft; +import net.fabricmc.fabric.api.network.PacketContext; + +public class ComputerDeletedClientMessage extends ComputerClientMessage +{ + public ComputerDeletedClientMessage( int instanceId ) + { + super( instanceId ); + } + + public ComputerDeletedClientMessage() + { + } + + @Override + public void handle( PacketContext context ) + { + ComputerCraft.clientComputerRegistry.remove( getInstanceId() ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java b/remappedSrc/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java new file mode 100644 index 000000000..75ddb5250 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java @@ -0,0 +1,47 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.client; + +import net.fabricmc.fabric.api.network.PacketContext; +import net.minecraft.network.PacketByteBuf; + +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( PacketContext context ) + { + getComputer().read( state ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/network/client/MonitorClientMessage.java b/remappedSrc/dan200/computercraft/shared/network/client/MonitorClientMessage.java new file mode 100644 index 000000000..740171dab --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/client/MonitorClientMessage.java @@ -0,0 +1,60 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.network.client; + +import dan200.computercraft.shared.network.NetworkMessage; +import dan200.computercraft.shared.peripheral.monitor.TileMonitor; +import net.fabricmc.fabric.api.network.PacketContext; +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 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( PacketContext 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/remappedSrc/dan200/computercraft/shared/network/client/PlayRecordClientMessage.java b/remappedSrc/dan200/computercraft/shared/network/client/PlayRecordClientMessage.java new file mode 100644 index 000000000..3e623a892 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/client/PlayRecordClientMessage.java @@ -0,0 +1,92 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.client; + +import dan200.computercraft.fabric.mixin.SoundEventAccess; +import dan200.computercraft.shared.network.NetworkMessage; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.network.PacketContext; +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.minecraft.util.registry.Registry; + +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 = Registry.SOUND_EVENT.get( buf.readIdentifier() ); + } + 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.writeIdentifier( ((SoundEventAccess) soundEvent).getId() ); + } + } + + @Override + @Environment( EnvType.CLIENT ) + public void handle( PacketContext context ) + { + MinecraftClient mc = MinecraftClient.getInstance(); + mc.worldRenderer.playSong( soundEvent, pos ); + if( name != null ) + { + mc.inGameHud.setRecordPlayingOverlay( new LiteralText( name ) ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/network/client/TerminalState.java b/remappedSrc/dan200/computercraft/shared/network/client/TerminalState.java new file mode 100644 index 000000000..0ecfe413c --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/client/TerminalState.java @@ -0,0 +1,196 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.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 net.minecraft.network.PacketByteBuf; + +import javax.annotation.Nullable; +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 ) + { + width = height = 0; + buffer = null; + } + else + { + width = terminal.getWidth(); + height = terminal.getHeight(); + + ByteBuf buf = buffer = Unpooled.buffer(); + terminal.write( new PacketByteBuf( buf ) ); + } + } + + public TerminalState( PacketByteBuf buf ) + { + colour = buf.readBoolean(); + compress = buf.readBoolean(); + + if( buf.readBoolean() ) + { + width = buf.readVarInt(); + height = buf.readVarInt(); + + int length = buf.readVarInt(); + buffer = readCompressed( buf, length, compress ); + } + else + { + width = height = 0; + buffer = null; + } + } + + 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; + } + } + + 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() ); + } + } + + 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; + } + + 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 ) ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/network/container/ComputerContainerData.java b/remappedSrc/dan200/computercraft/shared/network/container/ComputerContainerData.java new file mode 100644 index 000000000..5b97cca97 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/container/ComputerContainerData.java @@ -0,0 +1,59 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.container; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.ServerComputer; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.Identifier; + +public class ComputerContainerData implements ContainerData +{ + private static final Identifier IDENTIFIER = new Identifier( ComputerCraft.MOD_ID, "computer_container_data" ); + private int id; + private ComputerFamily family; + + public ComputerContainerData( ServerComputer computer ) + { + id = computer.getInstanceID(); + family = computer.getFamily(); + } + + public ComputerContainerData( PacketByteBuf byteBuf ) + { + fromBytes( byteBuf ); + } + + public void fromBytes( PacketByteBuf buf ) + { + id = buf.readInt(); + family = buf.readEnumConstant( ComputerFamily.class ); + } + + public Identifier getId() + { + return IDENTIFIER; + } + + @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/remappedSrc/dan200/computercraft/shared/network/container/ContainerData.java b/remappedSrc/dan200/computercraft/shared/network/container/ContainerData.java new file mode 100644 index 000000000..10f167418 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/container/ContainerData.java @@ -0,0 +1,46 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.container; + +import net.fabricmc.fabric.api.screenhandler.v1.ScreenHandlerRegistry; +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.util.Identifier; + +import javax.annotation.Nonnull; +import java.util.function.Function; + +/** + * An extension over the basic hooks, with a more convenient way of reading and writing data. + */ +public interface ContainerData +{ + static ScreenHandlerType toType( Identifier identifier, Function reader, + Factory factory ) + { + return ScreenHandlerRegistry.registerExtended( identifier, + ( id, playerInventory, packetByteBuf ) -> factory.create( id, + playerInventory, + reader.apply( packetByteBuf ) ) ); + } + + void toBytes( PacketByteBuf buf ); + + default void open( PlayerEntity player, NamedScreenHandlerFactory owner ) + { + player.openHandledScreen( owner ); + } + + interface Factory + { + C create( int id, @Nonnull PlayerInventory inventory, T data ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/network/container/HeldItemContainerData.java b/remappedSrc/dan200/computercraft/shared/network/container/HeldItemContainerData.java new file mode 100644 index 000000000..a32e35af0 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/container/HeldItemContainerData.java @@ -0,0 +1,46 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/network/container/ViewComputerContainerData.java b/remappedSrc/dan200/computercraft/shared/network/container/ViewComputerContainerData.java new file mode 100644 index 000000000..7d5943804 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/container/ViewComputerContainerData.java @@ -0,0 +1,80 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.container; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.computer.core.ServerComputer; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.Identifier; + +import javax.annotation.Nonnull; + +/** + * View an arbitrary computer on the client. + * + * @see dan200.computercraft.shared.command.CommandComputerCraft + */ +public class ViewComputerContainerData extends ComputerContainerData +{ + private static final Identifier IDENTIFIER = new Identifier( ComputerCraft.MOD_ID, "view_computer_container_data" ); + private int width; + private 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 packetByteBuf ) + { + super( new PacketByteBuf( packetByteBuf.copy() ) ); + fromBytes( packetByteBuf ); + } + + @Override + public void fromBytes( PacketByteBuf buf ) + { + super.fromBytes( buf ); + width = buf.readVarInt(); + height = buf.readVarInt(); + } + + @Override + public Identifier getId() + { + return IDENTIFIER; + } + + @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/remappedSrc/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java b/remappedSrc/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java new file mode 100644 index 000000000..0bdc624e8 --- /dev/null +++ b/remappedSrc/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-2021. 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 net.minecraft.network.PacketByteBuf; + +import javax.annotation.Nonnull; + +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/remappedSrc/dan200/computercraft/shared/network/server/ComputerServerMessage.java b/remappedSrc/dan200/computercraft/shared/network/server/ComputerServerMessage.java new file mode 100644 index 000000000..a97bc299b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/server/ComputerServerMessage.java @@ -0,0 +1,67 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.fabricmc.fabric.api.network.PacketContext; +import net.minecraft.network.PacketByteBuf; + +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( PacketContext context ) + { + ServerComputer computer = ComputerCraft.serverComputerRegistry.get( instanceId ); + if( computer == null ) + { + return; + } + + IContainerComputer container = computer.getContainer( context.getPlayer() ); + if( container == null ) + { + return; + } + + handle( computer, container ); + } + + protected abstract void handle( @Nonnull ServerComputer computer, @Nonnull IContainerComputer container ); +} diff --git a/remappedSrc/dan200/computercraft/shared/network/server/KeyEventServerMessage.java b/remappedSrc/dan200/computercraft/shared/network/server/KeyEventServerMessage.java new file mode 100644 index 000000000..03b7c11f2 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/server/KeyEventServerMessage.java @@ -0,0 +1,65 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.server; + +import dan200.computercraft.shared.computer.core.IContainerComputer; +import dan200.computercraft.shared.computer.core.InputState; +import dan200.computercraft.shared.computer.core.ServerComputer; +import net.minecraft.network.PacketByteBuf; + +import javax.annotation.Nonnull; + +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/remappedSrc/dan200/computercraft/shared/network/server/MouseEventServerMessage.java b/remappedSrc/dan200/computercraft/shared/network/server/MouseEventServerMessage.java new file mode 100644 index 000000000..fbccad974 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/server/MouseEventServerMessage.java @@ -0,0 +1,81 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.server; + +import dan200.computercraft.shared.computer.core.IContainerComputer; +import dan200.computercraft.shared.computer.core.InputState; +import dan200.computercraft.shared.computer.core.ServerComputer; +import net.minecraft.network.PacketByteBuf; + +import javax.annotation.Nonnull; + +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/remappedSrc/dan200/computercraft/shared/network/server/QueueEventServerMessage.java b/remappedSrc/dan200/computercraft/shared/network/server/QueueEventServerMessage.java new file mode 100644 index 000000000..69f94a300 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/server/QueueEventServerMessage.java @@ -0,0 +1,63 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.server; + +import dan200.computercraft.shared.computer.core.IContainerComputer; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.util.NBTUtil; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.PacketByteBuf; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * 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.writeNbt( args == null ? null : NBTUtil.encodeObjects( args ) ); + } + + @Override + public void fromBytes( @Nonnull PacketByteBuf buf ) + { + super.fromBytes( buf ); + event = buf.readString( Short.MAX_VALUE ); + + NbtCompound args = buf.readNbt(); + 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/remappedSrc/dan200/computercraft/shared/network/server/RequestComputerMessage.java b/remappedSrc/dan200/computercraft/shared/network/server/RequestComputerMessage.java new file mode 100644 index 000000000..2a0afddac --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/network/server/RequestComputerMessage.java @@ -0,0 +1,51 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.network.server; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.network.NetworkMessage; +import net.fabricmc.fabric.api.network.PacketContext; +import net.minecraft.network.PacketByteBuf; + +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( PacketContext context ) + { + ServerComputer computer = ComputerCraft.serverComputerRegistry.get( instance ); + if( computer != null ) + { + computer.sendComputerState( context.getPlayer() ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java b/remappedSrc/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java new file mode 100644 index 000000000..c812dd91b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java @@ -0,0 +1,100 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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 net.minecraft.block.entity.CommandBlockBlockEntity; +import net.minecraft.util.Identifier; + +import javax.annotation.Nonnull; + +/** + * 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 + */ +public class CommandBlockPeripheral implements IPeripheral +{ + private static final Identifier CAP_ID = new Identifier( ComputerCraft.MOD_ID, "command_block" ); + + private final CommandBlockBlockEntity commandBlock; + + public CommandBlockPeripheral( CommandBlockBlockEntity commandBlock ) + { + this.commandBlock = commandBlock; + } + + @Nonnull + @Override + public String getType() + { + return "command"; + } + + @Nonnull + @Override + public Object getTarget() + { + return commandBlock; + } + + @Override + public boolean equals( IPeripheral other ) + { + return other != null && other.getClass() == getClass(); + } + + /** + * 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" }; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/BlockDiskDrive.java b/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/BlockDiskDrive.java new file mode 100644 index 000000000..35ec91d6f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/BlockDiskDrive.java @@ -0,0 +1,91 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.diskdrive; + +import dan200.computercraft.shared.ComputerCraftRegistry; +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, ComputerCraftRegistry.ModTiles.DISK_DRIVE ); + setDefaultState( getStateManager().getDefaultState() + .with( FACING, Direction.NORTH ) + .with( STATE, DiskDriveState.EMPTY ) ); + } + + @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(); + } + } + } + + @Override + protected void appendProperties( StateManager.Builder properties ) + { + properties.add( FACING, STATE ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/ContainerDiskDrive.java b/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/ContainerDiskDrive.java new file mode 100644 index 000000000..385fe16b1 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/ContainerDiskDrive.java @@ -0,0 +1,104 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.diskdrive; + +import dan200.computercraft.shared.ComputerCraftRegistry; +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 ) + { + this( id, player, new SimpleInventory( 1 ) ); + } + + public ContainerDiskDrive( int id, PlayerInventory player, Inventory inventory ) + { + super( ComputerCraftRegistry.ModContainers.DISK_DRIVE, 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 ) ); + } + } + + @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; + } + + @Override + public boolean canUse( @Nonnull PlayerEntity player ) + { + return inventory.canPlayerUse( player ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java b/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java new file mode 100644 index 000000000..91ee5bc82 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java @@ -0,0 +1,224 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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"; + } + + @Override + public void attach( @Nonnull IComputerAccess computer ) + { + diskDrive.mount( computer ); + } + + @Override + public void detach( @Nonnull IComputerAccess computer ) + { + diskDrive.unmount( computer ); + } + + @Nonnull + @Override + public Object getTarget() + { + return diskDrive; + } + + @Override + public boolean equals( IPeripheral other ) + { + return this == other || other instanceof DiskDrivePeripheral && ((DiskDrivePeripheral) other).diskDrive == diskDrive; + } + + /** + * 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 false} if no audio disk is inserted. + * @cc.treturn string|nil|false The title of the audio, {@code false} if no disk is inserted, or {@code nil} if the disk has no audio. + */ + @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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveState.java b/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveState.java new file mode 100644 index 000000000..1e146c3c7 --- /dev/null +++ b/remappedSrc/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-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.diskdrive; + +import net.minecraft.util.StringIdentifiable; + +import javax.annotation.Nonnull; + +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/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java b/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java new file mode 100644 index 000000000..f896b31bc --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java @@ -0,0 +1,575 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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.api.peripheral.IPeripheralTile; +import dan200.computercraft.shared.MediaProviders; +import dan200.computercraft.shared.common.TileGeneric; +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.NbtCompound; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.sound.SoundEvent; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public final class TileDiskDrive extends TileGeneric implements DefaultInventory, Tickable, IPeripheralTile, Nameable, NamedScreenHandlerFactory +{ + private static final String NBT_NAME = "CustomName"; + private static final String NBT_ITEM = "Item"; + private final Map computers = new HashMap<>(); + Text customName; + @Nonnull + private ItemStack diskStack = ItemStack.EMPTY; + private IMount diskMount = null; + private boolean recordQueued = false; + private boolean recordPlaying = false; + private boolean restartRecord = false; + private boolean ejectQueued; + + public TileDiskDrive( BlockEntityType type ) + { + super( type ); + } + + @Override + public void destroy() + { + ejectContents( true ); + if( recordPlaying ) + { + stopRecord(); + } + } + + @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 ) + { + player.openHandledScreen( this ); + } + return ActionResult.SUCCESS; + } + } + + public Direction getDirection() + { + return getCachedState().get( BlockDiskDrive.FACING ); + } + + @Override + public void readNbt( @Nonnull BlockState state, @Nonnull NbtCompound nbt ) + { + super.readNbt( state, nbt ); + customName = nbt.contains( NBT_NAME ) ? Text.Serializer.fromJson( nbt.getString( NBT_NAME ) ) : null; + if( nbt.contains( NBT_ITEM ) ) + { + NbtCompound item = nbt.getCompound( NBT_ITEM ); + diskStack = ItemStack.fromNbt( item ); + diskMount = null; + } + } + + @Nonnull + @Override + public NbtCompound writeNbt( @Nonnull NbtCompound nbt ) + { + if( customName != null ) + { + nbt.putString( NBT_NAME, Text.Serializer.toJson( customName ) ); + } + + if( !diskStack.isEmpty() ) + { + NbtCompound item = new NbtCompound(); + diskStack.writeNbt( item ); + nbt.put( NBT_ITEM, item ); + } + return super.writeNbt( nbt ); + } + + @Override + public void markDirty() + { + if( !world.isClient ) + { + updateBlockState(); + } + super.markDirty(); + } + + @Override + public void tick() + { + // Ejection + if( ejectQueued ) + { + ejectContents( false ); + ejectQueued = false; + } + + // Music + synchronized( this ) + { + if( !world.isClient && recordPlaying != recordQueued || restartRecord ) + { + restartRecord = false; + if( recordQueued ) + { + IMedia contents = getDiskMedia(); + SoundEvent record = contents != null ? contents.getAudio( diskStack ) : null; + if( record != null ) + { + recordPlaying = true; + playRecord(); + } + else + { + recordQueued = false; + } + } + else + { + stopRecord(); + recordPlaying = false; + } + } + } + } + + // IInventory implementation + + @Override + public int size() + { + return 1; + } + + @Override + public boolean isEmpty() + { + return diskStack.isEmpty(); + } + + @Nonnull + @Override + public ItemStack getStack( int slot ) + { + return diskStack; + } + + @Nonnull + @Override + public ItemStack removeStack( int slot, int count ) + { + if( diskStack.isEmpty() ) + { + return ItemStack.EMPTY; + } + + if( diskStack.getCount() <= count ) + { + ItemStack disk = diskStack; + setStack( slot, ItemStack.EMPTY ); + return disk; + } + + ItemStack part = diskStack.split( count ); + setStack( slot, diskStack.isEmpty() ? ItemStack.EMPTY : diskStack ); + return part; + } + + @Nonnull + @Override + public ItemStack removeStack( int slot ) + { + ItemStack result = diskStack; + diskStack = ItemStack.EMPTY; + diskMount = null; + + return result; + } + + @Override + public void setStack( int slot, @Nonnull ItemStack stack ) + { + if( getWorld().isClient ) + { + diskStack = stack; + diskMount = null; + markDirty(); + return; + } + + synchronized( this ) + { + if( InventoryUtil.areItemsStackable( stack, diskStack ) ) + { + diskStack = stack; + return; + } + + // Unmount old disk + if( !diskStack.isEmpty() ) + { + // TODO: Is this iteration thread safe? + Set computers = this.computers.keySet(); + for( IComputerAccess computer : computers ) + { + unmountDisk( computer ); + } + } + + // Stop music + if( recordPlaying ) + { + stopRecord(); + recordPlaying = false; + recordQueued = false; + } + + // Swap disk over + diskStack = stack; + diskMount = null; + markDirty(); + + // Mount new disk + if( !diskStack.isEmpty() ) + { + Set computers = this.computers.keySet(); + for( IComputerAccess computer : computers ) + { + mountDisk( computer ); + } + } + } + } + + @Override + public boolean canPlayerUse( @Nonnull PlayerEntity player ) + { + return isUsable( player, false ); + } + + @Override + public void clear() + { + setStack( 0, ItemStack.EMPTY ); + } + + @Nonnull + @Override + public IPeripheral getPeripheral( Direction side ) + { + return new DiskDrivePeripheral( this ); + } + + String getDiskMountPath( IComputerAccess computer ) + { + synchronized( this ) + { + MountInfo info = computers.get( computer ); + return info != null ? info.mountPath : null; + } + } + + void mount( IComputerAccess computer ) + { + synchronized( this ) + { + computers.put( computer, new MountInfo() ); + mountDisk( computer ); + } + } + + private synchronized void mountDisk( IComputerAccess computer ) + { + if( !diskStack.isEmpty() ) + { + MountInfo info = computers.get( computer ); + IMedia contents = getDiskMedia(); + if( contents != null ) + { + if( diskMount == null ) + { + diskMount = contents.createDataMount( diskStack, getWorld() ); + } + if( diskMount != null ) + { + if( 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) 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, diskMount ); + n++; + } + } + } + else + { + info.mountPath = null; + } + } + computer.queueEvent( "disk", computer.getAttachmentName() ); + } + } + + private IMedia getDiskMedia() + { + return MediaProviders.get( getDiskStack() ); + } + + @Nonnull + ItemStack getDiskStack() + { + return getStack( 0 ); + } + + void setDiskStack( @Nonnull ItemStack stack ) + { + setStack( 0, stack ); + } + + void unmount( IComputerAccess computer ) + { + synchronized( this ) + { + unmountDisk( computer ); + computers.remove( computer ); + } + } + + private synchronized void unmountDisk( IComputerAccess computer ) + { + if( !diskStack.isEmpty() ) + { + MountInfo info = computers.get( computer ); + assert info != null; + if( info.mountPath != null ) + { + computer.unmount( info.mountPath ); + info.mountPath = null; + } + computer.queueEvent( "disk_eject", computer.getAttachmentName() ); + } + } + + void playDiskAudio() + { + synchronized( this ) + { + IMedia media = getDiskMedia(); + if( media != null && media.getAudioTitle( diskStack ) != null ) + { + recordQueued = true; + restartRecord = recordPlaying; + } + } + } + + void stopDiskAudio() + { + synchronized( this ) + { + recordQueued = false; + restartRecord = false; + } + } + + // private methods + + void ejectDisk() + { + synchronized( this ) + { + ejectQueued = true; + } + } + + private void updateBlockState() + { + if( removed ) + { + return; + } + + if( !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 || diskStack.isEmpty() ) + { + return; + } + + // Remove the disks from the inventory + ItemStack disks = 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 void playRecord() + { + IMedia contents = getDiskMedia(); + SoundEvent record = contents != null ? contents.getAudio( diskStack ) : null; + if( record != null ) + { + RecordUtil.playRecord( record, contents.getAudioTitle( diskStack ), getWorld(), getPos() ); + } + else + { + RecordUtil.playRecord( null, null, getWorld(), getPos() ); + } + } + + // Private methods + + private void stopRecord() + { + RecordUtil.playRecord( null, null, getWorld(), getPos() ); + } + + @Nonnull + @Override + public Text getName() + { + return customName != null ? customName : new TranslatableText( getCachedState().getBlock() + .getTranslationKey() ); + } + + @Override + public boolean hasCustomName() + { + return customName != null; + } + + @Nonnull + @Override + public Text getDisplayName() + { + return Nameable.super.getDisplayName(); + } + + @Nullable + @Override + public Text getCustomName() + { + return customName; + } + + @Nonnull + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory inventory, @Nonnull PlayerEntity player ) + { + return new ContainerDiskDrive( id, inventory, this ); + } + + private static class MountInfo + { + String mountPath; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java b/remappedSrc/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java new file mode 100644 index 000000000..c4c59ee4a --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java @@ -0,0 +1,76 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IDynamicPeripheral; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.util.Identifier; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +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 = BlockEntityType.getId( tile.getType() ); + this.tile = tile; + this.type = type == null ? "unknown" : type.toString(); + this.methods = methods; + } + + @Nonnull + @Override + public String[] getMethodNames() + { + String[] names = new String[methods.size()]; + for( int i = 0; i < methods.size(); i++ ) names[i] = methods.get( i ).getName(); + return names; + } + + @Nonnull + @Override + public MethodResult callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments ) throws LuaException + { + return methods.get( method ).apply( context, computer, arguments ); + } + + @Nonnull + @Override + public String getType() + { + return type; + } + + @Nullable + @Override + public Object getTarget() + { + return tile; + } + + @Override + public boolean equals( @Nullable IPeripheral other ) + { + if( other == this ) return true; + if( !(other instanceof GenericPeripheral) ) return false; + + GenericPeripheral generic = (GenericPeripheral) other; + return tile == generic.tile && methods.equals( generic.methods ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java b/remappedSrc/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java new file mode 100644 index 000000000..d4b3e0b16 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/generic/GenericPeripheralProvider.java @@ -0,0 +1,45 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.asm.NamedMethod; +import dan200.computercraft.core.asm.PeripheralMethod; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +public class GenericPeripheralProvider +{ + @Nullable + public static IPeripheral getPeripheral( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side ) + { + BlockEntity tile = world.getBlockEntity( pos ); + if( tile == null ) return null; + + ArrayList saturated = new ArrayList<>( 0 ); + + List> tileMethods = PeripheralMethod.GENERATOR.getMethods( tile.getClass() ); + if( !tileMethods.isEmpty() ) addSaturated( saturated, tile, tileMethods ); + + return saturated.isEmpty() ? null : new GenericPeripheral( tile, saturated ); + } + + private static void addSaturated( ArrayList saturated, Object target, List> methods ) + { + saturated.ensureCapacity( saturated.size() + methods.size() ); + for( NamedMethod method : methods ) + { + saturated.add( new SaturatedMethod( target, method ) ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java b/remappedSrc/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java new file mode 100644 index 000000000..eca38e63f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/generic/SaturatedMethod.java @@ -0,0 +1,58 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.core.asm.NamedMethod; +import dan200.computercraft.core.asm.PeripheralMethod; + +import javax.annotation.Nonnull; + +final class SaturatedMethod +{ + private final Object target; + private final String name; + private final PeripheralMethod method; + + SaturatedMethod( Object target, NamedMethod method ) + { + this.target = target; + 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/remappedSrc/dan200/computercraft/shared/peripheral/generic/data/BlockData.java b/remappedSrc/dan200/computercraft/shared/peripheral/generic/data/BlockData.java new file mode 100644 index 000000000..4b9a9e0ed --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/generic/data/BlockData.java @@ -0,0 +1,40 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic.data; + +import com.google.common.collect.ImmutableMap; +import net.minecraft.block.BlockState; +import net.minecraft.state.property.Property; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; + +public class BlockData +{ + @Nonnull + public static > T fill( @Nonnull T data, @Nonnull BlockState state ) + { + data.put( "name", DataHelpers.getId( state.getBlock() ) ); + + Map stateTable = new HashMap<>(); + for( ImmutableMap.Entry, ? extends Comparable> entry : state.getEntries().entrySet() ) + { + Property property = entry.getKey(); + stateTable.put( property.getName(), getPropertyValue( property, entry.getValue() ) ); + } + data.put( "state", stateTable ); + + return data; + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + private static Object getPropertyValue( Property property, Comparable value ) + { + if( value instanceof String || value instanceof Number || value instanceof Boolean ) return value; + return property.name( value ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/generic/data/DataHelpers.java b/remappedSrc/dan200/computercraft/shared/peripheral/generic/data/DataHelpers.java new file mode 100644 index 000000000..210aa5b5a --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/generic/data/DataHelpers.java @@ -0,0 +1,53 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic.data; + +import net.minecraft.block.Block; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.item.Item; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public final class DataHelpers +{ + private DataHelpers() + { } + + @Nonnull + public static Map getTags( @Nonnull Collection tags ) + { + Map result = new HashMap<>( tags.size() ); + for( Identifier location : tags ) result.put( location.toString(), true ); + return result; + } + + @Nullable + public static String getId( @Nonnull Block block ) + { + Identifier id = Registry.BLOCK.getId( block ); + return id == null ? null : id.toString(); + } + + @Nullable + public static String getId( @Nonnull Item item ) + { + Identifier id = Registry.ITEM.getId( item ); + return id == null ? null : id.toString(); + } + + @Nullable + public static String getId( @Nonnull Enchantment enchantment ) + { + Identifier id = Registry.ENCHANTMENT.getId( enchantment ); + return id == null ? null : id.toString(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/generic/data/ItemData.java b/remappedSrc/dan200/computercraft/shared/peripheral/generic/data/ItemData.java new file mode 100644 index 000000000..a5589be06 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/generic/data/ItemData.java @@ -0,0 +1,182 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic.data; + +import com.google.gson.JsonParseException; +import dan200.computercraft.shared.util.NBTUtil; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.enchantment.EnchantmentHelper; +import net.minecraft.item.EnchantedBookItem; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.nbt.NbtList; +import net.minecraft.tag.ServerTagManagerHolder; +import net.minecraft.tag.TagGroup; +import net.minecraft.text.Text; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Data providers for items. + */ +public class ItemData +{ + @Nonnull + public static > T fillBasicSafe( @Nonnull T data, @Nonnull ItemStack stack ) + { + data.put( "name", DataHelpers.getId( stack.getItem() ) ); + data.put( "count", stack.getCount() ); + + return data; + } + + @Nonnull + public static > T fillBasic( @Nonnull T data, @Nonnull ItemStack stack ) + { + fillBasicSafe( data, stack ); + String hash = NBTUtil.getNBTHash( stack.getNbt() ); + 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.isDamaged() ) + { + data.put( "durability", (double) stack.getDamage() / stack.getMaxDamage() ); + } + + // requireNonNull is safe because we got the Identifiers out of the TagGroup to start with. Would be nicer + // to stream the tags directly but TagGroup isn't a collection :( + TagGroup itemTags = ServerTagManagerHolder.getTagManager().getItems(); + data.put( "tags", DataHelpers.getTags( itemTags.getTagIds().stream() + .filter( id -> Objects.requireNonNull( itemTags.getTag( id ) ).contains( stack.getItem() ) ) + .collect( Collectors.toList() ) + ) ); // chaos x2 + + NbtCompound tag = stack.getNbt(); + if( tag != null && tag.contains( "display", NBTUtil.TAG_COMPOUND ) ) + { + NbtCompound displayTag = tag.getCompound( "display" ); + if( displayTag.contains( "Lore", NBTUtil.TAG_LIST ) ) + { + NbtList loreTag = displayTag.getList( "Lore", NBTUtil.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 NbtElement x ) + { + try + { + return Text.Serializer.fromJson( x.toString() ); + } + catch( JsonParseException e ) + { + return null; + } + } + + /** + * Retrieve all visible enchantments from given stack. Try to follow all tooltip rules : order and visibility. + * + * @param stack Stack to analyse + * @param hideFlags An int used as bit field to provide visibility rules. + * @return A filled list that contain all visible enchantments. + */ + @Nonnull + private static List> getAllEnchants( @Nonnull ItemStack stack, int hideFlags ) + { + ArrayList> enchants = new ArrayList<>( 0 ); + + if( stack.getItem() instanceof EnchantedBookItem && (hideFlags & 32) == 0 ) + { + addEnchantments( EnchantedBookItem.getEnchantmentNbt( stack ), enchants ); + } + + if( stack.hasEnchantments() && (hideFlags & 1) == 0 ) + { + /* + * Mimic the EnchantmentHelper.getEnchantments(ItemStack stack) behavior without special case for Enchanted book. + * I'll do that to have the same data than ones displayed in tooltip. + * @see EnchantmentHelper.getEnchantments(ItemStack stack) + */ + addEnchantments( stack.getEnchantments(), enchants ); + } + + return enchants; + } + + /** + * Converts a Mojang enchant map to a Lua list. + * + * @param rawEnchants The raw NBT list of enchantments + * @param enchants The enchantment map to add it to. + * @see EnchantmentHelper + */ + private static void addEnchantments( @Nonnull NbtList rawEnchants, @Nonnull ArrayList> enchants ) + { + if( rawEnchants.isEmpty() ) return; + + enchants.ensureCapacity( enchants.size() + rawEnchants.size() ); + + + for( Map.Entry entry : EnchantmentHelper.fromNbt( 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/remappedSrc/dan200/computercraft/shared/peripheral/generic/methods/ArgumentHelpers.java b/remappedSrc/dan200/computercraft/shared/peripheral/generic/methods/ArgumentHelpers.java new file mode 100644 index 000000000..119831409 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/generic/methods/ArgumentHelpers.java @@ -0,0 +1,37 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic.methods; + +import dan200.computercraft.api.lua.LuaException; + +/** + * A few helpers for working with arguments. + * + * This should really be moved into the public API. However, until I have settled on a suitable format, we'll keep it + * where it is used. + */ +final class ArgumentHelpers +{ + private ArgumentHelpers() + { + } + + public static void assertBetween( double value, double min, double max, String message ) throws LuaException + { + if( value < min || value > max || Double.isNaN( value ) ) + { + throw new LuaException( String.format( message, "between " + min + " and " + max ) ); + } + } + + public static void assertBetween( int value, int min, int max, String message ) throws LuaException + { + if( value < min || value > max ) + { + throw new LuaException( String.format( message, "between " + min + " and " + max ) ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/remappedSrc/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java new file mode 100644 index 000000000..4b5d76be1 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java @@ -0,0 +1,349 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.generic.methods; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.GenericSource; +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.shared.peripheral.generic.data.ItemData; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.ItemStorage; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.Nameable; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static dan200.computercraft.shared.peripheral.generic.methods.ArgumentHelpers.assertBetween; + +/** + * Methods for interacting with inventories. + * + * @cc.module inventory + */ +public class InventoryMethods implements GenericSource +{ + @Nonnull + @Override + public Identifier id() + { + return new Identifier( ComputerCraft.MOD_ID, "inventory" ); + } + + /** + * Get the size of this inventory. + * + * @param inventory The current inventory. + * @return The number of slots in this inventory. + */ + @LuaFunction( mainThread = true ) + public static int size( Inventory inventory ) + { + return extractHandler( inventory ).size(); + } + + /** + * Get the name of this inventory. + * + * @param inventory The current inventory. + * @return The name of this inventory, or {@code nil} if not present. + */ + @LuaFunction( mainThread = true ) + public static String name( Inventory inventory ) + { + if( inventory instanceof Nameable ) + { + Nameable i = (Nameable) inventory; + return i.hasCustomName() ? i.getName().asString() : null; + } + return null; + } + + /** + * List all items in this inventory. This returns a table, with an entry for each slot. + * + * Each item in the inventory is represented by a table containing some basic information, much like + * {@link dan200.computercraft.shared.turtle.apis.TurtleAPI#getItemDetail} includes. More information can be fetched + * with {@link #getItemDetail}. The table contains the item `name`, the `count` and an a (potentially nil) hash of + * the item's `nbt.` This NBT data doesn't contain anything useful, but allows you to distinguish identical items. + * + * The returned table is sparse, and so empty slots will be `nil` - it is recommended to loop over using `pairs` + * rather than `ipairs`. + * + * @param inventory The current inventory. + * @return All items in this inventory. + * @cc.treturn { (table|nil)... } All items in this inventory. + * @cc.usage Find an adjacent chest and print all items in it. + * + *
{@code
+     * local chest = peripheral.find("minecraft:chest")
+     * for slot, item in pairs(chest.list()) do
+     *   print(("%d x %s in slot %d"):format(item.count, item.name, slot))
+     * end
+     * }
+ */ + @LuaFunction( mainThread = true ) + public static Map> list( Inventory inventory ) + { + ItemStorage itemStorage = extractHandler( inventory ); + + Map> result = new HashMap<>(); + int size = itemStorage.size(); + for( int i = 0; i < size; i++ ) + { + ItemStack stack = itemStorage.getStack( i ); + if( !stack.isEmpty() ) result.put( i + 1, ItemData.fillBasic( new HashMap<>( 4 ), stack ) ); + } + + return result; + } + + /** + * Get detailed information about an item. + * + * The returned information contains the same information as each item in + * {@link #list}, as well as additional details like the display name + * (`displayName`) and item durability (`damage`, `maxDamage`, `durability`). + * + * Some items include more information (such as enchantments) - it is + * recommended to print it out using @{textutils.serialize} or in the Lua + * REPL, to explore what is available. + * + * @param inventory The current inventory. + * @param slot The slot to get information about. + * @return Information about the item in this slot, or {@code nil} if not present. + * @throws LuaException If the slot is out of range. + * @cc.treturn table Information about the item in this slot, or {@code nil} if not present. + * @cc.usage Print some information about the first in a chest. + * + *
{@code
+     * local chest = peripheral.find("minecraft:chest")
+     * local item = chest.getItemDetail(1)
+     * if not item then print("No item") return end
+     *
+     * print(("%s (%s)"):format(item.displayName, item.name))
+     * print(("Count: %d/%d"):format(item.count, item.maxCount))
+     * if item.damage then
+     *   print(("Damage: %d/%d"):format(item.damage, item.maxDamage))
+     * end
+     * }
+ */ + @Nullable + @LuaFunction( mainThread = true ) + public static Map getItemDetail( Inventory inventory, int slot ) throws LuaException + { + ItemStorage itemStorage = extractHandler( inventory ); + + assertBetween( slot, 1, itemStorage.size(), "Slot out of range (%s)" ); + + ItemStack stack = itemStorage.getStack( slot - 1 ); + return stack.isEmpty() ? null : ItemData.fill( new HashMap<>(), stack ); + } + + /** + * Get the maximum number of items which can be stored in this slot. + * + * Typically this will be limited to 64 items. However, some inventories (such as barrels or caches) can store + * hundreds or thousands of items in one slot. + * + * @param inventory Inventory to probe. + * @param slot The slot + * @return The maximum number of items in this slot. + * @throws LuaException If the slot is out of range. + * @cc.usage Count the maximum number of items an adjacent chest can hold. + *
{@code
+     * local chest = peripheral.find("minecraft:chest")
+     * local total = 0
+     * for i = 1, chest.size() do
+     *   total = total + chest.getItemLimit(i)
+     * end
+     * print(total)
+     * }
+ */ + @LuaFunction( mainThread = true ) + public static int getItemLimit( Inventory inventory, int slot ) throws LuaException + { + assertBetween( slot, 1, inventory.size(), "Slot out of range (%s)" ); + return inventory.getMaxCountPerStack(); + } + + /** + * Push items from one inventory to another connected one. + * + * This allows you to push an item in an inventory to another inventory on the same wired network. Both + * inventories must attached to wired modems which are connected via a cable. + * + * @param from Inventory to move items from. + * @param computer The current computer. + * @param toName The name of the peripheral/inventory to push to. This is the string given to @{peripheral.wrap}, + * and displayed by the wired modem. + * @param fromSlot The slot in the current inventory to move items to. + * @param limit The maximum number of items to move. Defaults to the current stack limit. + * @param toSlot The slot in the target inventory to move to. If not given, the item will be inserted into any slot. + * @return The number of transferred items. + * @throws LuaException If the peripheral to transfer to doesn't exist or isn't an inventory. + * @throws LuaException If either source or destination slot is out of range. + * @cc.see peripheral.getName Allows you to get the name of a @{peripheral.wrap|wrapped} peripheral. + * @cc.usage Wrap two chests, and push an item from one to another. + *
{@code
+     * local chest_a = peripheral.wrap("minecraft:chest_0")
+     * local chest_b = peripheral.wrap("minecraft:chest_1")
+     *
+     * chest_a.pushItems(peripheral.getName(chest_b), 1)
+     * }
+ */ + @LuaFunction( mainThread = true ) + public static int pushItems( + Inventory from, IComputerAccess computer, + String toName, int fromSlot, Optional limit, Optional toSlot + ) throws LuaException + { + ItemStorage fromStorage = extractHandler( from ); + + // Find location to transfer to + IPeripheral location = computer.getAvailablePeripheral( toName ); + if( location == null ) throw new LuaException( "Target '" + toName + "' does not exist" ); + + ItemStorage toStorage = extractHandler( location.getTarget() ); + if( toStorage == null ) throw new LuaException( "Target '" + toName + "' is not an inventory" ); + + // Validate slots + int actualLimit = limit.orElse( Integer.MAX_VALUE ); + assertBetween( fromSlot, 1, fromStorage.size(), "From slot out of range (%s)" ); + if( toSlot.isPresent() ) assertBetween( toSlot.get(), 1, toStorage.size(), "To slot out of range (%s)" ); + + if( actualLimit <= 0 ) return 0; + return moveItem( fromStorage, fromSlot - 1, toStorage, toSlot.orElse( 0 ) - 1, actualLimit ); + } + + /** + * Pull items from a connected inventory into this one. + * + * This allows you to transfer items between inventories on the same wired network. Both this and the source + * inventory must attached to wired modems which are connected via a cable. + * + * @param to Inventory to move items to. + * @param computer The current computer. + * @param fromName The name of the peripheral/inventory to pull from. This is the string given to @{peripheral.wrap}, + * and displayed by the wired modem. + * @param fromSlot The slot in the source inventory to move items from. + * @param limit The maximum number of items to move. Defaults to the current stack limit. + * @param toSlot The slot in current inventory to move to. If not given, the item will be inserted into any slot. + * @return The number of transferred items. + * @throws LuaException If the peripheral to transfer to doesn't exist or isn't an inventory. + * @throws LuaException If either source or destination slot is out of range. + * @cc.see peripheral.getName Allows you to get the name of a @{peripheral.wrap|wrapped} peripheral. + * @cc.usage Wrap two chests, and push an item from one to another. + *
{@code
+     * local chest_a = peripheral.wrap("minecraft:chest_0")
+     * local chest_b = peripheral.wrap("minecraft:chest_1")
+     *
+     * chest_a.pullItems(peripheral.getName(chest_b), 1)
+     * }
+ */ + @LuaFunction( mainThread = true ) + public static int pullItems( + Inventory to, IComputerAccess computer, + String fromName, int fromSlot, Optional limit, Optional toSlot + ) throws LuaException + { + // Get appropriate inventory for source peripheral + ItemStorage toStorage = extractHandler( to ); + + // Find location to transfer to + IPeripheral location = computer.getAvailablePeripheral( fromName ); + if( location == null ) throw new LuaException( "Source '" + fromName + "' does not exist" ); + + ItemStorage fromStorage = extractHandler( location.getTarget() ); + if( fromStorage == null ) throw new LuaException( "Source '" + fromName + "' is not an inventory" ); + + // Validate slots + int actualLimit = limit.orElse( Integer.MAX_VALUE ); + assertBetween( fromSlot, 1, fromStorage.size(), "From slot out of range (%s)" ); + if( toSlot.isPresent() ) assertBetween( toSlot.get(), 1, toStorage.size(), "To slot out of range (%s)" ); + + if( actualLimit <= 0 ) return 0; + return moveItem( fromStorage, fromSlot - 1, toStorage, toSlot.orElse( 0 ) - 1, actualLimit ); + } + + + @Nullable + private static ItemStorage extractHandler( @Nullable Object object ) + { + if( object instanceof BlockEntity ) + { + Inventory inventory = InventoryUtil.getInventory( (BlockEntity) object ); + if( inventory != null ) + { + return ItemStorage.wrap( inventory ); + } + } + else if ( object instanceof Inventory ) + { + return ItemStorage.wrap( (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( ItemStorage from, int fromSlot, ItemStorage to, int toSlot, final int limit ) + { + // Moving nothing is easy + if( limit == 0 ) + { + return 0; + } + + // Get stack to move + ItemStack stack = InventoryUtil.takeItems( limit, from, fromSlot, 1, fromSlot ); + if( stack.isEmpty() ) + { + return 0; + } + int stackCount = stack.getCount(); + + // Move items in + ItemStack remainder; + if( toSlot < 0 ) + { + remainder = InventoryUtil.storeItems( stack, to ); + } + else + { + remainder = InventoryUtil.storeItems( stack, to, toSlot, 1, toSlot ); + } + + // Calculate items moved + int count = stackCount - remainder.getCount(); + + if( !remainder.isEmpty() ) + { + // Put the remainder back + InventoryUtil.storeItems( remainder, from, fromSlot, 1, fromSlot ); + } + + return count; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/ModemPeripheral.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/ModemPeripheral.java new file mode 100644 index 000000000..27e02f18f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/ModemPeripheral.java @@ -0,0 +1,272 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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 final Set computers = new HashSet<>( 1 ); + private final ModemState state; + private IPacketNetwork network; + + protected ModemPeripheral( ModemState state ) + { + this.state = state; + } + + public ModemState getModemState() + { + return state; + } + + public void destroy() + { + setNetwork( null ); + } + + @Override + public void receiveSameDimension( @Nonnull Packet packet, double distance ) + { + if( packet.getSender() == this || !state.isOpen( packet.getChannel() ) ) + { + return; + } + + synchronized( computers ) + { + for( IComputerAccess computer : 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 || !state.isOpen( packet.getChannel() ) ) + { + return; + } + + synchronized( computers ) + { + for( IComputerAccess computer : computers ) + { + computer.queueEvent( "modem_message", computer.getAttachmentName(), packet.getChannel(), packet.getReplyChannel(), packet.getPayload() ); + } + } + } + + @Nonnull + @Override + public String getType() + { + return "modem"; + } + + @Override + public synchronized void attach( @Nonnull IComputerAccess computer ) + { + synchronized( computers ) + { + computers.add( computer ); + } + + setNetwork( getNetwork() ); + } + + protected abstract IPacketNetwork getNetwork(); + + private synchronized void setNetwork( IPacketNetwork network ) + { + if( this.network == network ) + { + return; + } + + // Leave old network + if( this.network != null ) + { + this.network.removeReceiver( this ); + } + + // Set new network + this.network = network; + + // Join new network + if( this.network != null ) + { + this.network.addReceiver( this ); + } + } + + @Override + public synchronized void detach( @Nonnull IComputerAccess computer ) + { + boolean empty; + synchronized( computers ) + { + computers.remove( computer ); + empty = computers.isEmpty(); + } + + if( empty ) + { + setNetwork( null ); + } + } + + /** + * 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 + { + state.open( parseChannel( channel ) ); + } + + private static int parseChannel( int channel ) throws LuaException + { + if( channel < 0 || channel > 65535 ) + { + throw new LuaException( "Expected number in range 0-65535" ); + } + return 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 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 + { + state.close( parseChannel( channel ) ); + } + + /** + * Close all open channels. + */ + @LuaFunction + public final void closeAll() + { + 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 = this.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 = this.network; + return network != null && network.isWireless(); + } + + @Nonnull + @Override + public String getSenderID() + { + synchronized( computers ) + { + if( computers.size() != 1 ) + { + return "unknown"; + } + else + { + IComputerAccess computer = computers.iterator() + .next(); + return computer.getID() + "_" + computer.getAttachmentName(); + } + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/ModemShapes.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/ModemShapes.java new file mode 100644 index 000000000..1cf37fe63 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/ModemShapes.java @@ -0,0 +1,38 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.modem; + +import net.minecraft.util.math.Direction; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; + +import javax.annotation.Nonnull; + +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/remappedSrc/dan200/computercraft/shared/peripheral/modem/ModemState.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/ModemState.java new file mode 100644 index 000000000..95f52550b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/ModemState.java @@ -0,0 +1,99 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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 final IntSet channels = new IntOpenHashSet(); + private boolean open = false; + + public ModemState() + { + onChanged = null; + } + + public ModemState( Runnable onChanged ) + { + this.onChanged = onChanged; + } + + public boolean pollChanged() + { + return changed.getAndSet( false ); + } + + public boolean isOpen() + { + return open; + } + + private void setOpen( boolean open ) + { + if( this.open == open ) + { + return; + } + this.open = open; + if( !changed.getAndSet( true ) && onChanged != null ) + { + onChanged.run(); + } + } + + 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/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java new file mode 100644 index 000000000..27874fc9f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java @@ -0,0 +1,274 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.modem.wired; + +import com.google.common.collect.ImmutableMap; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.shared.ComputerCraftRegistry; +import dan200.computercraft.shared.common.BlockGeneric; +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.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.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.BlockView; +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, ComputerCraftRegistry.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 ) ); + } + + public static boolean canConnectIn( BlockState state, Direction direction ) + { + return state.get( BlockCable.CABLE ) && state.get( BlockCable.MODEM ) + .getFacing() != direction; + } + + @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 ) ); + } + + 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() ) != null; + } + + @Nonnull + @Override + @Deprecated + public FluidState getFluidState( @Nonnull BlockState state ) + { + return getWaterloggedFluidState( 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( ComputerCraftRegistry.ModItems.WIRED_MODEM.get() ); + // } + // else + // { + // newState = state.with( CABLE, false ); + // item = new ItemStack( ComputerCraftRegistry.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 ); + // } + + // TODO Re-implement, likely will need mixin + // @Override + // public ItemStack getPickStack(BlockView world, BlockPos pos, BlockState state) { + // 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( ComputerCraftRegistry.ModItems.WIRED_MODEM.get() ); + // if( modem == null ) return new ItemStack( ComputerCraftRegistry.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( ComputerCraftRegistry.ModItems.WIRED_MODEM.get() ) + // : new ItemStack( ComputerCraftRegistry.ModItems.CABLE.get() ); + // } + + @Override + @Deprecated + public boolean canPlaceAt( BlockState state, @Nonnull WorldView world, @Nonnull BlockPos pos ) + { + Direction facing = state.get( MODEM ) + .getFacing(); + if( facing == null ) + { + return true; + } + + return sideCoversSmallSquare( world, pos.offset( facing ), facing.getOpposite() ); + } + + @Nonnull + @Override + @Deprecated + public VoxelShape getOutlineShape( @Nonnull BlockState state, @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull ShapeContext context ) + { + return CableShapes.getShape( state ); + } + + @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() ) ); + } + } + + @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 ); + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( MODEM, CABLE, NORTH, SOUTH, EAST, WEST, UP, DOWN, WATERLOGGED ); + } + + 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/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/BlockWiredModemFull.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/BlockWiredModemFull.java new file mode 100644 index 000000000..bdc49847e --- /dev/null +++ b/remappedSrc/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-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.modem.wired; + +import dan200.computercraft.shared.common.BlockGeneric; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +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, BlockEntityType type ) + { + super( settings, type ); + 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/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java new file mode 100644 index 000000000..a45f27db7 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java @@ -0,0 +1,85 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.modem.wired; + +import net.minecraft.util.StringIdentifiable; +import net.minecraft.util.math.Direction; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +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/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/CableShapes.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/CableShapes.java new file mode 100644 index 000000000..12bd696cb --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/CableShapes.java @@ -0,0 +1,160 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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() + { + } + + public static VoxelShape getCableShape( BlockState state ) + { + if( !state.get( CABLE ) ) + { + return VoxelShapes.empty(); + } + return getCableShape( getCableIndex( state ) ); + } + + 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; + } + + 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; + } + + 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; + } + + public static VoxelShape getModemShape( BlockState state ) + { + Direction facing = state.get( MODEM ) + .getFacing(); + return facing == null ? VoxelShapes.empty() : ModemShapes.getBounds( facing ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/ItemBlockCable.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/ItemBlockCable.java new file mode 100644 index 000000000..8e6cc47e7 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/ItemBlockCable.java @@ -0,0 +1,177 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.modem.wired; + +import dan200.computercraft.shared.ComputerCraftRegistry; +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.ActionResult; +import net.minecraft.util.Util; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.World; + +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 placeAtCorrected( World world, BlockPos pos, BlockState state ) + { + return placeAt( world, pos, correctConnections( world, pos, state ), null ); + } + + 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() + .getSoundGroup( state ); + 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; + } + + @Nonnull + @Override + public String getTranslationKey() + { + if( translationKey == null ) + { + translationKey = Util.createTranslationKey( "block", Registry.ITEM.getId( this ) ); + } + return translationKey; + } + + @Override + public void appendStacks( @Nonnull ItemGroup group, @Nonnull DefaultedList list ) + { + if( isIn( group ) ) + { + list.add( new ItemStack( this ) ); + } + } + + 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() == ComputerCraftRegistry.ModBlocks.CABLE && 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() == ComputerCraftRegistry.ModBlocks.CABLE && !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() == ComputerCraftRegistry.ModBlocks.CABLE && !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/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java new file mode 100644 index 000000000..32f851d32 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java @@ -0,0 +1,464 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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.api.peripheral.IPeripheralTile; +import dan200.computercraft.shared.ComputerCraftRegistry; +import dan200.computercraft.shared.command.text.ChatHelpers; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.peripheral.modem.ModemState; +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.NbtCompound; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.Map; + +public class TileCable extends TileGeneric implements IPeripheralTile +{ + private static final String NBT_PERIPHERAL_ENABLED = "PeirpheralAccess"; + private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(); + private final WiredModemElement cable = new CableElement(); + private final IWiredNode node = cable.getNode(); + private boolean peripheralAccessAllowed; + private boolean destroyed = false; + private Direction modemDirection = Direction.NORTH; + private final WiredModemPeripheral modem = new WiredModemPeripheral( new ModemState( () -> TickScheduler.schedule( this ) ), cable ) + { + @Nonnull + @Override + protected WiredModemLocalPeripheral getLocalPeripheral() + { + return 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 boolean hasModemDirection = false; + private boolean connectionsFormed = false; + + public TileCable( BlockEntityType type ) + { + super( type ); + } + + @Override + public void destroy() + { + if( !destroyed ) + { + destroyed = true; + modem.destroy(); + onRemove(); + } + } + + @Override + public void onChunkUnloaded() + { + super.onChunkUnloaded(); + onRemove(); + } + + private void onRemove() + { + if( world == null || !world.isClient ) + { + node.remove(); + connectionsFormed = false; + } + } + + @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 = peripheral.getConnectedName(); + togglePeripheralAccess(); + String newName = peripheral.getConnectedName(); + if( !Objects.equal( newName, oldName ) ) + { + if( oldName != null ) + { + player.sendMessage( new TranslatableText( "chat.computercraft.wired_modem.peripheral_disconnected", + ChatHelpers.copy( oldName ) ), false ); + } + if( newName != null ) + { + player.sendMessage( new TranslatableText( "chat.computercraft.wired_modem.peripheral_connected", + ChatHelpers.copy( newName ) ), false ); + } + } + + return ActionResult.SUCCESS; + } + + @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( ComputerCraftRegistry.ModItems.WIRED_MODEM ) ); + getWorld().setBlockState( getPos(), + getCachedState().with( BlockCable.MODEM, CableModemVariant.None ) ); + modemChanged(); + connectionsChanged(); + } + else + { + // Drop everything and remove block + Block.dropStack( getWorld(), getPos(), new ItemStack( ComputerCraftRegistry.ModItems.WIRED_MODEM ) ); + getWorld().removeBlock( getPos(), false ); + // This'll call #destroy(), so we don't need to reset the network here. + } + + return; + } + + onNeighbourTileEntityChange( neighbour ); + } + + @Nonnull + private Direction getDirection() + { + refreshDirection(); + return modemDirection == null ? Direction.NORTH : modemDirection; + } + + public boolean hasModem() + { + return getCachedState().get( BlockCable.MODEM ) != CableModemVariant.None; + } + + boolean hasCable() + { + return getCachedState().get( BlockCable.CABLE ); + } + + void modemChanged() + { + // Tell anyone who cares that the connection state has changed + if( getWorld().isClient ) + { + return; + } + + // If we can no longer attach peripherals, then detach any + // which may have existed + if( !canAttachPeripheral() && peripheralAccessAllowed ) + { + peripheralAccessAllowed = false; + peripheral.detach(); + node.updatePeripherals( Collections.emptyMap() ); + markDirty(); + updateBlockState(); + } + } + + 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.isChunkLoaded( offset ) ) + { + continue; + } + + IWiredElement element = ComputerCraftAPI.getWiredElementAt( world, offset, facing.getOpposite() ); + if( element != null ) + { + // TODO Figure out why this crashes. + IWiredNode node = element.getNode(); + if( node != null && this.node != null ) + { + if( BlockCable.canConnectIn( state, facing ) ) + { + // If we can connect to it then do so + this.node.connectTo( node ); + } + else if( this.node.getNetwork() == node.getNetwork() ) + { + // Otherwise if we're on the same network then attempt to void it. + this.node.disconnectFrom( node ); + } + } + } + } + } + + private boolean canAttachPeripheral() + { + return hasCable() && hasModem(); + } + + private void updateBlockState() + { + BlockState state = getCachedState(); + CableModemVariant oldVariant = state.get( BlockCable.MODEM ); + CableModemVariant newVariant = CableModemVariant.from( oldVariant.getFacing(), modem.getModemState() + .isOpen(), peripheralAccessAllowed ); + + if( oldVariant != newVariant ) + { + world.setBlockState( getPos(), state.with( BlockCable.MODEM, newVariant ) ); + } + } + + private void refreshPeripheral() + { + if( world != null && !isRemoved() && peripheral.attach( world, getPos(), getDirection() ) ) + { + updateConnectedPeripherals(); + } + } + + private void updateConnectedPeripherals() + { + Map peripherals = peripheral.toMap(); + if( peripherals.isEmpty() ) + { + // If there are no peripherals then disable access and update the display state. + peripheralAccessAllowed = false; + updateBlockState(); + } + + node.updatePeripherals( peripherals ); + } + + @Override + public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour ) + { + super.onNeighbourTileEntityChange( neighbour ); + if( !world.isClient && peripheralAccessAllowed ) + { + Direction facing = getDirection(); + if( getPos().offset( facing ) + .equals( neighbour ) ) + { + refreshPeripheral(); + } + } + } + + @Override + public void blockTick() + { + if( getWorld().isClient ) + { + return; + } + + refreshDirection(); + + if( modem.getModemState() + .pollChanged() ) + { + updateBlockState(); + } + + if( !connectionsFormed ) + { + connectionsFormed = true; + + connectionsChanged(); + if( peripheralAccessAllowed ) + { + peripheral.attach( world, pos, modemDirection ); + updateConnectedPeripherals(); + } + } + } + + private void togglePeripheralAccess() + { + if( !peripheralAccessAllowed ) + { + peripheral.attach( world, getPos(), getDirection() ); + if( !peripheral.hasPeripheral() ) + { + return; + } + + peripheralAccessAllowed = true; + node.updatePeripherals( peripheral.toMap() ); + } + else + { + peripheral.detach(); + + peripheralAccessAllowed = false; + node.updatePeripherals( Collections.emptyMap() ); + } + + updateBlockState(); + } + + @Nullable + private Direction getMaybeDirection() + { + refreshDirection(); + return modemDirection; + } + + private void refreshDirection() + { + if( hasModemDirection ) + { + return; + } + + hasModemDirection = true; + modemDirection = getCachedState().get( BlockCable.MODEM ) + .getFacing(); + } + + @Override + public void readNbt( @Nonnull BlockState state, @Nonnull NbtCompound nbt ) + { + super.readNbt( state, nbt ); + peripheralAccessAllowed = nbt.getBoolean( NBT_PERIPHERAL_ENABLED ); + peripheral.read( nbt, "" ); + } + + @Nonnull + @Override + public NbtCompound writeNbt( NbtCompound nbt ) + { + nbt.putBoolean( NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed ); + peripheral.write( nbt, "" ); + return super.writeNbt( nbt ); + } + + @Override + public void markRemoved() + { + super.markRemoved(); + onRemove(); + } + + @Override + public void cancelRemoval() + { + super.cancelRemoval(); + TickScheduler.schedule( this ); + } + + @Override + public void resetBlock() + { + super.resetBlock(); + hasModemDirection = false; + if( !world.isClient ) + { + world.getBlockTickScheduler() + .schedule( pos, + getCachedState().getBlock(), 0 ); + } + } + + public IWiredElement getElement( Direction facing ) + { + return BlockCable.canConnectIn( getCachedState(), facing ) ? cable : null; + } + + @Nonnull + @Override + public IPeripheral getPeripheral( Direction side ) + { + return !destroyed && hasModem() && side == getDirection() ? modem : null; + } + + 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 ) + { + modem.attachPeripheral( name, peripheral ); + } + + @Override + protected void detachPeripheral( String name ) + { + modem.detachPeripheral( name ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java new file mode 100644 index 000000000..5d2bb768a --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java @@ -0,0 +1,446 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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.api.peripheral.IPeripheralTile; +import dan200.computercraft.shared.command.text.ChatHelpers; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.peripheral.modem.ModemState; +import dan200.computercraft.shared.util.DirectionUtil; +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.NbtCompound; +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 javax.annotation.Nonnull; +import java.util.*; + +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 implements IPeripheralTile +{ + private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess"; + private final WiredModemPeripheral[] modems = new WiredModemPeripheral[6]; + private final WiredModemLocalPeripheral[] peripherals = new WiredModemLocalPeripheral[6]; + private final ModemState modemState = new ModemState( () -> TickScheduler.schedule( this ) ); + private final WiredModemElement element = new FullElement( this ); + private final IWiredNode node = element.getNode(); + private boolean peripheralAccessAllowed = false; + private boolean destroyed = false; + private boolean connectionsFormed = false; + + public TileWiredModemFull( BlockEntityType type ) + { + super( type ); + for( int i = 0; i < peripherals.length; i++ ) + { + Direction facing = Direction.byId( i ); + peripherals[i] = new WiredModemLocalPeripheral(); + } + } + + @Override + public void destroy() + { + if( !destroyed ) + { + destroyed = true; + doRemove(); + } + super.destroy(); + } + + @Override + public void onChunkUnloaded() + { + super.onChunkUnloaded(); + doRemove(); + } + + private void doRemove() + { + if( world == null || !world.isClient ) + { + node.remove(); + connectionsFormed = false; + } + } + + @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; + } + + @Override + public void onNeighbourChange( @Nonnull BlockPos neighbour ) + { + onNeighbourTileEntityChange( neighbour ); + } + + @Override + public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour ) + { + if( !world.isClient && peripheralAccessAllowed ) + { + for( Direction facing : DirectionUtil.FACINGS ) + { + if( getPos().offset( facing ) + .equals( neighbour ) ) + { + refreshPeripheral( facing ); + } + } + } + } + + @Override + public void blockTick() + { + if( getWorld().isClient ) + { + return; + } + + if( modemState.pollChanged() ) + { + updateBlockState(); + } + + if( !connectionsFormed ) + { + connectionsFormed = true; + + connectionsChanged(); + if( peripheralAccessAllowed ) + { + for( Direction facing : DirectionUtil.FACINGS ) + { + 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.isChunkLoaded( offset ) ) + { + continue; + } + + IWiredElement element = ComputerCraftAPI.getWiredElementAt( world, offset, facing.getOpposite() ); + if( element == null ) + { + continue; + } + + node.connectTo( element.getNode() ); + } + } + + private void refreshPeripheral( @Nonnull Direction facing ) + { + WiredModemLocalPeripheral peripheral = peripherals[facing.ordinal()]; + if( world != null && !isRemoved() && peripheral.attach( world, getPos(), facing ) ) + { + updateConnectedPeripherals(); + } + } + + private void updateConnectedPeripherals() + { + Map peripherals = getConnectedPeripherals(); + if( peripherals.isEmpty() ) + { + // If there are no peripherals then disable access and update the display state. + peripheralAccessAllowed = false; + updateBlockState(); + } + + node.updatePeripherals( peripherals ); + } + + private Map getConnectedPeripherals() + { + if( !peripheralAccessAllowed ) + { + return Collections.emptyMap(); + } + + Map peripherals = new HashMap<>( 6 ); + for( WiredModemLocalPeripheral peripheral : this.peripherals ) + { + peripheral.extendMap( peripherals ); + } + return peripherals; + } + + private void updateBlockState() + { + BlockState state = getCachedState(); + boolean modemOn = modemState.isOpen(), peripheralOn = 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 ) ); + } + + private Set getConnectedPeripheralNames() + { + if( !peripheralAccessAllowed ) + { + return Collections.emptySet(); + } + + Set peripherals = new HashSet<>( 6 ); + for( WiredModemLocalPeripheral peripheral : this.peripherals ) + { + String name = peripheral.getConnectedName(); + if( name != null ) + { + peripherals.add( name ); + } + } + return peripherals; + } + + private void togglePeripheralAccess() + { + if( !peripheralAccessAllowed ) + { + boolean hasAny = false; + for( Direction facing : DirectionUtil.FACINGS ) + { + WiredModemLocalPeripheral peripheral = peripherals[facing.ordinal()]; + peripheral.attach( world, getPos(), facing ); + hasAny |= peripheral.hasPeripheral(); + } + + if( !hasAny ) + { + return; + } + + peripheralAccessAllowed = true; + node.updatePeripherals( getConnectedPeripherals() ); + } + else + { + peripheralAccessAllowed = false; + + for( WiredModemLocalPeripheral peripheral : peripherals ) + { + peripheral.detach(); + } + node.updatePeripherals( Collections.emptyMap() ); + } + + updateBlockState(); + } + + 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( ChatHelpers.copy( names.get( i ) ) ); + } + + player.sendMessage( new TranslatableText( kind, base ), false ); + } + + @Override + public void readNbt( @Nonnull BlockState state, @Nonnull NbtCompound nbt ) + { + super.readNbt( state, nbt ); + peripheralAccessAllowed = nbt.getBoolean( NBT_PERIPHERAL_ENABLED ); + for( int i = 0; i < peripherals.length; i++ ) + { + peripherals[i].read( nbt, Integer.toString( i ) ); + } + } + + @Nonnull + @Override + public NbtCompound writeNbt( NbtCompound nbt ) + { + nbt.putBoolean( NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed ); + for( int i = 0; i < peripherals.length; i++ ) + { + peripherals[i].write( nbt, Integer.toString( i ) ); + } + return super.writeNbt( nbt ); + } + + @Override + public void markRemoved() + { + super.markRemoved(); + doRemove(); + } + + @Override + public void cancelRemoval() + { + super.cancelRemoval(); + TickScheduler.schedule( this ); + } + + public IWiredElement getElement() + { + return element; + } + + @Nonnull + @Override + public IPeripheral getPeripheral( Direction side ) + { + WiredModemPeripheral peripheral = modems[side.ordinal()]; + if( peripheral != null ) + { + return peripheral; + } + + WiredModemLocalPeripheral localPeripheral = peripherals[side.ordinal()]; + return modems[side.ordinal()] = new WiredModemPeripheral( modemState, 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; + } + }; + } + + private static final class FullElement extends WiredModemElement + { + private final TileWiredModemFull entity; + + private FullElement( TileWiredModemFull entity ) + { + this.entity = entity; + } + + @Override + protected void detachPeripheral( String name ) + { + for( int i = 0; i < 6; i++ ) + { + WiredModemPeripheral modem = entity.modems[i]; + if( modem != null ) + { + modem.detachPeripheral( name ); + } + } + } + + @Override + protected void attachPeripheral( String name, IPeripheral peripheral ) + { + for( int i = 0; i < 6; i++ ) + { + WiredModemPeripheral modem = entity.modems[i]; + if( modem != null ) + { + modem.attachPeripheral( name, peripheral ); + } + } + } + + @Nonnull + @Override + public World getWorld() + { + return entity.getWorld(); + } + + @Nonnull + @Override + public Vec3d getPosition() + { + BlockPos pos = entity.getPos(); + return new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/WiredModemElement.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/WiredModemElement.java new file mode 100644 index 000000000..a5b3d3e85 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/WiredModemElement.java @@ -0,0 +1,69 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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() ); + } + } + + protected abstract void detachPeripheral( String name ); + + protected abstract void attachPeripheral( String name, IPeripheral peripheral ); + + public Map getRemotePeripherals() + { + return remotePeripherals; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java new file mode 100644 index 000000000..de10208a1 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java @@ -0,0 +1,156 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.modem.wired; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.ComputerCraftRegistry; +import dan200.computercraft.shared.Peripherals; +import dan200.computercraft.shared.util.IDAssigner; +import dan200.computercraft.shared.util.NBTUtil; +import net.minecraft.block.Block; +import net.minecraft.nbt.NbtCompound; +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.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; + + /** + * 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 ); + } + } + + @Nullable + private IPeripheral getPeripheralFrom( World world, BlockPos pos, Direction direction ) + { + BlockPos offset = pos.offset( direction ); + + Block block = world.getBlockState( offset ) + .getBlock(); + if( block == ComputerCraftRegistry.ModBlocks.WIRED_MODEM_FULL || block == ComputerCraftRegistry.ModBlocks.CABLE ) + { + return null; + } + + IPeripheral peripheral = Peripherals.getPeripheral( world, offset, direction.getOpposite() ); + return peripheral instanceof WiredModemPeripheral ? null : 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 NbtCompound 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 NbtCompound tag, @Nonnull String suffix ) + { + id = tag.contains( NBT_PERIPHERAL_ID + suffix, NBTUtil.TAG_ANY_NUMERIC ) ? tag.getInt( NBT_PERIPHERAL_ID + suffix ) : -1; + + type = tag.contains( NBT_PERIPHERAL_TYPE + suffix, NBTUtil.TAG_STRING ) ? tag.getString( NBT_PERIPHERAL_TYPE + suffix ) : null; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java new file mode 100644 index 000000000..90b7555b5 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java @@ -0,0 +1,451 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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; + } + + @Override + public double getRange() + { + return 256.0; + } + + //region IPacketSender implementation + @Override + public boolean isInterdimensional() + { + return false; + } + + @Override + protected IPacketNetwork getNetwork() + { + return modem.getNode(); + } + + @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 ); + } + //endregion + + //region Peripheral methods + + 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(); + } + } + + @Nonnull + @Override + public World getWorld() + { + return modem.getWorld(); + } + + /** + * 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(); + } + + private ConcurrentMap getWrappers( IComputerAccess computer ) + { + synchronized( peripheralWrappers ) + { + return peripheralWrappers.get( computer ); + } + } + + /** + * 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; + } + + private RemotePeripheralWrapper getWrapper( IComputerAccess computer, String remoteName ) + { + ConcurrentMap wrappers = getWrappers( computer ); + return wrappers == null ? null : wrappers.get( remoteName ); + } + + /** + * 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 ) ); + } + //endregion + + /** + * 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 }; + } + + @Nonnull + protected abstract WiredModemLocalPeripheral getLocalPeripheral(); + + @Override + public boolean equals( IPeripheral other ) + { + if( other instanceof WiredModemPeripheral ) + { + WiredModemPeripheral otherModem = (WiredModemPeripheral) other; + return otherModem.modem == modem; + } + return false; + } + + @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 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 ); + } + + @Nonnull + @Override + public String getAttachmentName() + { + return name; + } + + @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 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 ); + } + } + + @Nonnull + @Override + public IWorkMonitor getMainThreadMonitor() + { + return computer.getMainThreadMonitor(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/BlockWirelessModem.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/BlockWirelessModem.java new file mode 100644 index 000000000..9ffeeab88 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/BlockWirelessModem.java @@ -0,0 +1,98 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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 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, BlockEntityType type ) + { + super( settings, type ); + setDefaultState( getStateManager().getDefaultState() + .with( FACING, Direction.NORTH ) + .with( ON, false ) + .with( WATERLOGGED, false ) ); + } + + @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; + } + + @Nonnull + @Override + @Deprecated + public FluidState getFluidState( @Nonnull BlockState state ) + { + return getWaterloggedFluidState( 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() ); + } + + @Nonnull + @Override + @Deprecated + public VoxelShape getOutlineShape( BlockState blockState, @Nonnull BlockView blockView, @Nonnull BlockPos blockPos, @Nonnull ShapeContext context ) + { + return ModemShapes.getBounds( blockState.get( FACING ) ); + } + + @Nullable + @Override + public BlockState getPlacementState( ItemPlacementContext placement ) + { + return getDefaultState().with( FACING, + placement.getSide() + .getOpposite() ) + .with( WATERLOGGED, getWaterloggedStateForPlacement( placement ) ); + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( FACING, ON, WATERLOGGED ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java new file mode 100644 index 000000000..ac3f9e401 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java @@ -0,0 +1,150 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.modem.wireless; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IPeripheralTile; +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.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 javax.annotation.Nonnull; + +public class TileWirelessModem extends TileGeneric implements IPeripheralTile +{ + private final boolean advanced; + private final ModemPeripheral modem; + private boolean hasModemDirection = false; + private Direction modemDirection = Direction.DOWN; + private boolean destroyed = false; + + public TileWirelessModem( BlockEntityType type, boolean advanced ) + { + super( type ); + this.advanced = advanced; + modem = new Peripheral( this ); + } + + @Override + public void cancelRemoval() + { + super.cancelRemoval(); + TickScheduler.schedule( this ); + } + + @Override + public void resetBlock() + { + super.resetBlock(); + hasModemDirection = false; + world.getBlockTickScheduler() + .schedule( getPos(), + getCachedState().getBlock(), 0 ); + } + + @Override + public void destroy() + { + if( !destroyed ) + { + modem.destroy(); + destroyed = true; + } + } + + @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( 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 IPeripheral getPeripheral( Direction side ) + { + refreshDirection(); + return side == modemDirection ? modem : null; + } + + 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() ); + } + + @Nonnull + @Override + public Object getTarget() + { + return entity; + } + + @Override + public boolean equals( IPeripheral other ) + { + return this == other || (other instanceof Peripheral && entity == ((Peripheral) other).entity); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemPeripheral.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemPeripheral.java new file mode 100644 index 000000000..a628cadf0 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemPeripheral.java @@ -0,0 +1,67 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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 advanced; + + public WirelessModemPeripheral( ModemState state, boolean advanced ) + { + super( state ); + this.advanced = advanced; + } + + @Override + public double getRange() + { + if( 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 + public boolean isInterdimensional() + { + return advanced; + } + + @Override + protected IPacketNetwork getNetwork() + { + return WirelessNetwork.getUniversal(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/WirelessNetwork.java b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/WirelessNetwork.java new file mode 100644 index 000000000..14f809156 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/modem/wireless/WirelessNetwork.java @@ -0,0 +1,100 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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 universalNetwork = null; + private final Set receivers = Collections.newSetFromMap( new ConcurrentHashMap<>() ); + + public static WirelessNetwork getUniversal() + { + if( universalNetwork == null ) + { + universalNetwork = new WirelessNetwork(); + } + return universalNetwork; + } + + public static void resetNetworks() + { + universalNetwork = null; + } + + @Override + public void addReceiver( @Nonnull IPacketReceiver receiver ) + { + Objects.requireNonNull( receiver, "device cannot be null" ); + receivers.add( receiver ); + } + + @Override + public void removeReceiver( @Nonnull IPacketReceiver receiver ) + { + Objects.requireNonNull( receiver, "device cannot be null" ); + receivers.remove( receiver ); + } + + @Override + public boolean isWireless() + { + return true; + } + + @Override + public void transmitSameDimension( @Nonnull Packet packet, double range ) + { + Objects.requireNonNull( packet, "packet cannot be null" ); + for( IPacketReceiver device : receivers ) + { + tryTransmit( device, packet, range, false ); + } + } + + @Override + public void transmitInterdimensional( @Nonnull Packet packet ) + { + Objects.requireNonNull( packet, "packet cannot be null" ); + for( IPacketReceiver device : 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 ); + } + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java new file mode 100644 index 000000000..cd427e379 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java @@ -0,0 +1,100 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.monitor; + +import dan200.computercraft.api.turtle.FakePlayer; +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 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, BlockEntityType 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 + @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; + // Defer the block update if we're being placed by another TE. See #691 + if( livingEntity == null || livingEntity instanceof FakePlayer ) + { + monitor.updateNeighborsDeferred(); + return; + } + + monitor.updateNeighbors(); + } + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( ORIENTATION, FACING, STATE ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java new file mode 100644 index 000000000..eabaafb6f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java @@ -0,0 +1,157 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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 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; + +@Environment( EnvType.CLIENT ) +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; + } + + @Environment( EnvType.CLIENT ) + public static void destroyAll() + { + synchronized( allMonitors ) + { + for( Iterator iterator = allMonitors.iterator(); iterator.hasNext(); ) + { + ClientMonitor monitor = iterator.next(); + monitor.deleteBuffers(); + + iterator.remove(); + } + } + } + + 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_R8UI, 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 deleteBuffers() + { + + if( tboBuffer != 0 ) + { + RenderSystem.glDeleteBuffers( tboBuffer ); + tboBuffer = 0; + } + + if( tboTexture != 0 ) + { + GlStateManager.deleteTexture( tboTexture ); + tboTexture = 0; + } + + if( buffer != null ) + { + buffer.close(); + buffer = null; + } + } + + private void addMonitor() + { + synchronized( allMonitors ) + { + allMonitors.add( this ); + } + } + + @Environment( EnvType.CLIENT ) + public void destroy() + { + if( tboBuffer != 0 || buffer != null ) + { + synchronized( allMonitors ) + { + allMonitors.remove( this ); + } + + deleteBuffers(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/monitor/MonitorEdgeState.java b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/MonitorEdgeState.java new file mode 100644 index 000000000..1c4a0a87e --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/MonitorEdgeState.java @@ -0,0 +1,75 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.monitor; + +import net.minecraft.util.StringIdentifiable; + +import javax.annotation.Nonnull; + +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 static final MonitorEdgeState[] BY_FLAG = new MonitorEdgeState[16]; + + static + { + for( MonitorEdgeState state : values() ) + { + BY_FLAG[state.flags] = state; + } + } + + private final String name; + private final int flags; + + MonitorEdgeState( String name, int flags ) + { + this.name = name; + this.flags = flags; + } + + 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/remappedSrc/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java new file mode 100644 index 000000000..2ca3c7da9 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java @@ -0,0 +1,136 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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"; + } + + @Override + public void attach( @Nonnull IComputerAccess computer ) + { + monitor.addComputer( computer ); + } + + @Override + public void detach( @Nonnull IComputerAccess computer ) + { + monitor.removeComputer( computer ); + } + + @Nullable + @Override + public Object getTarget() + { + return monitor; + } + + @Override + public boolean equals( IPeripheral other ) + { + return other instanceof MonitorPeripheral && monitor == ((MonitorPeripheral) other).monitor; + } + + /** + * 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; + } + + /** + * 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 ); + } + + @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(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/monitor/MonitorRenderer.java b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/MonitorRenderer.java new file mode 100644 index 000000000..14aec1173 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/MonitorRenderer.java @@ -0,0 +1,104 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.monitor; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.render.TileEntityMonitorRenderer; +import net.fabricmc.loader.api.FabricLoader; +import org.lwjgl.opengl.GL; + +import javax.annotation.Nonnull; +import java.util.Arrays; +import java.util.List; + +/** + * 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. + */ + VBO; + + private static boolean initialised = false; + private static boolean textureBuffer = false; + private static boolean shaderMod = false; + //TODO find out which shader mods do better with VBOs and add them here. + private static List shaderModIds = Arrays.asList( "optifabric" ); + + /** + * 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() + { + if( !initialised ) + { + checkCapabilities(); + checkForShaderMods(); + if( textureBuffer && shaderMod ) + { + ComputerCraft.log.warn( "Shader mod detected. Enabling VBO renderer for compatibility." ); + } + + initialised = true; + } + + return textureBuffer && !shaderMod ? TBO : VBO; + } + + private static void checkCapabilities() + { + textureBuffer = GL.getCapabilities().OpenGL31; + } + + private static void checkForShaderMods() + { + shaderMod = FabricLoader.getInstance().getAllMods().stream() + .map( modContainer -> modContainer.getMetadata().getId() ) + .anyMatch( id -> shaderModIds.contains( id ) ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java new file mode 100644 index 000000000..f5225574c --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java @@ -0,0 +1,96 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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 final AtomicBoolean resized = new AtomicBoolean( false ); + private final AtomicBoolean changed = new AtomicBoolean( false ); + private int textScale = 2; + + public ServerMonitor( boolean colour, TileMonitor origin ) + { + super( colour ); + this.origin = origin; + } + + @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 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(); + } + } + + public boolean pollResized() + { + return resized.getAndSet( false ); + } + + public boolean pollTerminalChanged() + { + update(); + return hasTerminalChanged(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java new file mode 100644 index 000000000..4ba434d58 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java @@ -0,0 +1,869 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.monitor; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IPeripheralTile; +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.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.NbtCompound; +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.world.World; + +import javax.annotation.Nonnull; +import java.util.HashSet; +import java.util.Set; + +public class TileMonitor extends TileGeneric implements IPeripheralTile +{ + 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 final Set computers = new HashSet<>(); + // MonitorWatcher state. + boolean enqueued; + TerminalState cached; + private ServerMonitor serverMonitor; + private ClientMonitor clientMonitor; + private MonitorPeripheral peripheral; + private boolean needsUpdate = false; + private boolean destroyed = false; + private boolean visiting = false; + private int width = 1; + private int height = 1; + private int xIndex = 0; + private int yIndex = 0; + + public TileMonitor( BlockEntityType type, boolean advanced ) + { + super( type ); + this.advanced = advanced; + } + + @Override + public void destroy() + { + // TODO: Call this before using the block + if( destroyed ) + { + return; + } + destroyed = true; + if( !getWorld().isClient ) + { + contractNeighbours(); + } + } + + @Override + public void markRemoved() + { + super.markRemoved(); + if( clientMonitor != null && xIndex == 0 && yIndex == 0 ) + { + clientMonitor.destroy(); + } + } + + @Override + public void onChunkUnloaded() + { + super.onChunkUnloaded(); + if( clientMonitor != null && xIndex == 0 && yIndex == 0 ) + { + clientMonitor.destroy(); + } + clientMonitor = null; + } + + @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; + } + + @Override + public void blockTick() + { + if( needsUpdate ) + { + needsUpdate = false; + updateNeighbors(); + } + + if( xIndex != 0 || yIndex != 0 || serverMonitor == null ) + { + return; + } + + serverMonitor.clearChanged(); + + if( serverMonitor.pollResized() ) + { + for( int x = 0; x < width; x++ ) + { + for( int y = 0; y < height; y++ ) + { + TileMonitor monitor = getNeighbour( x, y ); + if( monitor == null ) + { + continue; + } + + for( IComputerAccess computer : monitor.computers ) + { + computer.queueEvent( "monitor_resize", computer.getAttachmentName() ); + } + } + } + } + + if( serverMonitor.pollTerminalChanged() ) + { + updateBlock(); + } + } + + @Override + protected final void readDescription( @Nonnull NbtCompound nbt ) + { + super.readDescription( nbt ); + + int oldXIndex = xIndex; + int oldYIndex = yIndex; + int oldWidth = width; + int oldHeight = height; + + xIndex = nbt.getInt( NBT_X ); + yIndex = nbt.getInt( NBT_Y ); + width = nbt.getInt( NBT_WIDTH ); + height = nbt.getInt( NBT_HEIGHT ); + + if( oldXIndex != xIndex || oldYIndex != 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 && clientMonitor != null ) + { + clientMonitor.destroy(); + } + clientMonitor = null; + } + + if( xIndex == 0 && yIndex == 0 ) + { + // If we're the origin terminal then create it. + if( clientMonitor == null ) + { + clientMonitor = new ClientMonitor( advanced, this ); + } + clientMonitor.readDescription( nbt ); + } + + if( oldXIndex != xIndex || oldYIndex != yIndex || oldWidth != width || oldHeight != height ) + { + // One of our properties has changed, so ensure we redraw the block + updateBlock(); + } + } + + @Override + protected void writeDescription( @Nonnull NbtCompound nbt ) + { + super.writeDescription( nbt ); + nbt.putInt( NBT_X, xIndex ); + nbt.putInt( NBT_Y, yIndex ); + nbt.putInt( NBT_WIDTH, width ); + nbt.putInt( NBT_HEIGHT, height ); + + if( xIndex == 0 && yIndex == 0 && serverMonitor != null ) + { + serverMonitor.writeDescription( nbt ); + } + } + + private TileMonitor getNeighbour( int x, int y ) + { + BlockPos pos = getPos(); + Direction right = getRight(); + Direction down = getDown(); + int xOffset = -xIndex + x; + int yOffset = -yIndex + y; + return getSimilarMonitorAt( pos.offset( right, xOffset ) + .offset( down, yOffset ) ); + } + + 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(); + } + + private TileMonitor getSimilarMonitorAt( BlockPos pos ) + { + if( pos.equals( getPos() ) ) + { + return this; + } + + int y = pos.getY(); + World world = getWorld(); + if( world == null || !world.isChunkLoaded( pos ) ) + { + return null; + } + + BlockEntity tile = world.getBlockEntity( pos ); + if( !(tile instanceof TileMonitor) ) + { + return null; + } + + TileMonitor monitor = (TileMonitor) tile; + return !monitor.visiting && !monitor.destroyed && advanced == monitor.advanced && getDirection() == monitor.getDirection() && getOrientation() == monitor.getOrientation() ? monitor : null; + } + + // region Sizing and placement stuff + public Direction getDirection() + { + // Ensure we're actually a monitor block. This _should_ always be the case, but sometimes there's + // fun problems with the block being missing on the client. + BlockState state = getCachedState(); + return state.contains( BlockMonitor.FACING ) ? state.get( BlockMonitor.FACING ) : Direction.NORTH; + } + + public Direction getOrientation() + { + return getCachedState().get( BlockMonitor.ORIENTATION ); + } + + @Override + public void readNbt( @Nonnull BlockState state, @Nonnull NbtCompound nbt ) + { + super.readNbt( state, nbt ); + + xIndex = nbt.getInt( NBT_X ); + yIndex = nbt.getInt( NBT_Y ); + width = nbt.getInt( NBT_WIDTH ); + height = nbt.getInt( NBT_HEIGHT ); + } + + // Networking stuff + + @Nonnull + @Override + public NbtCompound writeNbt( NbtCompound tag ) + { + tag.putInt( NBT_X, xIndex ); + tag.putInt( NBT_Y, yIndex ); + tag.putInt( NBT_WIDTH, width ); + tag.putInt( NBT_HEIGHT, height ); + return super.writeNbt( tag ); + } + + @Override + public double getRenderDistance() + { + return ComputerCraft.monitorDistanceSq; + } + + // Sizing and placement stuff + + @Override + public void cancelRemoval() + { + super.cancelRemoval(); + TickScheduler.schedule( this ); + } + + @Nonnull + @Override + public IPeripheral getPeripheral( Direction side ) + { + createServerMonitor(); // Ensure the monitor is created before doing anything else. + if( peripheral == null ) + { + peripheral = new MonitorPeripheral( this ); + } + return peripheral; + } + + public ServerMonitor getCachedServerMonitor() + { + return serverMonitor; + } + + private ServerMonitor getServerMonitor() + { + if( serverMonitor != null ) + { + return serverMonitor; + } + + TileMonitor origin = getOrigin(); + if( origin == null ) + { + return null; + } + + return serverMonitor = origin.serverMonitor; + } + + private ServerMonitor createServerMonitor() + { + if( serverMonitor != null ) + { + return serverMonitor; + } + + if( xIndex == 0 && yIndex == 0 ) + { + // If we're the origin, set up the new monitor + serverMonitor = new ServerMonitor( advanced, this ); + serverMonitor.rebuild(); + + // And propagate it to child monitors + for( int x = 0; x < width; x++ ) + { + for( int y = 0; y < height; y++ ) + { + TileMonitor monitor = getNeighbour( x, y ); + if( monitor != null ) + { + monitor.serverMonitor = serverMonitor; + } + } + } + + return 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(), -xIndex ) + .offset( getDown(), -yIndex ) ); + if( !(te instanceof TileMonitor) ) + { + return null; + } + + return serverMonitor = ((TileMonitor) te).createServerMonitor(); + } + } + + public ClientMonitor getClientMonitor() + { + if( clientMonitor != null ) + { + return clientMonitor; + } + + BlockPos pos = getPos(); + BlockEntity te = world.getBlockEntity( pos.offset( getRight(), -xIndex ) + .offset( getDown(), -yIndex ) ); + if( !(te instanceof TileMonitor) ) + { + return null; + } + + return clientMonitor = ((TileMonitor) te).clientMonitor; + } + + public final void read( TerminalState state ) + { + if( xIndex != 0 || yIndex != 0 ) + { + ComputerCraft.log.warn( "Receiving monitor state for non-origin terminal at {}", getPos() ); + return; + } + + if( clientMonitor == null ) + { + clientMonitor = new ClientMonitor( advanced, this ); + } + clientMonitor.read( state ); + } + + private void updateBlockState() + { + getWorld().setBlockState( getPos(), + getCachedState().with( BlockMonitor.STATE, + MonitorEdgeState.fromConnections( yIndex < height - 1, + yIndex > 0, xIndex > 0, xIndex < width - 1 ) ), + 2 ); + } + + public Direction getFront() + { + Direction orientation = getOrientation(); + return orientation == Direction.NORTH ? getDirection() : orientation; + } + + public int getWidth() + { + return width; + } + + public int getHeight() + { + return height; + } + + public int getXIndex() + { + return xIndex; + } + + public int getYIndex() + { + return yIndex; + } + + 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( xIndex != 0 || yIndex != 0 ) + { + serverMonitor = null; + } + + xIndex = 0; + yIndex = 0; + this.width = width; + this.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( serverMonitor == null ) + { + serverMonitor = new ServerMonitor( advanced, this ); + } + } + else + { + 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( serverMonitor != null ) + { + 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.xIndex = x; + monitor.yIndex = y; + monitor.width = width; + monitor.height = height; + monitor.serverMonitor = serverMonitor; + monitor.updateBlockState(); + monitor.updateBlock(); + } + } + } + + private boolean mergeLeft() + { + TileMonitor left = getNeighbour( -1, 0 ); + if( left == null || left.yIndex != 0 || left.height != height ) + { + return false; + } + + int width = left.width + this.width; + if( width > ComputerCraft.monitorWidth ) + { + return false; + } + + TileMonitor origin = left.getOrigin(); + if( origin != null ) + { + origin.resize( width, height ); + } + left.expand(); + return true; + } + + private boolean mergeRight() + { + TileMonitor right = getNeighbour( width, 0 ); + if( right == null || right.yIndex != 0 || right.height != height ) + { + return false; + } + + int width = this.width + right.width; + if( width > ComputerCraft.monitorWidth ) + { + return false; + } + + TileMonitor origin = getOrigin(); + if( origin != null ) + { + origin.resize( width, height ); + } + expand(); + return true; + } + + private boolean mergeUp() + { + TileMonitor above = getNeighbour( 0, height ); + if( above == null || above.xIndex != 0 || above.width != width ) + { + return false; + } + + int height = above.height + this.height; + if( height > ComputerCraft.monitorHeight ) + { + return false; + } + + TileMonitor origin = getOrigin(); + if( origin != null ) + { + origin.resize( width, height ); + } + expand(); + return true; + } + + private boolean mergeDown() + { + TileMonitor below = getNeighbour( 0, -1 ); + if( below == null || below.xIndex != 0 || below.width != width ) + { + return false; + } + + int height = this.height + below.height; + if( height > ComputerCraft.monitorHeight ) + { + return false; + } + + TileMonitor origin = below.getOrigin(); + if( origin != null ) + { + origin.resize( width, height ); + } + below.expand(); + return true; + } + + void updateNeighborsDeferred() + { + needsUpdate = true; + } + + void updateNeighbors() + { + contractNeighbours(); + contract(); + expand(); + } + + @SuppressWarnings( "StatementWithEmptyBody" ) + void expand() + { + while( mergeLeft() || mergeRight() || mergeUp() || mergeDown() ) ; + } + + void contractNeighbours() + { + visiting = true; + if( xIndex > 0 ) + { + TileMonitor left = getNeighbour( xIndex - 1, yIndex ); + if( left != null ) + { + left.contract(); + } + } + if( xIndex + 1 < width ) + { + TileMonitor right = getNeighbour( xIndex + 1, yIndex ); + if( right != null ) + { + right.contract(); + } + } + if( yIndex > 0 ) + { + TileMonitor below = getNeighbour( xIndex, yIndex - 1 ); + if( below != null ) + { + below.contract(); + } + } + if( yIndex + 1 < height ) + { + TileMonitor above = getNeighbour( xIndex, yIndex + 1 ); + if( above != null ) + { + above.contract(); + } + } + visiting = false; + } + + void contract() + { + int height = this.height; + int width = this.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; + } + } + } + // endregion + + private void monitorTouched( float xPos, float yPos, float zPos ) + { + XYPair pair = XYPair.of( xPos, yPos, zPos, getDirection(), getOrientation() ) + .add( xIndex, height - yIndex - 1 ); + + if( pair.x > width - RENDER_BORDER || pair.y > 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 = (width - (RENDER_BORDER + RENDER_MARGIN) * 2.0) / originTerminal.getWidth(); + double yCharHeight = (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 < height; y++ ) + { + for( int x = 0; x < width; x++ ) + { + TileMonitor monitor = getNeighbour( x, y ); + if( monitor == null ) + { + continue; + } + + for( IComputerAccess computer : monitor.computers ) + { + computer.queueEvent( "monitor_touch", computer.getAttachmentName(), xCharPos, yCharPos ); + } + } + } + } + + void addComputer( IComputerAccess computer ) + { + computers.add( 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 ); + // } + // } + + void removeComputer( IComputerAccess computer ) + { + computers.remove( computer ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/monitor/XYPair.java b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/XYPair.java new file mode 100644 index 000000000..8aa24a4b3 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/monitor/XYPair.java @@ -0,0 +1,74 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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 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 ); + } + + public XYPair add( float x, float y ) + { + return new XYPair( this.x + x, this.y + y ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/printer/BlockPrinter.java b/remappedSrc/dan200/computercraft/shared/peripheral/printer/BlockPrinter.java new file mode 100644 index 000000000..e2c6ff778 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/printer/BlockPrinter.java @@ -0,0 +1,91 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.printer; + +import dan200.computercraft.shared.ComputerCraftRegistry; +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 +{ + static final BooleanProperty TOP = BooleanProperty.of( "top" ); + static final BooleanProperty BOTTOM = BooleanProperty.of( "bottom" ); + private static final DirectionProperty FACING = Properties.HORIZONTAL_FACING; + + public BlockPrinter( Settings settings ) + { + super( settings, ComputerCraftRegistry.ModTiles.PRINTER ); + setDefaultState( getStateManager().getDefaultState() + .with( FACING, Direction.NORTH ) + .with( TOP, false ) + .with( BOTTOM, false ) ); + } + + @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(); + } + } + } + + @Override + protected void appendProperties( StateManager.Builder properties ) + { + properties.add( FACING, TOP, BOTTOM ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/printer/ContainerPrinter.java b/remappedSrc/dan200/computercraft/shared/peripheral/printer/ContainerPrinter.java new file mode 100644 index 000000000..4b94cb54d --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/printer/ContainerPrinter.java @@ -0,0 +1,143 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.printer; + +import dan200.computercraft.shared.ComputerCraftRegistry; +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.ScreenHandler; +import net.minecraft.screen.slot.Slot; + +import javax.annotation.Nonnull; + +public class ContainerPrinter extends ScreenHandler +{ + private final Inventory inventory; + private final PropertyDelegate properties; + + public ContainerPrinter( int id, PlayerInventory player ) + { + this( id, player, new SimpleInventory( TilePrinter.SLOTS ), new ArrayPropertyDelegate( 1 ) ); + } + + private ContainerPrinter( int id, PlayerInventory player, Inventory inventory, PropertyDelegate properties ) + { + super( ComputerCraftRegistry.ModContainers.PRINTER, 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, TilePrinter printer ) + { + this( id, player, printer, (SingleIntArray) () -> printer.isPrinting() ? 1 : 0 ); + } + + public boolean isPrinting() + { + return properties.get( 0 ) != 0; + } + + @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( TilePrinter.isInk( stack ) ) + { + 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; + } + + @Override + public boolean canUse( @Nonnull PlayerEntity player ) + { + return inventory.canPlayerUse( player ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java b/remappedSrc/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java new file mode 100644 index 000000000..3627af739 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java @@ -0,0 +1,192 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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. + + @Nonnull + @Override + public Object getTarget() + { + return printer; + } + + @Override + public boolean equals( IPeripheral other ) + { + return other instanceof PrinterPeripheral && ((PrinterPeripheral) other).printer == printer; + } + + /** + * 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() ); + } + + @Nonnull + private Terminal getCurrentPage() throws LuaException + { + Terminal currentPage = printer.getCurrentPage(); + if( currentPage == null ) + { + throw new LuaException( "Page not started" ); + } + return currentPage; + } + + /** + * 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(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/printer/TilePrinter.java b/remappedSrc/dan200/computercraft/shared/peripheral/printer/TilePrinter.java new file mode 100644 index 000000000..5443d1b7f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/printer/TilePrinter.java @@ -0,0 +1,536 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.printer; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IPeripheralTile; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.media.items.ItemPrintout; +import dan200.computercraft.shared.util.ColourUtils; +import dan200.computercraft.shared.util.DefaultSidedInventory; +import dan200.computercraft.shared.util.ItemStorage; +import dan200.computercraft.shared.util.WorldUtil; +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.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.ActionResult; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Hand; +import net.minecraft.util.Nameable; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class TilePrinter extends TileGeneric implements DefaultSidedInventory, IPeripheralTile, Nameable, NamedScreenHandlerFactory +{ + static final int SLOTS = 13; + private static final String NBT_NAME = "CustomName"; + private static final String NBT_PRINTING = "Printing"; + private static final String NBT_PAGE_TITLE = "PageTitle"; + 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 }; + private final DefaultedList inventory = DefaultedList.ofSize( SLOTS, ItemStack.EMPTY ); + private final ItemStorage itemHandlerAll = ItemStorage.wrap( this ); + private final Terminal page = new Terminal( ItemPrintout.LINE_MAX_LENGTH, ItemPrintout.LINES_PER_PAGE ); + Text customName; + private String pageTitle = ""; + private boolean printing = false; + + public TilePrinter( BlockEntityType type ) + { + super( type ); + } + + @Override + public void destroy() + { + ejectContents(); + } + + @Nonnull + @Override + public ActionResult onActivate( PlayerEntity player, Hand hand, BlockHitResult hit ) + { + if( player.isInSneakingPose() ) + { + return ActionResult.PASS; + } + + if( !getWorld().isClient ) + { + player.openHandledScreen( this ); + } + return ActionResult.SUCCESS; + } + + private void ejectContents() + { + for( int i = 0; i < 13; i++ ) + { + ItemStack stack = 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 = inventory.get( i ); + if( !stack.isEmpty() && isPaper( stack ) ) + { + top = true; + break; + } + } + for( int i = 7; i < 13; i++ ) + { + ItemStack stack = inventory.get( i ); + if( !stack.isEmpty() && isPaper( stack ) ) + { + bottom = true; + break; + } + } + + updateBlockState( top, bottom ); + } + + 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 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 ) ); + } + + @Override + public void readNbt( @Nonnull BlockState state, @Nonnull NbtCompound nbt ) + { + super.readNbt( state, nbt ); + + customName = nbt.contains( NBT_NAME ) ? Text.Serializer.fromJson( nbt.getString( NBT_NAME ) ) : null; + + // Read page + synchronized( page ) + { + printing = nbt.getBoolean( NBT_PRINTING ); + pageTitle = nbt.getString( NBT_PAGE_TITLE ); + page.readFromNBT( nbt ); + } + + // Read inventory + Inventories.readNbt( nbt, inventory ); + } + + @Nonnull + @Override + public NbtCompound writeNbt( @Nonnull NbtCompound nbt ) + { + if( customName != null ) + { + nbt.putString( NBT_NAME, Text.Serializer.toJson( customName ) ); + } + + // Write page + synchronized( page ) + { + nbt.putBoolean( NBT_PRINTING, printing ); + nbt.putString( NBT_PAGE_TITLE, pageTitle ); + page.writeToNBT( nbt ); + } + + // Write inventory + Inventories.writeNbt( nbt, inventory ); + + return super.writeNbt( nbt ); + } + + boolean isPrinting() + { + return printing; + } + + // IInventory implementation + @Override + public int size() + { + return inventory.size(); + } + + @Override + public boolean isEmpty() + { + for( ItemStack stack : inventory ) + { + if( !stack.isEmpty() ) + { + return false; + } + } + return true; + } + + @Nonnull + @Override + public ItemStack getStack( int slot ) + { + return inventory.get( slot ); + } + + @Nonnull + @Override + public ItemStack removeStack( int slot, int count ) + { + ItemStack stack = 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( inventory.get( slot ) + .isEmpty() ) + { + inventory.set( slot, ItemStack.EMPTY ); + updateBlockState(); + } + markDirty(); + return part; + } + + @Nonnull + @Override + public ItemStack removeStack( int slot ) + { + ItemStack result = inventory.get( slot ); + inventory.set( slot, ItemStack.EMPTY ); + markDirty(); + updateBlockState(); + return result; + } + + // ISidedInventory implementation + + @Override + public void setStack( int slot, @Nonnull ItemStack stack ) + { + inventory.set( slot, stack ); + markDirty(); + updateBlockState(); + } + + @Override + public boolean canPlayerUse( @Nonnull PlayerEntity playerEntity ) + { + return isUsable( playerEntity, false ); + } + + @Override + public void clear() + { + for( int i = 0; i < inventory.size(); i++ ) + { + 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; + } + } + + static boolean isInk( @Nonnull ItemStack stack ) + { + return ColourUtils.getStackColour( stack ) != null; + } + + @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; + } + } + + @Nonnull + @Override + public IPeripheral getPeripheral( Direction side ) + { + return new PrinterPeripheral( this ); + } + + @Nullable + Terminal getCurrentPage() + { + synchronized( page ) + { + return printing ? page : null; + } + } + + boolean startNewPage() + { + synchronized( page ) + { + if( !canInputPage() ) + { + return false; + } + if( printing && !outputPage() ) + { + return false; + } + return inputPage(); + } + } + + boolean endCurrentPage() + { + synchronized( page ) + { + return printing && outputPage(); + } + } + + private boolean outputPage() + { + int height = page.getHeight(); + String[] lines = new String[height]; + String[] colours = new String[height]; + for( int i = 0; i < height; i++ ) + { + lines[i] = page.getLine( i ) + .toString(); + colours[i] = page.getTextColourLine( i ) + .toString(); + } + + ItemStack stack = ItemPrintout.createSingleFromTitleAndText( pageTitle, lines, colours ); + for( int slot : BOTTOM_SLOTS ) + { + if( inventory.get( slot ) + .isEmpty() ) + { + setStack( slot, stack ); + printing = false; + return true; + } + } + return false; + } + + int getInkLevel() + { + ItemStack inkStack = inventory.get( 0 ); + return isInk( inkStack ) ? inkStack.getCount() : 0; + } + + int getPaperLevel() + { + int count = 0; + for( int i = 1; i < 7; i++ ) + { + ItemStack paperStack = inventory.get( i ); + if( isPaper( paperStack ) ) + { + count += paperStack.getCount(); + } + } + return count; + } + + void setPageTitle( String title ) + { + synchronized( page ) + { + if( printing ) + { + pageTitle = title; + } + } + } + + private boolean canInputPage() + { + ItemStack inkStack = inventory.get( 0 ); + return !inkStack.isEmpty() && isInk( inkStack ) && getPaperLevel() > 0; + } + + private boolean inputPage() + { + ItemStack inkStack = inventory.get( 0 ); + DyeColor dye = ColourUtils.getStackColour( inkStack ); + if( dye == null ) return false; + + for( int i = 1; i < 7; i++ ) + { + ItemStack paperStack = inventory.get( i ); + if( paperStack.isEmpty() || !isPaper( paperStack ) ) + { + continue; + } + + // Setup the new page + page.setTextColour( dye.getId() ); + + page.clear(); + if( paperStack.getItem() instanceof ItemPrintout ) + { + pageTitle = ItemPrintout.getTitle( paperStack ); + String[] text = ItemPrintout.getText( paperStack ); + String[] textColour = ItemPrintout.getColours( paperStack ); + for( int y = 0; y < page.getHeight(); y++ ) + { + page.setLine( y, text[y], textColour[y], "" ); + } + } + else + { + pageTitle = ""; + } + page.setCursorPos( 0, 0 ); + + // Decrement ink + inkStack.decrement( 1 ); + if( inkStack.isEmpty() ) + { + inventory.set( 0, ItemStack.EMPTY ); + } + + // Decrement paper + paperStack.decrement( 1 ); + if( paperStack.isEmpty() ) + { + inventory.set( i, ItemStack.EMPTY ); + updateBlockState(); + } + + markDirty(); + printing = true; + return true; + } + return false; + } + + @Nonnull + @Override + public Text getName() + { + return customName != null ? customName : new TranslatableText( getCachedState().getBlock() + .getTranslationKey() ); + } + + @Override + public boolean hasCustomName() + { + return customName != null; + } + + @Override + public Text getDisplayName() + { + return Nameable.super.getDisplayName(); + } + + @Nullable + @Override + public Text getCustomName() + { + return customName; + } + + @Nonnull + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory inventory, @Nonnull PlayerEntity player ) + { + return new ContainerPrinter( id, inventory, this ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/speaker/BlockSpeaker.java b/remappedSrc/dan200/computercraft/shared/peripheral/speaker/BlockSpeaker.java new file mode 100644 index 000000000..50d6c08cd --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/speaker/BlockSpeaker.java @@ -0,0 +1,46 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.speaker; + +import dan200.computercraft.shared.ComputerCraftRegistry; +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, ComputerCraftRegistry.ModTiles.SPEAKER ); + setDefaultState( getStateManager().getDefaultState() + .with( FACING, Direction.NORTH ) ); + } + + @Nullable + @Override + public BlockState getPlacementState( ItemPlacementContext placement ) + { + return getDefaultState().with( FACING, + placement.getPlayerFacing() + .getOpposite() ); + } + + @Override + protected void appendProperties( StateManager.Builder properties ) + { + properties.add( FACING ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java b/remappedSrc/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java new file mode 100644 index 000000000..c44653cbd --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java @@ -0,0 +1,180 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.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 dan200.computercraft.fabric.mixin.SoundEventAccess; +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 final AtomicInteger notesThisTick = new AtomicInteger(); + private long clock = 0; + private long lastPlayTime = 0; + + public void update() + { + clock++; + notesThisTick.set( 0 ); + } + + public boolean madeSound( long ticks ) + { + return clock - 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 ); + } + + private synchronized boolean playSound( ILuaContext context, Identifier name, float volume, float pitch, boolean isNote ) throws LuaException + { + if( clock - lastPlayTime < TileSpeaker.MIN_TICKS_BETWEEN_SOUNDS && (!isNote || clock - lastPlayTime != 0 || 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; + } ); + + lastPlayTime = clock; + return true; + } + + public abstract World getWorld(); + + public abstract Vec3d getPosition(); + + /** + * 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, + ((SoundEventAccess) instrument.getSound()).getId(), + volume, + (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ), + true ); + if( success ) + { + notesThisTick.incrementAndGet(); + } + return success; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java b/remappedSrc/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java new file mode 100644 index 000000000..08d1e8a41 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java @@ -0,0 +1,75 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.speaker; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IPeripheralTile; +import dan200.computercraft.shared.common.TileGeneric; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class TileSpeaker extends TileGeneric implements Tickable, IPeripheralTile +{ + public static final int MIN_TICKS_BETWEEN_SOUNDS = 1; + + private final SpeakerPeripheral peripheral; + + public TileSpeaker( BlockEntityType type ) + { + super( type ); + peripheral = new Peripheral( this ); + } + + @Override + public void tick() + { + peripheral.update(); + } + + @Nonnull + @Override + public IPeripheral getPeripheral( Direction side ) + { + return peripheral; + } + + 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/remappedSrc/dan200/computercraft/shared/pocket/apis/PocketAPI.java b/remappedSrc/dan200/computercraft/shared/pocket/apis/PocketAPI.java new file mode 100644 index 000000000..d14c9b4ec --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/pocket/apis/PocketAPI.java @@ -0,0 +1,168 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.ItemStorage; +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; + +/** + * 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, ItemStorage.wrap( inventory ), inventory.selectedSlot ); + if( !stack.isEmpty() ) + { + WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.getPos() ); + } + } + } + + // Set the new upgrade + computer.setUpgrade( newUpgrade ); + + 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; + } + + /** + * 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, ItemStorage.wrap( inventory ), inventory.selectedSlot ); + if( stack.isEmpty() ) + { + WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.getPos() ); + } + } + + return new Object[] { true }; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/pocket/core/PocketServerComputer.java b/remappedSrc/dan200/computercraft/shared/pocket/core/PocketServerComputer.java new file mode 100644 index 000000000..af643dfc4 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/pocket/core/PocketServerComputer.java @@ -0,0 +1,210 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 dan200.computercraft.shared.util.NBTUtil; +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.NbtCompound; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +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 upgrade; + private Entity entity; + private ItemStack 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 = this.entity; + if( entity == null || stack == null || !entity.isAlive() ) + { + return null; + } + + if( entity instanceof PlayerEntity ) + { + PlayerInventory inventory = ((PlayerEntity) entity).inventory; + return inventory.main.contains( stack ) || inventory.offHand.contains( stack ) ? entity : null; + } + else if( entity instanceof LivingEntity ) + { + LivingEntity living = (LivingEntity) entity; + return living.getMainHandStack() == stack || living.getOffHandStack() == stack ? entity : null; + } + else + { + return null; + } + } + + @Override + public int getColour() + { + return IColouredItem.getColourBasic( stack ); + } + + @Override + public void setColour( int colour ) + { + IColouredItem.setColourBasic( stack, colour ); + updateUpgradeNBTData(); + } + + @Override + public int getLight() + { + NbtCompound tag = getUserData(); + return tag.contains( NBT_LIGHT, NBTUtil.TAG_ANY_NUMERIC ) ? tag.getInt( NBT_LIGHT ) : -1; + } + + @Override + public void setLight( int colour ) + { + NbtCompound tag = getUserData(); + if( colour >= 0 && colour <= 0xFFFFFF ) + { + if( !tag.contains( NBT_LIGHT, NBTUtil.TAG_ANY_NUMERIC ) || tag.getInt( NBT_LIGHT ) != colour ) + { + tag.putInt( NBT_LIGHT, colour ); + updateUserData(); + } + } + else if( tag.contains( NBT_LIGHT, NBTUtil.TAG_ANY_NUMERIC ) ) + { + tag.remove( NBT_LIGHT ); + updateUserData(); + } + } + + @Nonnull + @Override + public NbtCompound getUpgradeNBTData() + { + return ItemPocketComputer.getUpgradeInfo( stack ); + } + + @Override + public void updateUpgradeNBTData() + { + if( entity instanceof PlayerEntity ) + { + ((PlayerEntity) entity).inventory.markDirty(); + } + } + + @Override + public void invalidatePeripheral() + { + IPeripheral peripheral = upgrade == null ? null : upgrade.createPeripheral( this ); + setPeripheral( ComputerSide.BACK, peripheral ); + } + + @Nonnull + @Override + public Map getUpgrades() + { + return upgrade == null ? Collections.emptyMap() : Collections.singletonMap( upgrade.getUpgradeID(), getPeripheral( ComputerSide.BACK ) ); + } + + public IPocketUpgrade getUpgrade() + { + return 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( this.upgrade == upgrade ) + { + return; + } + + synchronized( this ) + { + ItemPocketComputer.setUpgrade( stack, upgrade ); + updateUpgradeNBTData(); + this.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 != this.entity && entity instanceof ServerPlayerEntity ) + { + markTerminalChanged(); + } + + this.entity = entity; + this.stack = stack; + + if( this.upgrade != upgrade ) + { + this.upgrade = upgrade; + invalidatePeripheral(); + } + } + + @Override + public void broadcastState( boolean force ) + { + super.broadcastState( force ); + + if( (hasTerminalChanged() || force) && entity instanceof ServerPlayerEntity ) + { + // Broadcast the state to the current entity if they're not already interacting with it. + ServerPlayerEntity player = (ServerPlayerEntity) entity; + if( player.networkHandler != null && !isInteracting( player ) ) + { + NetworkHandler.sendToPlayer( player, createTerminalPacket() ); + } + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java b/remappedSrc/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java new file mode 100644 index 000000000..8149d9199 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java @@ -0,0 +1,78 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.pocket.inventory; + +import dan200.computercraft.shared.ComputerCraftRegistry; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.computer.inventory.ContainerComputerBase; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.server.network.ServerPlayerEntity; +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( ComputerCraftRegistry.ModContainers.POCKET_COMPUTER, id, p -> { + ItemStack stack = p.getStackInHand( hand ); + return stack.getItem() == item && ItemPocketComputer.getServerComputer( stack ) == computer; + }, computer, item.getFamily() ); + } + + public ContainerPocketComputer( int id, PlayerInventory player, PacketByteBuf packetByteBuf ) + { + super( ComputerCraftRegistry.ModContainers.POCKET_COMPUTER, id, player, packetByteBuf ); + } + + public static class Factory implements ExtendedScreenHandlerFactory + { + 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; + 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 ); + } + + @Override + public void writeScreenOpeningData( ServerPlayerEntity serverPlayerEntity, PacketByteBuf packetByteBuf ) + { + packetByteBuf.writeInt( computer.getInstanceID() ); + packetByteBuf.writeEnumConstant( computer.getFamily() ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java b/remappedSrc/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java new file mode 100644 index 000000000..ed349658d --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java @@ -0,0 +1,437 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.NbtCompound; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; + +public class ItemPocketComputer extends Item implements IComputerItem, IMedia, IColouredItem +{ + public static final String NBT_LIGHT = "Light"; + private static final String NBT_UPGRADE = "Upgrade"; + private static final String NBT_UPGRADE_INFO = "UpgradeInfo"; + 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 static ServerComputer getServerComputer( @Nonnull ItemStack stack ) + { + int session = getSessionID( stack ); + if( session != ComputerCraft.serverComputerRegistry.getSessionID() ) return null; + + int instanceID = getInstanceID( stack ); + return instanceID >= 0 ? ComputerCraft.serverComputerRegistry.get( instanceID ) : null; + } + + @Environment( EnvType.CLIENT ) + public static ComputerState getState( @Nonnull ItemStack stack ) + { + ClientComputer computer = getClientComputer( stack ); + return computer == null ? ComputerState.OFF : computer.getState(); + } + + private static ClientComputer getClientComputer( @Nonnull ItemStack stack ) + { + int instanceID = getInstanceID( stack ); + return instanceID >= 0 ? ComputerCraft.clientComputerRegistry.get( instanceID ) : null; + } + + @Environment( EnvType.CLIENT ) + public static int getLightState( @Nonnull ItemStack stack ) + { + ClientComputer computer = getClientComputer( stack ); + if( computer != null && computer.isOn() ) + { + NbtCompound computerNBT = computer.getUserData(); + if( computerNBT != null && computerNBT.contains( NBT_LIGHT ) ) + { + return computerNBT.getInt( NBT_LIGHT ); + } + } + return -1; + } + + public static void setUpgrade( @Nonnull ItemStack stack, IPocketUpgrade upgrade ) + { + NbtCompound compound = stack.getOrCreateNbt(); + + if( upgrade == null ) + { + compound.remove( NBT_UPGRADE ); + } + else + { + compound.putString( NBT_UPGRADE, + upgrade.getUpgradeID() + .toString() ); + } + + compound.remove( NBT_UPGRADE_INFO ); + } + + public static NbtCompound getUpgradeInfo( @Nonnull ItemStack stack ) + { + return stack.getOrCreateSubNbt( NBT_UPGRADE_INFO ); + } + + // @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 ); + // } + + @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 ) + { + computer.sendTerminalState( player ); + new ComputerContainerData( computer ).open( player, new ContainerPocketComputer.Factory( computer, stack, this, hand ) ); + } + } + return new TypedActionResult<>( ActionResult.SUCCESS, stack ); + } + + @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 ); + } + } + + @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 ) ); + } + } + } + + @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 ); + } + } + + // IComputerItem implementation + + @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 ) ); + } + } + + public ItemStack create( int id, String label, int colour, IPocketUpgrade upgrade ) + { + ItemStack result = new ItemStack( this ); + if( id >= 0 ) + { + result.getOrCreateNbt() + .putInt( NBT_ID, id ); + } + if( label != null ) + { + result.setCustomName( new LiteralText( label ) ); + } + if( upgrade != null ) + { + result.getOrCreateNbt() + .putString( NBT_UPGRADE, + upgrade.getUpgradeID() + .toString() ); + } + if( colour != -1 ) + { + result.getOrCreateNbt() + .putInt( NBT_COLOUR, colour ); + } + return result; + } + + 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 IPocketUpgrade getUpgrade( @Nonnull ItemStack stack ) + { + NbtCompound compound = stack.getNbt(); + return compound != null && compound.contains( NBT_UPGRADE ) ? PocketUpgrades.get( compound.getString( NBT_UPGRADE ) ) : null; + + } + + // IMedia + + private static void setComputerID( @Nonnull ItemStack stack, int computerID ) + { + stack.getOrCreateNbt() + .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 ) ); + } + + @Override + public boolean setLabel( @Nonnull ItemStack stack, String label ) + { + if( label != null ) + { + stack.setCustomName( new LiteralText( label ) ); + } + else + { + stack.removeCustomName(); + } + return true; + } + + 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 int getInstanceID( @Nonnull ItemStack stack ) + { + NbtCompound nbt = stack.getNbt(); + return nbt != null && nbt.contains( NBT_INSTANCE ) ? nbt.getInt( NBT_INSTANCE ) : -1; + } + + private static int getSessionID( @Nonnull ItemStack stack ) + { + NbtCompound nbt = stack.getNbt(); + return nbt != null && nbt.contains( NBT_SESSION ) ? nbt.getInt( NBT_SESSION ) : -1; + } + + private static void setInstanceID( @Nonnull ItemStack stack, int instanceID ) + { + stack.getOrCreateNbt() + .putInt( NBT_INSTANCE, instanceID ); + } + + private static void setSessionID( @Nonnull ItemStack stack, int sessionID ) + { + stack.getOrCreateNbt() + .putInt( NBT_SESSION, sessionID ); + } + + @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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/pocket/items/PocketComputerItemFactory.java b/remappedSrc/dan200/computercraft/shared/pocket/items/PocketComputerItemFactory.java new file mode 100644 index 000000000..ac2d7303d --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/pocket/items/PocketComputerItemFactory.java @@ -0,0 +1,33 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.pocket.items; + +import dan200.computercraft.api.pocket.IPocketUpgrade; +import dan200.computercraft.shared.ComputerCraftRegistry; +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 ComputerCraftRegistry.ModItems.POCKET_COMPUTER_NORMAL.create( id, label, colour, upgrade ); + case ADVANCED: + return ComputerCraftRegistry.ModItems.POCKET_COMPUTER_ADVANCED.create( id, label, colour, upgrade ); + default: + return ItemStack.EMPTY; + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketModem.java b/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketModem.java new file mode 100644 index 000000000..6ba68d559 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketModem.java @@ -0,0 +1,61 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.pocket.peripherals; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.pocket.AbstractPocketUpgrade; +import dan200.computercraft.api.pocket.IPocketAccess; +import dan200.computercraft.shared.ComputerCraftRegistry; +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 ? ComputerCraftRegistry.ModBlocks.WIRELESS_MODEM_ADVANCED : ComputerCraftRegistry.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/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketModemPeripheral.java b/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketModemPeripheral.java new file mode 100644 index 000000000..d43c860b4 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketModemPeripheral.java @@ -0,0 +1,52 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketSpeaker.java b/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketSpeaker.java new file mode 100644 index 000000000..b5c98c7ad --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketSpeaker.java @@ -0,0 +1,52 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.pocket.peripherals; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.pocket.AbstractPocketUpgrade; +import dan200.computercraft.api.pocket.IPocketAccess; +import dan200.computercraft.shared.ComputerCraftRegistry; +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" ), ComputerCraftRegistry.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/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java b/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java new file mode 100644 index 000000000..dcec95bb9 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java @@ -0,0 +1,42 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java b/remappedSrc/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java new file mode 100644 index 000000000..279a2ad1c --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java @@ -0,0 +1,133 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 +{ + public static final RecipeSerializer SERIALIZER = new SpecialRecipeSerializer<>( PocketComputerUpgradeRecipe::new ); + + private PocketComputerUpgradeRecipe( Identifier identifier ) + { + super( identifier ); + } + + @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 !craft( inventory ).isEmpty(); + } + + @Nonnull + @Override + public ItemStack craft( @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 ); + } + + @Override + public boolean fits( int x, int y ) + { + return x >= 2 && y >= 2; + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java b/remappedSrc/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java new file mode 100644 index 000000000..cf16b9904 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java @@ -0,0 +1,146 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.proxy; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.media.IMedia; +import dan200.computercraft.api.peripheral.IPeripheralTile; +import dan200.computercraft.api.turtle.event.TurtleEvent; +import dan200.computercraft.core.computer.MainThread; +import dan200.computercraft.core.tracking.Tracking; +import dan200.computercraft.shared.TurtlePermissions; +import dan200.computercraft.shared.command.CommandComputerCraft; +import dan200.computercraft.shared.command.arguments.ArgumentSerializers; +import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider; +import dan200.computercraft.shared.common.TileGeneric; +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.commandblock.CommandBlockPeripheral; +import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods; +import dan200.computercraft.shared.peripheral.modem.wireless.WirelessNetwork; +import dan200.computercraft.shared.turtle.FurnaceRefuelHandler; +import dan200.computercraft.shared.turtle.SignInspectHandler; +import dan200.computercraft.shared.util.Config; +import dan200.computercraft.shared.util.TickScheduler; +import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerBlockEntityEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.CommandBlockBlockEntity; +import net.minecraft.item.Item; +import net.minecraft.item.MusicDiscItem; +import net.minecraft.loot.condition.LootConditionType; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +public final class ComputerCraftProxyCommon +{ + private static MinecraftServer server; + + public static void init() + { + NetworkHandler.setup(); + + registerProviders(); + registerHandlers(); + + ArgumentSerializers.register(); + + ComputerCraftAPI.registerGenericSource( new InventoryMethods() ); + } + + private static void registerProviders() + { + ComputerCraftAPI.registerPeripheralProvider( ( world, pos, side ) -> { + BlockEntity tile = world.getBlockEntity( pos ); + return tile instanceof IPeripheralTile ? ((IPeripheralTile) tile).getPeripheral( side ) : null; + } ); + + ComputerCraftAPI.registerPeripheralProvider( ( world, pos, side ) -> { + BlockEntity tile = world.getBlockEntity( pos ); + return ComputerCraft.enableCommandBlock && tile instanceof CommandBlockBlockEntity ? + new CommandBlockPeripheral( (CommandBlockBlockEntity) tile ) : null; + } ); + + // 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; + } ); + } + + private static void registerHandlers() + { + CommandRegistrationCallback.EVENT.register( CommandComputerCraft::register ); + + ServerTickEvents.START_SERVER_TICK.register( server -> { + MainThread.executePendingTasks(); + ComputerCraft.serverComputerRegistry.update(); + TickScheduler.tick(); + } ); + + ServerLifecycleEvents.SERVER_STARTED.register( server -> { + ComputerCraftProxyCommon.server = server; + ComputerCraft.serverComputerRegistry.reset(); + WirelessNetwork.resetNetworks(); + MainThread.reset(); + Tracking.reset(); + } ); + + ServerLifecycleEvents.SERVER_STOPPING.register( server -> { + ComputerCraft.serverComputerRegistry.reset(); + WirelessNetwork.resetNetworks(); + MainThread.reset(); + Tracking.reset(); + ComputerCraftProxyCommon.server = null; + } ); + + ServerBlockEntityEvents.BLOCK_ENTITY_UNLOAD.register( ( blockEntity, world ) -> { + if( blockEntity instanceof TileGeneric ) + { + ((TileGeneric) blockEntity).onChunkUnloaded(); + } + } ); + + // Config + ServerLifecycleEvents.SERVER_STARTING.register( Config::serverStarting ); + ServerLifecycleEvents.SERVER_STOPPING.register( Config::serverStopping ); + + TurtleEvent.EVENT_BUS.register( FurnaceRefuelHandler.INSTANCE ); + TurtleEvent.EVENT_BUS.register( new TurtlePermissions() ); + TurtleEvent.EVENT_BUS.register( new SignInspectHandler() ); + } + + 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 ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java b/remappedSrc/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java new file mode 100644 index 000000000..6ca4aaadd --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java @@ -0,0 +1,70 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.turtle; + +import com.google.common.eventbus.Subscribe; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.event.TurtleRefuelEvent; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.ItemStorage; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.block.entity.FurnaceBlockEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; + +public final class FurnaceRefuelHandler implements TurtleRefuelEvent.Handler +{ + public static final FurnaceRefuelHandler INSTANCE = new FurnaceRefuelHandler(); + + private FurnaceRefuelHandler() + { + } + + @Subscribe + public static void onTurtleRefuel( TurtleRefuelEvent event ) + { + if( event.getHandler() == null && getFuelPerItem( event.getStack() ) > 0 ) + { + event.setHandler( INSTANCE ); + } + } + + @Override + public int refuel( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack currentStack, int slot, int limit ) + { + ItemStorage storage = ItemStorage.wrap( turtle.getInventory() ); + ItemStack stack = storage.take( slot, limit, ItemStack.EMPTY, false ); + int fuelToGive = getFuelPerItem( stack ) * stack.getCount(); + + // Store the replacement item in the inventory + Item replacementStack = stack.getItem() + .getRecipeRemainder(); + if( replacementStack != null ) + { + ItemStack remainder = InventoryUtil.storeItems( new ItemStack( replacementStack ), storage, turtle.getSelectedSlot() ); + if( !remainder.isEmpty() ) + { + WorldUtil.dropItemStack( remainder, + turtle.getWorld(), + turtle.getPosition(), + turtle.getDirection() + .getOpposite() ); + } + } + + return fuelToGive; + } + + private static int getFuelPerItem( @Nonnull ItemStack stack ) + { + int burnTime = FurnaceBlockEntity.createFuelTimeMap() + .getOrDefault( stack.getItem(), 0 ); + return (burnTime * 5) / 100; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/SignInspectHandler.java b/remappedSrc/dan200/computercraft/shared/turtle/SignInspectHandler.java new file mode 100644 index 000000000..83308b347 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/SignInspectHandler.java @@ -0,0 +1,34 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.turtle; + +import com.google.common.eventbus.Subscribe; +import dan200.computercraft.api.turtle.event.TurtleBlockEvent; +import dan200.computercraft.fabric.mixin.SignBlockEntityAccess; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.SignBlockEntity; + +import java.util.HashMap; +import java.util.Map; + +public class SignInspectHandler +{ + @Subscribe + public void onTurtleInspect( TurtleBlockEvent.Inspect event ) + { + BlockEntity be = event.getWorld().getBlockEntity( event.getPos() ); + if( be instanceof SignBlockEntity ) + { + SignBlockEntity sbe = (SignBlockEntity) be; + Map textTable = new HashMap<>(); + for( int k = 0; k < 4; k++ ) + { + textTable.put( k + 1, ((SignBlockEntityAccess) sbe).getText()[k].asString() ); + } + event.getData().put( "text", textTable ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/remappedSrc/dan200/computercraft/shared/turtle/apis/TurtleAPI.java new file mode 100644 index 000000000..801f62d12 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -0,0 +1,784 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.TurtleEvent; +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 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" }; + } + + /** + * 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 ) ); + } + + private MethodResult trackCommand( ITurtleCommand command ) + { + environment.addTrackingChange( TrackingField.TURTLE_OPS ); + return turtle.executeCommand( command ); + } + + /** + * 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. + * + * "Placing" an item allows it to interact with blocks and entities in front of the turtle. For instance, buckets + * can pick up and place down fluids, and wheat can be used to breed cows. However, you cannot use {@link #place} to + * perform arbitrary block interactions, such as clicking buttons or flipping levers. + * + * @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. + * @see #place For more information about placing items. + */ + @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. + * @see #place For more information about placing items. + */ + @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 ) ) ); + } + + 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; + } + + /** + * 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(); + } ); + } + + 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; + } + + /** + * 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(); + } + + private static Optional checkSlot( Optional slot ) throws LuaException + { + return slot.isPresent() ? Optional.of( checkSlot( slot.get() ) ) : Optional.empty(); + } + + /** + * 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 ) ); + } + + /** + * Check if the block in front of the turtle is equal to the item in the currently selected slot. + * + * @return If the block and item are equal. + * @cc.treturn boolean If the block and item are equal. + */ + @LuaFunction + public final MethodResult compare() + { + return trackCommand( new TurtleCompareCommand( InteractDirection.FORWARD ) ); + } + + /** + * Check if the block above the turtle is equal to the item in the currently selected slot. + * + * @return If the block and item are equal. + * @cc.treturn boolean If the block and item are equal. + */ + @LuaFunction + public final MethodResult compareUp() + { + return trackCommand( new TurtleCompareCommand( InteractDirection.UP ) ); + } + + /** + * Check if the block below the turtle is equal to the item in the currently selected slot. + * + * @return If the block and item are equal. + * @cc.treturn boolean If the block and item are equal. + */ + @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 ) ) ); + } + + /** + * Get the maximum amount of fuel this turtle currently holds. + * + * @return The fuel level, or "unlimited". + * @cc.treturn [1] number The current amount of fuel a turtle this turtle has. + * @cc.treturn [2] "unlimited" If turtles do not consume fuel when moving. + * @see #getFuelLimit() + * @see #refuel(Optional) + */ + @LuaFunction + public final Object getFuelLevel() + { + return turtle.isFuelNeeded() ? turtle.getFuelLevel() : "unlimited"; + } + + /** + * Refuel this turtle. + * + * While most actions a turtle can perform (such as digging or placing blocks), moving consumes fuel from the + * turtle's internal buffer. If a turtle has no fuel, it will not move. + * + * {@link #refuel} refuels the turtle, consuming fuel items (such as coal or lava buckets) from the currently + * selected slot and converting them into energy. This finishes once the turtle is fully refuelled or all items have + * been consumed. + * + * @param countA The maximum number of items to consume. One can pass `0` to check if an item is combustable or not. + * @return If this turtle could be refuelled. + * @throws LuaException If the refuel count is out of range. + * @cc.treturn [1] true If the turtle was refuelled. + * @cc.treturn [2] false If the turtle was not refuelled. + * @cc.treturn [2] string The reason the turtle was not refuelled ( + * @cc.usage Refuel a turtle from the currently selected slot. + *
{@code
+     * local level = turtle.getFuelLevel()
+     * if new_level == "unlimited" then error("Turtle does not need fuel", 0) end
+     *
+     * local ok, err = turtle.refuel()
+     * if ok then
+     *   local new_level = turtle.getFuelLevel()
+     *   print(("Refuelled %d, current level is %d"):format(new_level - level, new_level))
+     * else
+     *   printError(err)
+     * end}
+ * @cc.usage Check if the current item is a valid fuel source. + *
{@code
+     * local is_fuel, reason = turtle.refuel(0)
+     * if not is_fuel then printError(reason) end
+     * }
+ * @see #getFuelLevel() + * @see #getFuelLimit() + */ + @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 ) ); + } + + /** + * Compare the item in the currently selected slot to the item in another slot. + * + * @param slot The slot to compare to. + * @return If the items are the same. + * @throws LuaException If the slot is out of range. + * @cc.treturn boolean If the two items are equal. + */ + @LuaFunction + public final MethodResult compareTo( int slot ) throws LuaException + { + return trackCommand( new TurtleCompareToCommand( checkSlot( slot ) ) ); + } + + /** + * Move an item from the selected slot to another one. + * + * @param slotArg The slot to move this item to. + * @param countArg The maximum number of items to move. + * @return If the item was moved or not. + * @throws LuaException If the slot is out of range. + * @throws LuaException If the number of items is out of range. + * @cc.treturn boolean If some items were successfully moved. + */ + @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 selected slot. + * + * @return The current slot. + * @see #select + */ + @LuaFunction + public final int getSelectedSlot() + { + return turtle.getSelectedSlot() + 1; + } + + /** + * Get the maximum amount of fuel this turtle can hold. + * + * By default, normal turtles have a limit of 20,000 and advanced turtles of 100,000. + * + * @return The limit, or "unlimited". + * @cc.treturn [1] number The maximum amount of fuel a turtle can hold. + * @cc.treturn [2] "unlimited" If turtles do not consume fuel when moving. + * @see #getFuelLevel() + * @see #refuel(Optional) + */ + @LuaFunction + public final Object getFuelLimit() + { + return turtle.isFuelNeeded() ? turtle.getFuelLimit() : "unlimited"; + } + + /** + * Equip (or unequip) an item on the left side of this turtle. + * + * This finds the item in the currently selected slot and attempts to equip it to the left side of the turtle. The + * previous upgrade is removed and placed into the turtle's inventory. If there is no item in the slot, the previous + * upgrade is removed, but no new one is equipped. + * + * @return Whether an item was equiped or not. + * @cc.treturn [1] true If the item was equipped. + * @cc.treturn [2] false If we could not equip the item. + * @cc.treturn [2] string The reason equipping this item failed. + * @see #equipRight() + */ + @LuaFunction + public final MethodResult equipLeft() + { + return trackCommand( new TurtleEquipCommand( TurtleSide.LEFT ) ); + } + + /** + * Equip (or unequip) an item on the right side of this turtle. + * + * This finds the item in the currently selected slot and attempts to equip it to the right side of the turtle. The + * previous upgrade is removed and placed into the turtle's inventory. If there is no item in the slot, the previous + * upgrade is removed, but no new one is equipped. + * + * @return Whether an item was equiped or not. + * @cc.treturn [1] true If the item was equipped. + * @cc.treturn [2] false If we could not equip the item. + * @cc.treturn [2] string The reason equipping this item failed. + * @see #equipRight() + */ + @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. + * @cc.usage
{@code
+     * local has_block, data = turtle.inspect()
+     * if has_block then
+     *   print(textutils.serialize(data))
+     *   -- {
+     *   --   name = "minecraft:oak_log",
+     *   --   state = { axis = "x" },
+     *   --   tags = { ["minecraft:logs"] = true, ... },
+     *   -- }
+     * else
+     *   print("No block in front of the turtle")
+     * end}
+ */ + @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,
+     *     -- }
+     *     }
+ * @see InventoryMethods#getItemDetail Describes the information returned by a detailed query. + */ + @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( TurtleEvent.post( event ) ) + { + return new Object[] { false, event.getFailureMessage() }; + } + + return new Object[] { table }; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java b/remappedSrc/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java new file mode 100644 index 000000000..bff000580 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java @@ -0,0 +1,179 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.*; +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.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 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, BlockEntityType type ) + { + super( settings, family, type ); + setDefaultState( getStateManager().getDefaultState() + .with( FACING, Direction.NORTH ) + .with( WATERLOGGED, false ) ); + } + + @Nonnull + @Override + @Deprecated + public BlockRenderType getRenderType( @Nonnull BlockState state ) + { + return BlockRenderType.ENTITYBLOCK_ANIMATED; + } + + @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; + } + + @Nonnull + @Override + @Deprecated + public FluidState getFluidState( @Nonnull BlockState state ) + { + return getWaterloggedFluidState( state ); + } + + @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 ); + } + + @Override + public float getBlastResistance() + { + // TODO Implement below functionality + return 2000; + } + + @Nullable + @Override + public BlockState getPlacementState( ItemPlacementContext placement ) + { + return getDefaultState().with( FACING, placement.getPlayerFacing() ) + .with( WATERLOGGED, getWaterloggedStateForPlacement( placement ) ); + } + + @Override + protected void appendProperties( StateManager.Builder builder ) + { + builder.add( FACING, WATERLOGGED ); + } + + @Nonnull + @Override + protected ItemStack getItem( TileComputerBase tile ) + { + return tile instanceof TileTurtle ? TurtleItemFactory.create( (TileTurtle) tile ) : ItemStack.EMPTY; + } + + // @Override + // public float getBlastResistance( 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 ); + // } + + @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 ); + } + } + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/blocks/ITurtleTile.java b/remappedSrc/dan200/computercraft/shared/turtle/blocks/ITurtleTile.java new file mode 100644 index 000000000..38224df84 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/blocks/ITurtleTile.java @@ -0,0 +1,31 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/turtle/blocks/TileTurtle.java b/remappedSrc/dan200/computercraft/shared/turtle/blocks/TileTurtle.java new file mode 100644 index 000000000..74107bed7 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/blocks/TileTurtle.java @@ -0,0 +1,569 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.turtle.blocks; + +import com.mojang.authlib.GameProfile; +import dan200.computercraft.ComputerCraft; +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.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.NbtCompound; +import net.minecraft.nbt.NbtList; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.util.ActionResult; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; + +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; + private final DefaultedList inventory = DefaultedList.ofSize( INVENTORY_SIZE, ItemStack.EMPTY ); + private final DefaultedList previousInventory = DefaultedList.ofSize( INVENTORY_SIZE, ItemStack.EMPTY ); + private boolean inventoryChanged = false; + private TurtleBrain brain = new TurtleBrain( this ); + private MoveState moveState = MoveState.NOT_MOVED; + + public TileTurtle( BlockEntityType type, ComputerFamily family ) + { + super( type, family ); + } + + @Override + protected void unload() + { + if( !hasMoved() ) + { + super.unload(); + } + } + + @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 ); + } + } + } + + private boolean hasMoved() + { + return moveState == MoveState.MOVED; + } + + @Override + public int size() + { + return INVENTORY_SIZE; + } + + @Override + public boolean isEmpty() + { + for( ItemStack stack : inventory ) + { + if( !stack.isEmpty() ) + { + return false; + } + } + return true; + } + + @Nonnull + @Override + public ItemStack getStack( int slot ) + { + return slot >= 0 && slot < INVENTORY_SIZE ? inventory.get( slot ) : ItemStack.EMPTY; + } + + @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; + } + + @Nonnull + @Override + public ItemStack removeStack( int slot ) + { + ItemStack result = getStack( slot ); + setStack( slot, ItemStack.EMPTY ); + return result; + } + + @Override + public void setStack( int i, @Nonnull ItemStack stack ) + { + if( i >= 0 && i < INVENTORY_SIZE && !InventoryUtil.areItemsEqual( stack, inventory.get( i ) ) ) + { + inventory.set( i, stack ); + onInventoryDefinitelyChanged(); + } + } + + @Override + public boolean canPlayerUse( @Nonnull PlayerEntity player ) + { + return isUsable( player, false ); + } + + private void onInventoryDefinitelyChanged() + { + super.markDirty(); + inventoryChanged = true; + } + + @Override + protected boolean canNameWithTag( PlayerEntity player ) + { + return true; + } + + @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( brain.getDyeColour() != dye ) + { + brain.setDyeColour( dye ); + if( !player.isCreative() ) + { + currentItem.decrement( 1 ); + } + } + } + return ActionResult.SUCCESS; + } + else if( currentItem.getItem() == Items.WATER_BUCKET && brain.getColour() != -1 ) + { + // Water to remove turtle colour + if( !getWorld().isClient ) + { + if( brain.getColour() != -1 ) + { + 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 + public void onNeighbourChange( @Nonnull BlockPos neighbour ) + { + if( moveState == MoveState.NOT_MOVED ) + { + super.onNeighbourChange( neighbour ); + } + } + + @Override + public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour ) + { + if( moveState == MoveState.NOT_MOVED ) + { + super.onNeighbourTileEntityChange( neighbour ); + } + } + + @Override + public void tick() + { + super.tick(); + brain.update(); + if( !getWorld().isClient && inventoryChanged ) + { + ServerComputer computer = getServerComputer(); + if( computer != null ) + { + computer.queueEvent( "turtle_inventory" ); + } + + inventoryChanged = false; + for( int n = 0; n < size(); n++ ) + { + previousInventory.set( n, + getStack( n ).copy() ); + } + } + } + + @Override + protected void updateBlockState( ComputerState newState ) + { + } + + @Nonnull + @Override + public NbtCompound writeNbt( @Nonnull NbtCompound nbt ) + { + // Write inventory + NbtList nbttaglist = new NbtList(); + for( int i = 0; i < INVENTORY_SIZE; i++ ) + { + if( !inventory.get( i ) + .isEmpty() ) + { + NbtCompound tag = new NbtCompound(); + tag.putByte( "Slot", (byte) i ); + inventory.get( i ) + .writeNbt( tag ); + nbttaglist.add( tag ); + } + } + nbt.put( "Items", nbttaglist ); + + // Write brain + nbt = brain.writeToNBT( nbt ); + + return super.writeNbt( nbt ); + } + + // IDirectionalTile + + @Override + public void readNbt( @Nonnull BlockState state, @Nonnull NbtCompound nbt ) + { + super.readNbt( state, nbt ); + + // Read inventory + NbtList nbttaglist = nbt.getList( "Items", NBTUtil.TAG_COMPOUND ); + inventory.clear(); + previousInventory.clear(); + for( int i = 0; i < nbttaglist.size(); i++ ) + { + NbtCompound tag = nbttaglist.getCompound( i ); + int slot = tag.getByte( "Slot" ) & 0xff; + if( slot < size() ) + { + inventory.set( slot, ItemStack.fromNbt( tag ) ); + previousInventory.set( slot, inventory.get( slot ) + .copy() ); + } + } + + // Read state + brain.readFromNBT( nbt ); + } + + @Override + protected boolean isPeripheralBlockedOnSide( ComputerSide localSide ) + { + return hasPeripheralUpgradeOnSide( localSide ); + } + + // ITurtleTile + + @Override + public Direction getDirection() + { + return getCachedState().get( BlockTurtle.FACING ); + } + + @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() ) ); + brain.setupComputer( computer ); + return computer; + } + + @Override + protected void writeDescription( @Nonnull NbtCompound nbt ) + { + super.writeDescription( nbt ); + brain.writeDescription( nbt ); + } + + @Override + protected void readDescription( @Nonnull NbtCompound nbt ) + { + super.readDescription( nbt ); + brain.readDescription( nbt ); + } + + @Override + public ComputerProxy createProxy() + { + return brain.getProxy(); + } + + 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(); + } + + public void onTileEntityChange() + { + super.markDirty(); + } + + 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(); + } + + // IInventory + + @Override + protected double getInteractRange( PlayerEntity player ) + { + return 12.0; + } + + public void notifyMoveStart() + { + if( moveState == MoveState.NOT_MOVED ) + { + moveState = MoveState.IN_PROGRESS; + } + } + + public void notifyMoveEnd() + { + // MoveState.MOVED is final + if( moveState == MoveState.IN_PROGRESS ) + { + moveState = MoveState.NOT_MOVED; + } + } + + @Override + public int getColour() + { + return brain.getColour(); + } + + @Override + public Identifier getOverlay() + { + return brain.getOverlay(); + } + + @Override + public ITurtleUpgrade getUpgrade( TurtleSide side ) + { + return brain.getUpgrade( side ); + } + + @Override + public ITurtleAccess getAccess() + { + return brain; + } + + @Override + public Vec3d getRenderOffset( float f ) + { + return brain.getRenderOffset( f ); + } + + @Override + public float getRenderYaw( float f ) + { + return brain.getVisualYaw( f ); + } + + @Override + public float getToolRenderAngle( TurtleSide side, float f ) + { + return brain.getToolRenderAngle( side, f ); + } + + void setOwningPlayer( GameProfile player ) + { + brain.setOwningPlayer( player ); + markDirty(); + } + + // Networking stuff + + @Override + public void markDirty() + { + super.markDirty(); + if( !inventoryChanged ) + { + for( int n = 0; n < size(); n++ ) + { + if( !ItemStack.areEqual( getStack( n ), previousInventory.get( n ) ) ) + { + inventoryChanged = true; + break; + } + } + } + } + + @Override + public void clear() + { + boolean changed = false; + for( int i = 0; i < INVENTORY_SIZE; i++ ) + { + if( !inventory.get( i ) + .isEmpty() ) + { + inventory.set( i, ItemStack.EMPTY ); + changed = true; + } + } + + if( changed ) + { + onInventoryDefinitelyChanged(); + } + } + + // Privates + + public void transferStateFrom( TileTurtle copy ) + { + super.transferStateFrom( copy ); + Collections.copy( inventory, copy.inventory ); + Collections.copy( previousInventory, copy.previousInventory ); + inventoryChanged = copy.inventoryChanged; + brain = copy.brain; + brain.setOwner( this ); + + // Mark the other turtle as having moved, and so its peripheral is dead. + copy.moveState = MoveState.MOVED; + } + + @Nullable + @Override + public ScreenHandler createMenu( int id, @Nonnull PlayerInventory inventory, @Nonnull PlayerEntity player ) + { + return new ContainerTurtle( id, inventory, brain ); + } + + enum MoveState + { + NOT_MOVED, IN_PROGRESS, MOVED + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/core/InteractDirection.java b/remappedSrc/dan200/computercraft/shared/turtle/core/InteractDirection.java new file mode 100644 index 000000000..2f22305e1 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/InteractDirection.java @@ -0,0 +1,29 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/turtle/core/MoveDirection.java b/remappedSrc/dan200/computercraft/shared/turtle/core/MoveDirection.java new file mode 100644 index 000000000..0364db549 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/MoveDirection.java @@ -0,0 +1,32 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/turtle/core/TurnDirection.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurnDirection.java new file mode 100644 index 000000000..becb48380 --- /dev/null +++ b/remappedSrc/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-2021. 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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleBrain.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleBrain.java new file mode 100644 index 000000000..bae0575a9 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleBrain.java @@ -0,0 +1,1011 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.*; +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.NbtCompound; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +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 final Queue commandQueue = new ArrayDeque<>(); + private final Map upgrades = new EnumMap<>( TurtleSide.class ); + private final Map peripherals = new EnumMap<>( TurtleSide.class ); + private final Map upgradeNBTData = new EnumMap<>( TurtleSide.class ); + TurtlePlayer cachedPlayer; + private TileTurtle owner; + private final Inventory inventory = (InventoryDelegate) () -> owner; + private ComputerProxy proxy; + private GameProfile owningPlayer; + private int commandsIssued = 0; + private int selectedSlot = 0; + private int fuelLevel = 0; + private int colourHex = -1; + private Identifier overlay = null; + private TurtleAnimation animation = TurtleAnimation.NONE; + private int animationProgress = 0; + private int lastAnimationProgress = 0; + + public TurtleBrain( TileTurtle turtle ) + { + owner = turtle; + } + + public TileTurtle getOwner() + { + return owner; + } + + public void setOwner( TileTurtle owner ) + { + this.owner = owner; + } + + public ComputerProxy getProxy() + { + if( proxy == null ) + { + proxy = new ComputerProxy( () -> owner ); + } + return proxy; + } + + public ComputerFamily getFamily() + { + return owner.getFamily(); + } + + public void setupComputer( ServerComputer computer ) + { + updatePeripherals( computer ); + } + + 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 static ComputerSide toDirection( TurtleSide side ) + { + switch( side ) + { + case LEFT: + return ComputerSide.LEFT; + case RIGHT: + default: + return ComputerSide.RIGHT; + } + } + + public void update() + { + World world = getWorld(); + if( !world.isClient ) + { + // Advance movement + updateCommands(); + + // The block may have been broken while the command was executing (for instance, if a block explodes + // when being mined). If so, abort. + if( owner.isRemoved() ) return; + } + + // Advance animation + updateAnimation(); + + // Advance upgrades + if( !upgrades.isEmpty() ) + { + for( Map.Entry entry : upgrades.entrySet() ) + { + entry.getValue() + .update( this, entry.getKey() ); + } + } + } + + @Nonnull + @Override + public World getWorld() + { + return owner.getWorld(); + } + + @Nonnull + @Override + public BlockPos getPosition() + { + return 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 = owner; + BlockPos oldPos = owner.getPos(); + BlockState oldBlock = 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.isChunkLoaded( pos ) ) + { + 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 = 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( 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 owner.getDirection(); + } + + @Override + public void setDirection( @Nonnull Direction dir ) + { + owner.setDirection( dir ); + } + + @Override + public int getSelectedSlot() + { + return selectedSlot; + } + + @Override + public void setSelectedSlot( int slot ) + { + if( getWorld().isClient ) + { + throw new UnsupportedOperationException( "Cannot set the slot on the client" ); + } + + if( slot >= 0 && slot < owner.size() ) + { + selectedSlot = slot; + owner.onTileEntityChange(); + } + } + + @Override + public int getColour() + { + return colourHex; + } + + @Override + public void setColour( int colour ) + { + if( colour >= 0 && colour <= 0xFFFFFF ) + { + if( colourHex != colour ) + { + colourHex = colour; + owner.updateBlock(); + } + } + else if( colourHex != -1 ) + { + colourHex = -1; + owner.updateBlock(); + } + } + + @Nullable + @Override + public GameProfile getOwningPlayer() + { + return owningPlayer; + } + + @Override + public boolean isFuelNeeded() + { + return ComputerCraft.turtlesNeedFuel; + } + + @Override + public int getFuelLevel() + { + return Math.min( fuelLevel, getFuelLimit() ); + } + + @Override + public void setFuelLevel( int level ) + { + fuelLevel = Math.min( level, getFuelLimit() ); + owner.onTileEntityChange(); + } + + @Override + public int getFuelLimit() + { + if( 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 ); + } + + @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; + } + + private int issueCommand( ITurtleCommand command ) + { + commandQueue.offer( new TurtleCommandQueueEntry( ++commandsIssued, command ) ); + return commandsIssued; + } + + @Override + public void playAnimation( @Nonnull TurtleAnimation animation ) + { + if( getWorld().isClient ) + { + throw new UnsupportedOperationException( "Cannot play animations on the client" ); + } + + this.animation = animation; + if( this.animation == TurtleAnimation.SHORT_WAIT ) + { + animationProgress = ANIM_DURATION / 2; + lastAnimationProgress = ANIM_DURATION / 2; + } + else + { + animationProgress = 0; + lastAnimationProgress = 0; + } + owner.updateBlock(); + } + + @Override + public ITurtleUpgrade getUpgrade( @Nonnull TurtleSide side ) + { + return upgrades.get( side ); + } + + @Override + public void setUpgrade( @Nonnull TurtleSide side, ITurtleUpgrade upgrade ) + { + // Remove old upgrade + if( upgrades.containsKey( side ) ) + { + if( upgrades.get( side ) == upgrade ) + { + return; + } + upgrades.remove( side ); + } + else + { + if( upgrade == null ) + { + return; + } + } + + upgradeNBTData.remove( side ); + + // Set new upgrade + if( upgrade != null ) + { + upgrades.put( side, upgrade ); + } + + // Notify clients and create peripherals + if( owner.getWorld() != null ) + { + updatePeripherals( owner.createServerComputer() ); + owner.updateBlock(); + } + } + + @Override + public IPeripheral getPeripheral( @Nonnull TurtleSide side ) + { + return peripherals.get( side ); + } + + @Nonnull + @Override + public NbtCompound getUpgradeNBTData( TurtleSide side ) + { + NbtCompound nbt = upgradeNBTData.get( side ); + if( nbt == null ) + { + upgradeNBTData.put( side, nbt = new NbtCompound() ); + } + return nbt; + } + + @Override + public void updateUpgradeNBTData( @Nonnull TurtleSide side ) + { + owner.updateBlock(); + } + + @Nonnull + @Override + public Inventory getInventory() + { + return inventory; + } + + public void setOwningPlayer( GameProfile profile ) + { + owningPlayer = profile; + } + + private void updateCommands() + { + if( animation != TurtleAnimation.NONE || commandQueue.isEmpty() ) + { + return; + } + + // If we've got a computer, ensure that we're allowed to perform work. + ServerComputer computer = owner.getServerComputer(); + if( computer != null && !computer.getComputer() + .getMainThreadMonitor() + .canWork() ) + { + return; + } + + // Pull a new command + TurtleCommandQueueEntry nextCommand = 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( animation != TurtleAnimation.NONE ) + { + World world = getWorld(); + + if( ComputerCraft.turtlesCanPush ) + { + // Advance entity pushing + if( animation == TurtleAnimation.MOVE_FORWARD || animation == TurtleAnimation.MOVE_BACK || animation == TurtleAnimation.MOVE_UP || animation == TurtleAnimation.MOVE_DOWN ) + { + BlockPos pos = getPosition(); + Direction moveDir; + switch( 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) (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 && animation == TurtleAnimation.MOVE_FORWARD && 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 + lastAnimationProgress = animationProgress; + if( ++animationProgress >= ANIM_DURATION ) + { + animation = TurtleAnimation.NONE; + animationProgress = 0; + lastAnimationProgress = 0; + } + } + } + + public Vec3d getRenderOffset( float f ) + { + switch( animation ) + { + case MOVE_FORWARD: + case MOVE_BACK: + case MOVE_UP: + case MOVE_DOWN: + // Get direction + Direction dir; + switch( 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; + } + } + + private float getAnimationFraction( float f ) + { + float next = (float) animationProgress / ANIM_DURATION; + float previous = (float) lastAnimationProgress / ANIM_DURATION; + return previous + (next - previous) * f; + } + + public void readFromNBT( NbtCompound nbt ) + { + readCommon( nbt ); + + // Read state + selectedSlot = nbt.getInt( NBT_SLOT ); + + // Read owner + if( nbt.contains( "Owner", NBTUtil.TAG_COMPOUND ) ) + { + NbtCompound owner = nbt.getCompound( "Owner" ); + owningPlayer = new GameProfile( new UUID( owner.getLong( "UpperId" ), owner.getLong( "LowerId" ) ), owner.getString( "Name" ) ); + } + else + { + owningPlayer = null; + } + } + + /** + * Read common data for saving and client synchronisation. + * + * @param nbt The tag to read from + */ + private void readCommon( NbtCompound nbt ) + { + // Read fields + colourHex = nbt.contains( NBT_COLOUR ) ? nbt.getInt( NBT_COLOUR ) : -1; + fuelLevel = nbt.contains( NBT_FUEL ) ? nbt.getInt( NBT_FUEL ) : 0; + 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 + upgradeNBTData.clear(); + if( nbt.contains( NBT_LEFT_UPGRADE_DATA ) ) + { + upgradeNBTData.put( TurtleSide.LEFT, + nbt.getCompound( NBT_LEFT_UPGRADE_DATA ) + .copy() ); + } + if( nbt.contains( NBT_RIGHT_UPGRADE_DATA ) ) + { + upgradeNBTData.put( TurtleSide.RIGHT, + nbt.getCompound( NBT_RIGHT_UPGRADE_DATA ) + .copy() ); + } + } + + public NbtCompound writeToNBT( NbtCompound nbt ) + { + writeCommon( nbt ); + + // Write state + nbt.putInt( NBT_SLOT, selectedSlot ); + + // Write owner + if( owningPlayer != null ) + { + NbtCompound owner = new NbtCompound(); + nbt.put( "Owner", owner ); + + owner.putLong( "UpperId", owningPlayer.getId() + .getMostSignificantBits() ); + owner.putLong( "LowerId", owningPlayer.getId() + .getLeastSignificantBits() ); + owner.putString( "Name", owningPlayer.getName() ); + } + + return nbt; + } + + private void writeCommon( NbtCompound nbt ) + { + nbt.putInt( NBT_FUEL, fuelLevel ); + if( colourHex != -1 ) + { + nbt.putInt( NBT_COLOUR, colourHex ); + } + if( overlay != null ) + { + nbt.putString( NBT_OVERLAY, 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( upgradeNBTData.containsKey( TurtleSide.LEFT ) ) + { + nbt.put( NBT_LEFT_UPGRADE_DATA, + getUpgradeNBTData( TurtleSide.LEFT ).copy() ); + } + if( upgradeNBTData.containsKey( TurtleSide.RIGHT ) ) + { + nbt.put( NBT_RIGHT_UPGRADE_DATA, + getUpgradeNBTData( TurtleSide.RIGHT ).copy() ); + } + } + + private static String getUpgradeId( ITurtleUpgrade upgrade ) + { + return upgrade != null ? upgrade.getUpgradeID() + .toString() : null; + } + + public void readDescription( NbtCompound nbt ) + { + readCommon( nbt ); + + // Animation + TurtleAnimation anim = TurtleAnimation.values()[nbt.getInt( "Animation" )]; + if( anim != animation && anim != TurtleAnimation.WAIT && anim != TurtleAnimation.SHORT_WAIT && anim != TurtleAnimation.NONE ) + { + animation = anim; + animationProgress = 0; + lastAnimationProgress = 0; + } + } + + public void writeDescription( NbtCompound nbt ) + { + writeCommon( nbt ); + nbt.putInt( "Animation", animation.ordinal() ); + } + + public Identifier getOverlay() + { + return overlay; + } + + public void setOverlay( Identifier overlay ) + { + if( !Objects.equal( this.overlay, overlay ) ) + { + this.overlay = overlay; + owner.updateBlock(); + } + } + + public DyeColor getDyeColour() + { + if( colourHex == -1 ) + { + return null; + } + Colour colour = Colour.fromHex( 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( colourHex != newColour ) + { + colourHex = newColour; + owner.updateBlock(); + } + } + + public float getToolRenderAngle( TurtleSide side, float f ) + { + return (side == TurtleSide.LEFT && animation == TurtleAnimation.SWING_LEFT_TOOL) || (side == TurtleSide.RIGHT && animation == TurtleAnimation.SWING_RIGHT_TOOL) ? 45.0f * (float) Math.sin( + getAnimationFraction( f ) * Math.PI ) : 0.0f; + } + + 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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCommandQueueEntry.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCommandQueueEntry.java new file mode 100644 index 000000000..17f203329 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCommandQueueEntry.java @@ -0,0 +1,21 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCompareCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCompareCommand.java new file mode 100644 index 000000000..b362f5d8b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCompareCommand.java @@ -0,0 +1,84 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 javax.annotation.Nonnull; +import java.util.List; + +public class TurtleCompareCommand implements ITurtleCommand +{ + private final InteractDirection direction; + + public TurtleCompareCommand( InteractDirection direction ) + { + this.direction = direction; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Get world direction from direction + Direction direction = this.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( !lookAtState.isAir() ) + { + // 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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCompareToCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCompareToCommand.java new file mode 100644 index 000000000..e5a638828 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCompareToCommand.java @@ -0,0 +1,43 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 slot; + + public TurtleCompareToCommand( int slot ) + { + this.slot = slot; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + ItemStack selectedStack = turtle.getInventory() + .getStack( turtle.getSelectedSlot() ); + ItemStack stack = turtle.getInventory() + .getStack( slot ); + if( InventoryUtil.areItemsStackable( selectedStack, stack ) ) + { + return TurtleCommandResult.success(); + } + else + { + return TurtleCommandResult.failure(); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCraftCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCraftCommand.java new file mode 100644 index 000000000..8b6d63e17 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleCraftCommand.java @@ -0,0 +1,58 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleDetectCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleDetectCommand.java new file mode 100644 index 000000000..cbc97f931 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleDetectCommand.java @@ -0,0 +1,42 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 direction; + + public TurtleDetectCommand( 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 ); + + return !WorldUtil.isLiquidBlock( world, newPosition ) && !world.isAir( newPosition ) ? TurtleCommandResult.success() : TurtleCommandResult.failure(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java new file mode 100644 index 000000000..bd53b9c66 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java @@ -0,0 +1,105 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.TurtleEvent; +import dan200.computercraft.api.turtle.event.TurtleInventoryEvent; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.ItemStorage; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; + +public class TurtleDropCommand implements ITurtleCommand +{ + private final InteractDirection direction; + private final int quantity; + + public TurtleDropCommand( InteractDirection direction, int quantity ) + { + this.direction = direction; + this.quantity = quantity; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Dropping nothing is easy + if( quantity == 0 ) + { + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + + // Get world direction from direction + Direction direction = this.direction.toWorldDir( turtle ); + + // Get things to drop + ItemStack stack = InventoryUtil.takeItems( 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(); + + Inventory 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( TurtleEvent.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, ItemStorage.wrap( inventory, side ) ); + 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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java new file mode 100644 index 000000000..d289efc40 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java @@ -0,0 +1,102 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.turtle.core; + +import dan200.computercraft.api.turtle.*; +import dan200.computercraft.api.turtle.event.TurtleAction; +import dan200.computercraft.api.turtle.event.TurtleActionEvent; +import dan200.computercraft.api.turtle.event.TurtleEvent; +import dan200.computercraft.shared.TurtleUpgrades; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.ItemStorage; +import dan200.computercraft.shared.util.WorldUtil; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; + +import javax.annotation.Nonnull; + +public class TurtleEquipCommand implements ITurtleCommand +{ + private final TurtleSide side; + + public TurtleEquipCommand( TurtleSide side ) + { + this.side = side; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Determine the upgrade to equipLeft + ITurtleUpgrade newUpgrade; + ItemStack newUpgradeStack; + Inventory inventory = turtle.getInventory(); + ItemStack selectedStack = inventory.getStack( 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( 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( TurtleEvent.post( event ) ) + { + return TurtleCommandResult.failure( event.getFailureMessage() ); + } + + // Do the swapping: + if( newUpgradeStack != null ) + { + // Consume new upgrades item + InventoryUtil.takeItems( 1, ItemStorage.wrap( inventory ), turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() ); + } + if( oldUpgradeStack != null ) + { + // Store old upgrades item + ItemStack remainder = InventoryUtil.storeItems( oldUpgradeStack, ItemStorage.wrap( 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( side, newUpgrade ); + + // Animate + if( newUpgrade != null || oldUpgrade != null ) + { + turtle.playAnimation( TurtleAnimation.WAIT ); + } + + return TurtleCommandResult.success(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java new file mode 100644 index 000000000..5e781b23a --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java @@ -0,0 +1,77 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.api.turtle.event.TurtleEvent; +import dan200.computercraft.shared.peripheral.generic.data.BlockData; +import net.minecraft.block.BlockState; +import net.minecraft.state.property.Property; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +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.isAir() ) + { + 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( TurtleEvent.post( event ) ) + { + return TurtleCommandResult.failure( event.getFailureMessage() ); + } + + return TurtleCommandResult.success( new Object[] { table } ); + } + + @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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java new file mode 100644 index 000000000..14bd79b2e --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java @@ -0,0 +1,166 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.api.turtle.event.TurtleEvent; +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 javax.annotation.Nonnull; +import java.util.List; + +public class TurtleMoveCommand implements ITurtleCommand +{ + private static final Box EMPTY_BOX = new Box( 0, 0, 0, 0, 0, 0 ); + private final MoveDirection direction; + + public TurtleMoveCommand( MoveDirection 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 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 || this.direction == MoveDirection.UP || this.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( TurtleEvent.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( this.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.isOutOfBuildLimitVertically( position ) ) + { + return TurtleCommandResult.failure( position.getY() < 0 ? "Too low to move" : "Too high to move" ); + } + if( !World.isInBuildLimit( 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.isChunkLoaded( position ) ) + { + 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(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java new file mode 100644 index 000000000..bc909ea91 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java @@ -0,0 +1,440 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.api.turtle.event.TurtleEvent; +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 org.apache.commons.lang3.tuple.Pair; + +import javax.annotation.Nonnull; +import java.util.List; + +public class TurtlePlaceCommand implements ITurtleCommand +{ + private final InteractDirection direction; + private final Object[] extraArguments; + + public TurtlePlaceCommand( InteractDirection direction, Object[] arguments ) + { + this.direction = direction; + extraArguments = arguments; + } + + 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 ); + } + + @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 = this.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( TurtleEvent.post( place ) ) + { + return TurtleCommandResult.failure( place.getFailureMessage() ); + } + + // Do the deploying + String[] errorMessage = new String[1]; + ItemStack remainder = deploy( stack, turtle, turtlePlayer, direction, 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 TurtlePlayer createPlayer( ITurtleAccess turtle, BlockPos position, Direction direction ) + { + TurtlePlayer turtlePlayer = TurtlePlayer.get( turtle ); + orientPlayer( turtle, turtlePlayer, position, direction ); + return turtlePlayer; + } + + 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 ); + return remainder; + + // If nothing worked, return the original stack unchanged + } + + 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 = hitEntity.interactAt( turtlePlayer, hitPos, Hand.MAIN_HAND ); + + if( cancelResult != null && cancelResult.isAccepted() ) + { + placed = true; + } + else + { + cancelResult = hitEntity.interact( turtlePlayer, Hand.MAIN_HAND ); + if( cancelResult != null && cancelResult.isAccepted() ) + { + 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; + } + } + + @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 ); + ItemPlacementContext placementContext = new ItemPlacementContext( context ); + 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 ); + + if( stackCopy.useOnBlock( context ).isAccepted() ) + { + placed = true; + turtlePlayer.loadInventory( stackCopy ); + } + + if( !placed && (item instanceof BucketItem || item instanceof BoatItem || item instanceof LilyPadItem || item instanceof GlassBottleItem) ) + { + 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; + } + } + + 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.isInBuildLimit( 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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/core/TurtlePlayer.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtlePlayer.java new file mode 100644 index 000000000..683cb189b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtlePlayer.java @@ -0,0 +1,240 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.turtle.core; + +import com.mojang.authlib.GameProfile; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.FakePlayer; +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.shared.ComputerCraftRegistry; +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.network.ServerPlayerEntity; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.OptionalInt; +import java.util.UUID; + +@SuppressWarnings( "EntityConstructor" ) +public final class TurtlePlayer extends FakePlayer +{ + private static final GameProfile DEFAULT_PROFILE = new GameProfile( UUID.fromString( "0d0c4ca0-4ff1-11e4-916c-0800200c9a66" ), "[ComputerCraft]" ); + + // TODO [M3R1-01] Fix Turtle not giving player achievement for actions + private TurtlePlayer( ServerWorld world, GameProfile name ) + { + super( world, name ); + } + + private static TurtlePlayer create( ITurtleAccess turtle ) + { + ServerWorld world = (ServerWorld) turtle.getWorld(); + GameProfile profile = turtle.getOwningPlayer(); + + TurtlePlayer player = new TurtlePlayer( world, getProfile( profile ) ); + player.networkHandler = new FakeNetHandler( player ); + player.setState( turtle ); + + if( profile != null && profile.getId() != null ) + { + // Constructing a player overrides the "active player" variable in advancements. As fake players cannot + // get advancements, this prevents a normal player who has placed a turtle from getting advancements. + // We try to locate the "actual" player and restore them. + ServerPlayerEntity actualPlayer = world.getServer().getPlayerManager().getPlayer( player.getUuid() ); + if( actualPlayer != null ) player.getAdvancementTracker().setOwner( actualPlayer ); + } + + return player; + } + + private static GameProfile getProfile( @Nullable GameProfile profile ) + { + return profile != null && profile.isComplete() ? profile : DEFAULT_PROFILE; + } + + private void setState( ITurtleAccess turtle ) + { + if( currentScreenHandler != playerScreenHandler ) + { + ComputerCraft.log.warn( "Turtle has open container ({})", currentScreenHandler ); + closeHandledScreen(); + } + + 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 create( access ); + + TurtleBrain brain = (TurtleBrain) access; + TurtlePlayer player = brain.cachedPlayer; + if( player == null || player.getGameProfile() != getProfile( access.getOwningPlayer() ) || player.getEntityWorld() != access.getWorld() ) + { + player = brain.cachedPlayer = create( 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 ComputerCraftRegistry.ModEntities.TURTLE_PLAYER; + } + + @Override + public float getEyeHeight( @Nonnull EntityPose pose ) + { + return 0; + } + + @Override + public Vec3d getPos() + { + return new Vec3d( getX(), getY(), getZ() ); + } + + @Override + public float getActiveEyeHeight( @Nonnull EntityPose pose, @Nonnull EntityDimensions size ) + { + return 0; + } + + @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 ) + { + } + + //region Code which depends on the connection + @Nonnull + @Override + public OptionalInt openHandledScreen( @Nullable NamedScreenHandlerFactory prover ) + { + return OptionalInt.empty(); + } + + @Override + public void openHorseInventory( @Nonnull HorseBaseEntity horse, @Nonnull Inventory inventory ) + { + } + + @Override + public void useBook( @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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleRefuelCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleRefuelCommand.java new file mode 100644 index 000000000..d97e28ad8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleRefuelCommand.java @@ -0,0 +1,59 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.TurtleEvent; +import dan200.computercraft.api.turtle.event.TurtleRefuelEvent; +import net.minecraft.item.ItemStack; + +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( TurtleEvent.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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java new file mode 100644 index 000000000..cd807af15 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java @@ -0,0 +1,165 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.TurtleEvent; +import dan200.computercraft.api.turtle.event.TurtleInventoryEvent; +import dan200.computercraft.shared.util.InventoryUtil; +import dan200.computercraft.shared.util.ItemStorage; +import net.minecraft.entity.ItemEntity; +import net.minecraft.inventory.Inventory; +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 javax.annotation.Nonnull; +import java.util.List; + +public class TurtleSuckCommand implements ITurtleCommand +{ + private final InteractDirection direction; + private final int quantity; + + public TurtleSuckCommand( InteractDirection direction, int quantity ) + { + this.direction = direction; + this.quantity = quantity; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Sucking nothing is easy + if( quantity == 0 ) + { + turtle.playAnimation( TurtleAnimation.WAIT ); + return TurtleCommandResult.success(); + } + + // Get world direction from direction + Direction direction = this.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(); + + Inventory 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( TurtleEvent.post( event ) ) + { + return TurtleCommandResult.failure( event.getFailureMessage() ); + } + + if( inventory != null ) + { + // Take from inventory of thing in front + ItemStack stack = InventoryUtil.takeItems( quantity, ItemStorage.wrap( 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, ItemStorage.wrap( 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() > quantity ) + { + storeStack = stack.split( 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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleToolCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleToolCommand.java new file mode 100644 index 000000000..77c679098 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleToolCommand.java @@ -0,0 +1,82 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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; + } + + 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 ); + } + + @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" ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleTransferToCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleTransferToCommand.java new file mode 100644 index 000000000..608d3196e --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleTransferToCommand.java @@ -0,0 +1,60 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 slot; + private final int quantity; + + public TurtleTransferToCommand( int slot, int limit ) + { + this.slot = slot; + quantity = limit; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + // Take stack + ItemStack stack = InventoryUtil.takeItems( 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(), slot, 1, 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/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleTurnCommand.java b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleTurnCommand.java new file mode 100644 index 000000000..fd669ac8d --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/core/TurtleTurnCommand.java @@ -0,0 +1,54 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 dan200.computercraft.api.turtle.event.TurtleEvent; + +import javax.annotation.Nonnull; + +public class TurtleTurnCommand implements ITurtleCommand +{ + private final TurnDirection direction; + + public TurtleTurnCommand( TurnDirection direction ) + { + this.direction = direction; + } + + @Nonnull + @Override + public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle ) + { + TurtleActionEvent event = new TurtleActionEvent( turtle, TurtleAction.TURN ); + if( TurtleEvent.post( event ) ) + { + return TurtleCommandResult.failure( event.getFailureMessage() ); + } + + switch( 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/remappedSrc/dan200/computercraft/shared/turtle/inventory/ContainerTurtle.java b/remappedSrc/dan200/computercraft/shared/turtle/inventory/ContainerTurtle.java new file mode 100644 index 000000000..f090722b8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/inventory/ContainerTurtle.java @@ -0,0 +1,153 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.turtle.inventory; + +import dan200.computercraft.shared.ComputerCraftRegistry; +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.network.PacketByteBuf; +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; + + 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 ); + } + + private ContainerTurtle( int id, Predicate canUse, IComputer computer, ComputerFamily family, PlayerInventory playerInventory, + Inventory inventory, PropertyDelegate properties ) + { + super( ComputerCraftRegistry.ModContainers.TURTLE, 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, PacketByteBuf packetByteBuf ) + { + this( id, player, new ComputerContainerData( packetByteBuf ) ); + } + + 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 + @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; + } + + @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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/items/ITurtleItem.java b/remappedSrc/dan200/computercraft/shared/turtle/items/ITurtleItem.java new file mode 100644 index 000000000..349fc3cab --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/items/ITurtleItem.java @@ -0,0 +1,28 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/turtle/items/ItemTurtle.java b/remappedSrc/dan200/computercraft/shared/turtle/items/ItemTurtle.java new file mode 100644 index 000000000..4193f3e32 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/items/ItemTurtle.java @@ -0,0 +1,182 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.turtle.items; + +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.NbtCompound; +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 static dan200.computercraft.shared.turtle.core.TurtleBrain.*; + +public class ItemTurtle extends ItemComputerBase implements ITurtleItem +{ + public ItemTurtle( BlockTurtle block, Settings settings ) + { + super( block, settings ); + } + + @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 ); + } + + 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.getOrCreateNbt() + .putInt( NBT_ID, id ); + } + IColouredItem.setColourBasic( stack, colour ); + if( fuelLevel > 0 ) + { + stack.getOrCreateNbt() + .putInt( NBT_FUEL, fuelLevel ); + } + if( overlay != null ) + { + stack.getOrCreateNbt() + .putString( NBT_OVERLAY, overlay.toString() ); + } + + if( leftUpgrade != null ) + { + stack.getOrCreateNbt() + .putString( NBT_LEFT_UPGRADE, + leftUpgrade.getUpgradeID() + .toString() ); + } + + if( rightUpgrade != null ) + { + stack.getOrCreateNbt() + .putString( NBT_RIGHT_UPGRADE, + rightUpgrade.getUpgradeID() + .toString() ); + } + + return stack; + } + + @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 ITurtleUpgrade getUpgrade( @Nonnull ItemStack stack, @Nonnull TurtleSide side ) + { + NbtCompound tag = stack.getNbt(); + 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 int getFuelLevel( @Nonnull ItemStack stack ) + { + NbtCompound tag = stack.getNbt(); + return tag != null && tag.contains( NBT_FUEL ) ? tag.getInt( NBT_FUEL ) : 0; + } + + @Override + public Identifier getOverlay( @Nonnull ItemStack stack ) + { + NbtCompound tag = stack.getNbt(); + return tag != null && tag.contains( NBT_OVERLAY ) ? new Identifier( tag.getString( NBT_OVERLAY ) ) : null; + } + + @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 ) ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/items/TurtleItemFactory.java b/remappedSrc/dan200/computercraft/shared/turtle/items/TurtleItemFactory.java new file mode 100644 index 000000000..9d604502f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/items/TurtleItemFactory.java @@ -0,0 +1,53 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.turtle.items; + +import dan200.computercraft.api.turtle.ITurtleAccess; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleSide; +import dan200.computercraft.shared.ComputerCraftRegistry; +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 ComputerCraftRegistry.ModItems.TURTLE_NORMAL.create( id, label, colour, leftUpgrade, rightUpgrade, fuelLevel, overlay ); + case ADVANCED: + return ComputerCraftRegistry.ModItems.TURTLE_ADVANCED.create( id, label, colour, leftUpgrade, rightUpgrade, fuelLevel, overlay ); + default: + return ItemStack.EMPTY; + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/recipes/TurtleRecipe.java b/remappedSrc/dan200/computercraft/shared/turtle/recipes/TurtleRecipe.java new file mode 100644 index 000000000..3fda22471 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/recipes/TurtleRecipe.java @@ -0,0 +1,55 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 +{ + public static final RecipeSerializer SERIALIZER = + new 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 ); + } + }; + + 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 ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java b/remappedSrc/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java new file mode 100644 index 000000000..fc297291a --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java @@ -0,0 +1,190 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 +{ + public static final RecipeSerializer SERIALIZER = new SpecialRecipeSerializer<>( TurtleUpgradeRecipe::new ); + + private TurtleUpgradeRecipe( Identifier id ) + { + super( id ); + } + + @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 !craft( inventory ).isEmpty(); + } + + @Nonnull + @Override + public ItemStack craft( @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 ); + } + + @Override + public boolean fits( int x, int y ) + { + return x >= 3 && y >= 1; + } + + @Nonnull + @Override + public RecipeSerializer getSerializer() + { + return SERIALIZER; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/upgrades/CraftingTablePeripheral.java b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/CraftingTablePeripheral.java new file mode 100644 index 000000000..a01195ab2 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/CraftingTablePeripheral.java @@ -0,0 +1,65 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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"; + } + + @Nonnull + @Override + public Object getTarget() + { + return turtle; + } + + @Override + public boolean equals( IPeripheral other ) + { + return other instanceof CraftingTablePeripheral; + } + + @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 ) ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleAxe.java b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleAxe.java new file mode 100644 index 000000000..635536c0f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleAxe.java @@ -0,0 +1,35 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleCraftingTable.java b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleCraftingTable.java new file mode 100644 index 000000000..88d668a87 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleCraftingTable.java @@ -0,0 +1,60 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 javax.annotation.Nonnull; + +public class TurtleCraftingTable extends AbstractTurtleUpgrade +{ + @Environment( EnvType.CLIENT ) + private ModelIdentifier leftModel; + + @Environment( EnvType.CLIENT ) + private ModelIdentifier 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 ); + } + + @Nonnull + @Override + @Environment( EnvType.CLIENT ) + public TransformedModel getModel( ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + loadModelLocations(); + return TransformedModel.of( side == TurtleSide.LEFT ? leftModel : rightModel ); + } + + @Environment( EnvType.CLIENT ) + private void loadModelLocations() + { + if( leftModel == null ) + { + leftModel = new ModelIdentifier( "computercraft:turtle_crafting_table_left", "inventory" ); + rightModel = new ModelIdentifier( "computercraft:turtle_crafting_table_right", "inventory" ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleHoe.java b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleHoe.java new file mode 100644 index 000000000..1676455c6 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleHoe.java @@ -0,0 +1,70 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 ); + } + + @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 ); + } + + @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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleInventoryCrafting.java b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleInventoryCrafting.java new file mode 100644 index 000000000..2b36437fd --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleInventoryCrafting.java @@ -0,0 +1,261 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 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 final ITurtleAccess turtle; + private int xStart; + private int 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 ); + this.turtle = turtle; + xStart = 0; + yStart = 0; + } + + @Nullable + private Recipe tryCrafting( int xStart, int yStart ) + { + this.xStart = xStart; + this.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 < this.xStart || x >= this.xStart + 3 || y < this.yStart || y >= this.yStart + 3 ) + { + if( !turtle.getInventory() + .getStack( x + y * TileTurtle.INVENTORY_WIDTH ) + .isEmpty() ) + { + return null; + } + } + } + } + + // Check the actual crafting + return turtle.getWorld() + .getRecipeManager() + .getFirstMatch( RecipeType.CRAFTING, this, 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( 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() ); + DefaultedList remainders = recipe.getRemainder( this ); + + 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.areNbtEqual( existing, remainder ) ) + { + remainder.increment( existing.getCount() ); + setStack( slot, remainder ); + } + else + { + results.add( remainder ); + } + } + } + + return results; + } + + @Override + public int getMaxCountPerStack() + { + return turtle.getInventory() + .getMaxCountPerStack(); + } + + @Override + public int getWidth() + { + return 3; + } + + @Override + public boolean isValid( int i, @Nonnull ItemStack stack ) + { + i = modifyIndex( i ); + return turtle.getInventory() + .isValid( i, stack ); + } + + @Override + public int getHeight() + { + return 3; + } + + private int modifyIndex( int index ) + { + int x = xStart + index % getWidth(); + int y = 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 turtle.getInventory() + .getStack( i ); + } + + @Nonnull + @Override + public ItemStack removeStack( int i ) + { + i = modifyIndex( i ); + return turtle.getInventory() + .removeStack( i ); + } + + @Nonnull + @Override + public ItemStack removeStack( int i, int size ) + { + i = modifyIndex( i ); + return turtle.getInventory() + .removeStack( i, size ); + } + + @Override + public void setStack( int i, @Nonnull ItemStack stack ) + { + i = modifyIndex( i ); + turtle.getInventory() + .setStack( i, stack ); + } + + + @Override + public void markDirty() + { + turtle.getInventory() + .markDirty(); + } + + @Override + public boolean canPlayerUse( @Nonnull PlayerEntity player ) + { + return true; + } + + + @Override + public void clear() + { + for( int i = 0; i < size(); i++ ) + { + int j = modifyIndex( i ); + turtle.getInventory() + .setStack( j, ItemStack.EMPTY ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java new file mode 100644 index 000000000..9ef5b0b6a --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java @@ -0,0 +1,150 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.turtle.upgrades; + +import dan200.computercraft.api.client.TransformedModel; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.turtle.*; +import dan200.computercraft.shared.ComputerCraftRegistry; +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.NbtCompound; +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 javax.annotation.Nonnull; + +public class TurtleModem extends AbstractTurtleUpgrade +{ + private final boolean advanced; + @Environment( EnvType.CLIENT ) + private ModelIdentifier leftOffModel; + @Environment( EnvType.CLIENT ) + private ModelIdentifier rightOffModel; + @Environment( EnvType.CLIENT ) + private ModelIdentifier leftOnModel; + @Environment( EnvType.CLIENT ) + private ModelIdentifier rightOnModel; + + public TurtleModem( boolean advanced, Identifier id ) + { + super( id, + TurtleUpgradeType.PERIPHERAL, + advanced ? ComputerCraftRegistry.ModBlocks.WIRELESS_MODEM_ADVANCED : ComputerCraftRegistry.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(); + } + + @Nonnull + @Override + @Environment( EnvType.CLIENT ) + public TransformedModel getModel( ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + loadModelLocations(); + + boolean active = false; + if( turtle != null ) + { + NbtCompound turtleNBT = turtle.getUpgradeNBTData( side ); + active = turtleNBT.contains( "active" ) && turtleNBT.getBoolean( "active" ); + } + + return side == TurtleSide.LEFT ? TransformedModel.of( active ? leftOnModel : leftOffModel ) : TransformedModel.of( active ? rightOnModel : rightOffModel ); + } + + @Environment( EnvType.CLIENT ) + private void loadModelLocations() + { + if( leftOffModel == null ) + { + if( advanced ) + { + leftOffModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_off_left", "inventory" ); + rightOffModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_off_right", "inventory" ); + leftOnModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_on_left", "inventory" ); + rightOnModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_on_right", "inventory" ); + } + else + { + leftOffModel = new ModelIdentifier( "computercraft:turtle_modem_normal_off_left", "inventory" ); + rightOffModel = new ModelIdentifier( "computercraft:turtle_modem_normal_off_right", "inventory" ); + leftOnModel = new ModelIdentifier( "computercraft:turtle_modem_normal_on_left", "inventory" ); + rightOnModel = new ModelIdentifier( "computercraft:turtle_modem_normal_on_right", "inventory" ); + } + } + } + + @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 ); + } + } + } + } + + 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); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleShovel.java b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleShovel.java new file mode 100644 index 000000000..f08164bb2 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleShovel.java @@ -0,0 +1,70 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 ); + } + + @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 ); + } + + @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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java new file mode 100644 index 000000000..3fbc845ca --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java @@ -0,0 +1,102 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.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.ComputerCraftRegistry; +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 javax.annotation.Nonnull; + +public class TurtleSpeaker extends AbstractTurtleUpgrade +{ + @Environment( EnvType.CLIENT ) + private ModelIdentifier leftModel; + @Environment( EnvType.CLIENT ) + private ModelIdentifier rightModel; + + public TurtleSpeaker( Identifier id ) + { + super( id, TurtleUpgradeType.PERIPHERAL, ComputerCraftRegistry.ModBlocks.SPEAKER ); + } + + @Override + public IPeripheral createPeripheral( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + return new TurtleSpeaker.Peripheral( turtle ); + } + + @Nonnull + @Override + @Environment( EnvType.CLIENT ) + public TransformedModel getModel( ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + loadModelLocations(); + return TransformedModel.of( side == TurtleSide.LEFT ? leftModel : rightModel ); + } + + @Environment( EnvType.CLIENT ) + private void loadModelLocations() + { + if( leftModel == null ) + { + leftModel = new ModelIdentifier( "computercraft:turtle_speaker_upgrade_left", "inventory" ); + rightModel = new ModelIdentifier( "computercraft:turtle_speaker_upgrade_right", "inventory" ); + } + } + + @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(); + } + } + + 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); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleSword.java b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleSword.java new file mode 100644 index 000000000..7c41f9b46 --- /dev/null +++ b/remappedSrc/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-2021. 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 float getDamageMultiplier() + { + return 9.0f; + } + + @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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java new file mode 100644 index 000000000..91fc88b3b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java @@ -0,0 +1,299 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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.api.turtle.event.TurtleEvent; +import dan200.computercraft.shared.TurtlePermissions; +import dan200.computercraft.shared.turtle.core.TurtleBrain; +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.fabricmc.fabric.api.event.player.AttackEntityCallback; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.block.entity.BlockEntity; +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.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.AffineTransformation; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.Vec3f; +import net.minecraft.world.World; +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; + + private static final int TAG_LIST = 9; + private static final int TAG_COMPOUND = 10; + + 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 ); + item = toolItem; + } + + @Override + public boolean isItemSuitable( @Nonnull ItemStack stack ) + { + NbtCompound tag = stack.getNbt(); + if( tag == null || tag.isEmpty() ) return true; + + // Check we've not got anything vaguely interesting on the item. We allow other mods to add their + // own NBT, with the understanding such details will be lost to the mist of time. + if( stack.isDamaged() || stack.hasEnchantments() || stack.hasCustomName() ) return false; + return !tag.contains( "AttributeModifiers", TAG_LIST ) || + tag.getList( "AttributeModifiers", TAG_COMPOUND ).isEmpty(); + } + + @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" ); + } + } + + @Nonnull + @Override + @Environment( EnvType.CLIENT ) + public TransformedModel getModel( ITurtleAccess turtle, @Nonnull TurtleSide side ) + { + float xOffset = side == TurtleSide.LEFT ? -0.40625f : 0.40625f; + return TransformedModel.of( getCraftingItem(), new AffineTransformation( new Vec3f( xOffset + 1, 0, 1 ), Vec3f.POSITIVE_Y.getDegreesQuaternion( 270 ), new Vec3f( 1, 1, 1 ), Vec3f.POSITIVE_Z.getDegreesQuaternion( 90 ) ) ); + } + + private TurtleCommandResult attack( ITurtleAccess turtle, Direction direction, TurtleSide side ) + { + // Create a fake player, and orient it appropriately + World world = turtle.getWorld(); + BlockPos position = turtle.getPosition(); + BlockEntity turtleBlock = turtle instanceof TurtleBrain ? ((TurtleBrain) turtle).getOwner() : world.getBlockEntity( position ); + if( turtleBlock == null ) return TurtleCommandResult.failure( "Turtle has vanished from existence." ); + + 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 inventoryf + ItemStack stackCopy = item.copy(); + turtlePlayer.loadInventory( stackCopy ); + + Entity hitEntity = hit.getKey(); + + // Fire several events to ensure we have permissions. + if( AttackEntityCallback.EVENT.invoker() + .interact( turtlePlayer, + world, + Hand.MAIN_HAND, + hitEntity, + null ) == ActionResult.FAIL || !hitEntity.isAttackable() ) + { + return TurtleCommandResult.failure( "Nothing to attack here" ); + } + + TurtleAttackEvent attackEvent = new TurtleAttackEvent( turtle, turtlePlayer, hitEntity, this, side ); + if( TurtleEvent.post( attackEvent ) ) + { + return TurtleCommandResult.failure( attackEvent.getFailureMessage() ); + } + + // Start claiming entity drops + DropConsumer.set( hitEntity, turtleDropConsumer( turtleBlock, 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( turtleBlock, 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(); + BlockEntity turtleBlock = turtle instanceof TurtleBrain ? ((TurtleBrain) turtle).getOwner() : world.getBlockEntity( turtlePosition ); + if( turtleBlock == null ) return TurtleCommandResult.failure( "Turtle has vanished from existence." ); + + + 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 ); + + TurtlePlayer turtlePlayer = TurtlePlaceCommand.createPlayer( turtle, turtlePosition, direction ); + turtlePlayer.loadInventory( item.copy() ); + + if( ComputerCraft.turtlesObeyBlockProtection ) + { + 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( TurtleEvent.post( digEvent ) ) + { + return TurtleCommandResult.failure( digEvent.getFailureMessage() ); + } + + // Consume the items the block drops + DropConsumer.set( world, blockPosition, turtleDropConsumer( turtleBlock, 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 + state.getBlock() + .onBreak( world, blockPosition, state, turtlePlayer ); + if( world.removeBlock( blockPosition, false ) ) + { + state.getBlock() + .onBroken( world, blockPosition, state ); + if( turtlePlayer.canHarvest( state ) ) + { + state.getBlock() + .afterBreak( world, turtlePlayer, blockPosition, state, tile, turtlePlayer.getMainHandStack() ); + } + } + + stopConsuming( turtleBlock, turtle ); + + return TurtleCommandResult.success(); + + } + + private static Function turtleDropConsumer( BlockEntity turtleBlock, ITurtleAccess turtle ) + { + return drop -> turtleBlock.isRemoved() ? drop : InventoryUtil.storeItems( drop, turtle.getItemHandler(), turtle.getSelectedSlot() ); + } + + protected float getDamageMultiplier() + { + return 3.0f; + } + + private static void stopConsuming( BlockEntity turtleBlock, ITurtleAccess turtle ) + { + Direction direction = turtleBlock.isRemoved() ? null : turtle.getDirection().getOpposite(); + List extra = DropConsumer.clear(); + for( ItemStack remainder : extra ) + { + WorldUtil.dropItemStack( remainder, + turtle.getWorld(), + turtle.getPosition(), + direction ); + } + } + + protected boolean canBreakBlock( BlockState state, World world, BlockPos pos, TurtlePlayer player ) + { + Block block = state.getBlock(); + return !state.isAir() && block != Blocks.BEDROCK && state.calcBlockBreakingDelta( player, world, pos ) > 0; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/Colour.java b/remappedSrc/dan200/computercraft/shared/util/Colour.java new file mode 100644 index 000000000..73589ee0f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/Colour.java @@ -0,0 +1,94 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +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(); + 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 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; + } + + public int getHex() + { + return hex; + } + + public Colour getNext() + { + return VALUES[(ordinal() + 1) % 16]; + } + + public Colour getPrevious() + { + return VALUES[(ordinal() + 15) % 16]; + } + + 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/remappedSrc/dan200/computercraft/shared/util/ColourTracker.java b/remappedSrc/dan200/computercraft/shared/util/ColourTracker.java new file mode 100644 index 000000000..da0e00bb1 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/ColourTracker.java @@ -0,0 +1,61 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import net.minecraft.util.DyeColor; + +/** + * 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( float r, float g, float b ) + { + addColour( (int) (r * 255), (int) (g * 255), (int) (b * 255) ); + } + + 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( DyeColor dye ) + { + Colour colour = Colour.VALUES[15 - dye.getId()]; + addColour( colour.getR(), colour.getG(), colour.getB() ); + } + + 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/remappedSrc/dan200/computercraft/shared/util/ColourUtils.java b/remappedSrc/dan200/computercraft/shared/util/ColourUtils.java new file mode 100644 index 000000000..92a1a13fc --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/ColourUtils.java @@ -0,0 +1,26 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import net.minecraft.item.DyeItem; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.DyeColor; + +import javax.annotation.Nullable; + +public final class ColourUtils +{ + @Nullable + private ColourUtils() {} + + public static DyeColor getStackColour( ItemStack stack ) + { + Item item = stack.getItem(); + return item instanceof DyeItem ? ((DyeItem) item).getColor() : null; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/CommentedConfigSpec.java b/remappedSrc/dan200/computercraft/shared/util/CommentedConfigSpec.java new file mode 100644 index 000000000..830eb4a5a --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/CommentedConfigSpec.java @@ -0,0 +1,56 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.util; + +import com.electronwill.nightconfig.core.CommentedConfig; +import com.electronwill.nightconfig.core.Config; +import com.electronwill.nightconfig.core.ConfigSpec; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.electronwill.nightconfig.core.utils.StringUtils.split; + +public class CommentedConfigSpec extends ConfigSpec +{ + private final Map, String> comments = new HashMap<>(); + + public void comment( List path, String comment ) + { + comments.put( path, comment ); + } + + public void comment( String path, String comment ) + { + comment( split( path, '.' ), comment ); + } + + @Override + public int correct( Config config ) + { + return correct( config, ( action, path, incorrectValue, correctedValue ) -> { } ); + } + + @Override + public int correct( Config config, ConfigSpec.CorrectionListener listener ) + { + int corrections = super.correct( config, listener ); + if( config instanceof CommentedConfig ) + { + insertComments( (CommentedConfig) config ); + } + return corrections; + } + + private void insertComments( CommentedConfig config ) + { + for( Map.Entry, String> entry : comments.entrySet() ) + { + config.setComment( entry.getKey(), entry.getValue() ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/Config.java b/remappedSrc/dan200/computercraft/shared/util/Config.java new file mode 100644 index 000000000..241f0282d --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/Config.java @@ -0,0 +1,412 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.util; + +import com.electronwill.nightconfig.core.ConfigSpec; +import com.electronwill.nightconfig.core.EnumGetMethod; +import com.electronwill.nightconfig.core.UnmodifiableConfig; +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.electronwill.nightconfig.core.file.FileNotFoundAction; +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.fabric.mixin.WorldSavePathAccess; +import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; +import net.fabricmc.loader.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.WorldSavePath; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public final class Config +{ + private static final int MODEM_MAX_RANGE = 100000; + + public static final String TRANSLATION_PREFIX = "gui.computercraft.config."; + + public static final CommentedConfigSpec serverSpec; + public static final CommentedConfigSpec clientSpec; + + public static CommentedFileConfig serverConfig; + public static CommentedFileConfig clientConfig; + + private static final WorldSavePath serverDir = WorldSavePathAccess.createWorldSavePath( "serverconfig" ); + private static final String serverFileName = "computercraft-server.toml"; + + private static Path serverPath = null; + private static final Path clientPath = FabricLoader.INSTANCE.getConfigDir().resolve( "computercraft-client.toml" ); + + private Config() + { + } + + static + { + System.setProperty( "nightconfig.preserveInsertionOrder", "true" ); + + serverSpec = new CommentedConfigSpec(); + { // General computers + serverSpec.comment( "computer_space_limit", + "The disk space limit for computers and turtles, in bytes" ); + serverSpec.define( "computer_space_limit", ComputerCraft.computerSpaceLimit ); + + serverSpec.comment( "floppy_space_limit", + "The disk space limit for floppy disks, in bytes" ); + serverSpec.define( "floppy_space_limit", ComputerCraft.floppySpaceLimit ); + + serverSpec.comment( "maximum_open_files", + "Set how many files a computer can have open at the same time. Set to 0 for unlimited." ); + serverSpec.defineInRange( "maximum_open_files", ComputerCraft.maximumFilesOpen, 0, Integer.MAX_VALUE ); + + serverSpec.comment( "disable_lua51_features", + "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." ); + serverSpec.define( "disable_lua51_features", ComputerCraft.disableLua51Features ); + + serverSpec.comment( "default_computer_settings", + "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" ); + serverSpec.define( "default_computer_settings", ComputerCraft.defaultComputerSettings ); + + serverSpec.comment( "debug_enabled", + "Enable Lua's debug library. This is sandboxed to each computer, so is generally safe to be used by players." ); + serverSpec.define( "debug_enabled", ComputerCraft.debugEnable ); + + serverSpec.comment( "log_computer_errors", + "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." ); + serverSpec.define( "log_computer_errors", ComputerCraft.logComputerErrors ); + + serverSpec.comment( "command_require_creative", + "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." ); + serverSpec.define( "command_require_creative", ComputerCraft.commandRequireCreative ); + } + + { // Execution + serverSpec.comment( "execution", + "Controls execution behaviour of computers. This is largely intended for fine-tuning " + + "servers, and generally shouldn't need to be touched" ); + + serverSpec.comment( "execution.computer_threads", + "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." ); + serverSpec.defineInRange( "execution.computer_threads", ComputerCraft.computerThreads, 1, Integer.MAX_VALUE ); + + serverSpec.comment( "execution.max_main_global_time", + "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." ); + serverSpec.defineInRange( "execution.max_main_global_time", (int) TimeUnit.NANOSECONDS.toMillis( ComputerCraft.maxMainGlobalTime ), 1, Integer.MAX_VALUE ); + + serverSpec.comment( "execution.max_main_computer_time", + "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." ); + serverSpec.defineInRange( "execution.max_main_computer_time", (int) TimeUnit.NANOSECONDS.toMillis( ComputerCraft.maxMainComputerTime ), 1, Integer.MAX_VALUE ); + } + + { // HTTP + serverSpec.comment( "http", "Controls the HTTP API" ); + + serverSpec.comment( "http.enabled", + "Enable the \"http\" API on Computers (see \"rules\" for more fine grained control than this)." ); + serverSpec.define( "http.enabled", ComputerCraft.httpEnabled ); + + serverSpec.comment( "http.websocket_enabled", + "Enable use of http websockets. This requires the \"http_enable\" option to also be true." ); + serverSpec.define( "http.websocket_enabled", ComputerCraft.httpWebsocketEnabled ); + + serverSpec.comment( "http.rules", + "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." ); + serverSpec.defineList( "http.rules", Arrays.asList( + AddressRuleConfig.makeRule( "$private", Action.DENY ), + AddressRuleConfig.makeRule( "*", Action.ALLOW ) + ), x -> x instanceof UnmodifiableConfig && AddressRuleConfig.checkRule( (UnmodifiableConfig) x ) ); + + serverSpec.comment( "http.max_requests", + "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." ); + serverSpec.defineInRange( "http.max_requests", ComputerCraft.httpMaxRequests, 0, Integer.MAX_VALUE ); + + serverSpec.comment( "http.max_websockets", + "The number of websockets a computer can have open at one time. Set to 0 for unlimited." ); + serverSpec.defineInRange( "http.max_websockets", ComputerCraft.httpMaxWebsockets, 1, Integer.MAX_VALUE ); + } + + { // Peripherals + serverSpec.comment( "peripheral", "Various options relating to peripherals." ); + + serverSpec.comment( "peripheral.command_block_enabled", + "Enable Command Block peripheral support" ); + serverSpec.define( "peripheral.command_block_enabled", ComputerCraft.enableCommandBlock ); + + serverSpec.comment( "peripheral.modem_range", + "The range of Wireless Modems at low altitude in clear weather, in meters" ); + serverSpec.defineInRange( "peripheral.modem_range", ComputerCraft.modemRange, 0, MODEM_MAX_RANGE ); + + serverSpec.comment( "peripheral.modem_high_altitude_range", + "The range of Wireless Modems at maximum altitude in clear weather, in meters" ); + serverSpec.defineInRange( "peripheral.modem_high_altitude_range", ComputerCraft.modemHighAltitudeRange, 0, MODEM_MAX_RANGE ); + + serverSpec.comment( "peripheral.modem_range_during_storm", + "The range of Wireless Modems at low altitude in stormy weather, in meters" ); + serverSpec.defineInRange( "peripheral.modem_range_during_storm", ComputerCraft.modemRangeDuringStorm, 0, MODEM_MAX_RANGE ); + + serverSpec.comment( "peripheral.modem_high_altitude_range_during_storm", + "The range of Wireless Modems at maximum altitude in stormy weather, in meters" ); + serverSpec.defineInRange( "peripheral.modem_high_altitude_range_during_storm", ComputerCraft.modemHighAltitudeRangeDuringStorm, 0, MODEM_MAX_RANGE ); + + serverSpec.comment( "peripheral.max_notes_per_tick", + "Maximum amount of notes a speaker can play at once" ); + serverSpec.defineInRange( "peripheral.max_notes_per_tick", ComputerCraft.maxNotesPerTick, 1, Integer.MAX_VALUE ); + + serverSpec.comment( "peripheral.monitor_bandwidth", + "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." ); + serverSpec.defineInRange( "peripheral.monitor_bandwidth", (int) ComputerCraft.monitorBandwidth, 0, Integer.MAX_VALUE ); + } + + { // Turtles + serverSpec.comment( "turtle", "Various options relating to turtles." ); + + serverSpec.comment( "turtle.need_fuel", + "Set whether Turtles require fuel to move" ); + serverSpec.define( "turtle.need_fuel", ComputerCraft.turtlesNeedFuel ); + + serverSpec.comment( "turtle.normal_fuel_limit", "The fuel limit for Turtles" ); + serverSpec.defineInRange( "turtle.normal_fuel_limit", ComputerCraft.turtleFuelLimit, 0, Integer.MAX_VALUE ); + + serverSpec.comment( "turtle.advanced_fuel_limit", + "The fuel limit for Advanced Turtles" ); + serverSpec.defineInRange( "turtle.advanced_fuel_limit", ComputerCraft.advancedTurtleFuelLimit, 0, Integer.MAX_VALUE ); + + serverSpec.comment( "turtle.obey_block_protection", + "If set to true, Turtles will be unable to build, dig, or enter protected areas (such as near the server spawn point)" ); + serverSpec.define( "turtle.obey_block_protection", ComputerCraft.turtlesObeyBlockProtection ); + + serverSpec.comment( "turtle.can_push", + "If set to true, Turtles will push entities out of the way instead of stopping if there is space to do so" ); + serverSpec.define( "turtle.can_push", ComputerCraft.turtlesCanPush ); + + serverSpec.comment( "turtle.disabled_actions", + "A list of turtle actions which are disabled." ); + serverSpec.defineList( "turtle.disabled_actions", Collections.emptyList(), x -> x instanceof String && getAction( (String) x ) != null ); + } + + { // Terminal sizes + serverSpec.comment( "term_sizes", "Configure the size of various computer's terminals.\n" + + "Larger terminals require more bandwidth, so use with care." ); + + serverSpec.comment( "term_sizes.computer", "Terminal size of computers" ); + serverSpec.defineInRange( "term_sizes.computer.width", ComputerCraft.computerTermWidth, 1, 255 ); + serverSpec.defineInRange( "term_sizes.computer.height", ComputerCraft.computerTermHeight, 1, 255 ); + + serverSpec.comment( "term_sizes.pocket_computer", "Terminal size of pocket computers" ); + serverSpec.defineInRange( "term_sizes.pocket_computer.width", ComputerCraft.pocketTermWidth, 1, 255 ); + serverSpec.defineInRange( "term_sizes.pocket_computer.height", ComputerCraft.pocketTermHeight, 1, 255 ); + + serverSpec.comment( "term_sizes.monitor", "Maximum size of monitors (in blocks)" ); + serverSpec.defineInRange( "term_sizes.monitor.width", ComputerCraft.monitorWidth, 1, 32 ); + serverSpec.defineInRange( "term_sizes.monitor.height", ComputerCraft.monitorHeight, 1, 32 ); + } + + clientSpec = new CommentedConfigSpec(); + + clientSpec.comment( "monitor_renderer", + "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." ); + clientSpec.defineRestrictedEnum( "monitor_renderer", MonitorRenderer.BEST, EnumSet.allOf( MonitorRenderer.class ), EnumGetMethod.NAME_IGNORECASE ); + + clientSpec.comment( "monitor_distance", + "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." ); + clientSpec.defineInRange( "monitor_distance", 64, 16, 1024 ); + } + + private static final FileNotFoundAction MAKE_DIRECTORIES_AND_FILE = ( file, configFormat ) -> { + Files.createDirectories( file.getParent() ); + Files.createFile( file ); + configFormat.initEmptyFile( file ); + return false; + }; + + private static CommentedFileConfig buildFileConfig( Path path ) + { + return CommentedFileConfig.builder( path ) + .onFileNotFound( MAKE_DIRECTORIES_AND_FILE ) + .preserveInsertionOrder() + .build(); + } + + private static void saveConfig( UnmodifiableConfig config, CommentedConfigSpec spec, Path path ) + { + try( CommentedFileConfig fileConfig = buildFileConfig( path ) ) + { + fileConfig.putAll( config ); + spec.correct( fileConfig ); + fileConfig.save(); + } + } + + public static void save() + { + if( clientConfig != null ) + { + saveConfig( clientConfig, clientSpec, clientPath ); + } + if( serverConfig != null && serverPath != null ) + { + saveConfig( serverConfig, serverSpec, serverPath ); + } + } + + public static void serverStarting( MinecraftServer server ) + { + serverPath = server.getSavePath( serverDir ).resolve( serverFileName ); + + try( CommentedFileConfig config = buildFileConfig( serverPath ) ) + { + config.load(); + serverSpec.correct( config, Config::correctionListener ); + config.save(); + serverConfig = config; + sync(); + } + } + + public static void serverStopping( MinecraftServer server ) + { + serverConfig = null; + serverPath = null; + } + + public static void clientStarted( MinecraftClient client ) + { + try( CommentedFileConfig config = buildFileConfig( clientPath ) ) + { + config.load(); + clientSpec.correct( config, Config::correctionListener ); + config.save(); + clientConfig = config; + sync(); + } + } + + private static void correctionListener( ConfigSpec.CorrectionAction action, List path, Object incorrectValue, Object correctedValue ) + { + String key = String.join( ".", path ); + switch( action ) + { + case ADD: + ComputerCraft.log.warn( "Config key {} missing -> added default value.", key ); + break; + case REMOVE: + ComputerCraft.log.warn( "Config key {} not defined -> removed from config.", key ); + break; + case REPLACE: + ComputerCraft.log.warn( "Config key {} not valid -> replaced with default value.", key ); + } + } + + public static void sync() + { + if( serverConfig != null ) + { + // General + ComputerCraft.computerSpaceLimit = serverConfig.get( "computer_space_limit" ); + ComputerCraft.floppySpaceLimit = serverConfig.get( "floppy_space_limit" ); + ComputerCraft.maximumFilesOpen = serverConfig.get( "maximum_open_files" ); + ComputerCraft.disableLua51Features = serverConfig.get( "disable_lua51_features" ); + ComputerCraft.defaultComputerSettings = serverConfig.get( "default_computer_settings" ); + ComputerCraft.debugEnable = serverConfig.get( "debug_enabled" ); + ComputerCraft.logComputerErrors = serverConfig.get( "log_computer_errors" ); + ComputerCraft.commandRequireCreative = serverConfig.get( "command_require_creative" ); + + // Execution + ComputerCraft.computerThreads = serverConfig.get( "execution.computer_threads" ); + ComputerCraft.maxMainGlobalTime = TimeUnit.MILLISECONDS.toNanos( serverConfig.get( "execution.max_main_global_time" ) ); + ComputerCraft.maxMainComputerTime = TimeUnit.MILLISECONDS.toNanos( serverConfig.get( "execution.max_main_computer_time" ) ); + + // HTTP + ComputerCraft.httpEnabled = serverConfig.get( "http.enabled" ); + ComputerCraft.httpWebsocketEnabled = serverConfig.get( "http.websocket_enabled" ); + ComputerCraft.httpRules = serverConfig.>get( "http.rules" ).stream().map( AddressRuleConfig::parseRule ) + .filter( Objects::nonNull ).collect( Collectors.toList() ); + ComputerCraft.httpMaxRequests = serverConfig.get( "http.max_requests" ); + ComputerCraft.httpMaxWebsockets = serverConfig.get( "http.max_websockets" ); + + // Peripherals + ComputerCraft.enableCommandBlock = serverConfig.get( "peripheral.command_block_enabled" ); + ComputerCraft.modemRange = serverConfig.get( "peripheral.modem_range" ); + ComputerCraft.modemHighAltitudeRange = serverConfig.get( "peripheral.modem_high_altitude_range" ); + ComputerCraft.modemRangeDuringStorm = serverConfig.get( "peripheral.modem_range_during_storm" ); + ComputerCraft.modemHighAltitudeRangeDuringStorm = serverConfig.get( "peripheral.modem_high_altitude_range_during_storm" ); + ComputerCraft.maxNotesPerTick = serverConfig.get( "peripheral.max_notes_per_tick" ); + ComputerCraft.monitorBandwidth = serverConfig.get( "peripheral.monitor_bandwidth" ); + + // Turtles + ComputerCraft.turtlesNeedFuel = serverConfig.get( "turtle.need_fuel" ); + ComputerCraft.turtleFuelLimit = serverConfig.get( "turtle.normal_fuel_limit" ); + ComputerCraft.advancedTurtleFuelLimit = serverConfig.get( "turtle.advanced_fuel_limit" ); + ComputerCraft.turtlesObeyBlockProtection = serverConfig.get( "turtle.obey_block_protection" ); + ComputerCraft.turtlesCanPush = serverConfig.get( "turtle.can_push" ); + + ComputerCraft.turtleDisabledActions.clear(); + for( String value : serverConfig.>get( "turtle.disabled_actions" ) ) + { + ComputerCraft.turtleDisabledActions.add( getAction( value ) ); + } + + // Terminal Size + ComputerCraft.computerTermWidth = serverConfig.get( "term_sizes.computer.width" ); + ComputerCraft.computerTermHeight = serverConfig.get( "term_sizes.computer.height" ); + ComputerCraft.pocketTermWidth = serverConfig.get( "term_sizes.pocket_computer.width" ); + ComputerCraft.pocketTermHeight = serverConfig.get( "term_sizes.pocket_computer.height" ); + ComputerCraft.monitorWidth = serverConfig.get( "term_sizes.monitor.width" ); + ComputerCraft.monitorHeight = serverConfig.get( "term_sizes.monitor.height" ); + } + + // Client + if( clientConfig != null ) + { + ComputerCraft.monitorRenderer = clientConfig.getEnum( "monitor_renderer", MonitorRenderer.class ); + int distance = clientConfig.get( "monitor_distance" ); + ComputerCraft.monitorDistanceSq = distance * distance; + } + } + + 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/remappedSrc/dan200/computercraft/shared/util/DefaultInventory.java b/remappedSrc/dan200/computercraft/shared/util/DefaultInventory.java new file mode 100644 index 000000000..857fafd34 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/DefaultInventory.java @@ -0,0 +1,38 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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/remappedSrc/dan200/computercraft/shared/util/DefaultSidedInventory.java b/remappedSrc/dan200/computercraft/shared/util/DefaultSidedInventory.java new file mode 100644 index 000000000..706a0047b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/DefaultSidedInventory.java @@ -0,0 +1,29 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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/remappedSrc/dan200/computercraft/shared/util/DirectionUtil.java b/remappedSrc/dan200/computercraft/shared/util/DirectionUtil.java new file mode 100644 index 000000000..9f6cb01a9 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/DirectionUtil.java @@ -0,0 +1,60 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import dan200.computercraft.core.computer.ComputerSide; +import net.minecraft.util.math.Direction; + +public final class DirectionUtil +{ + public static final Direction[] FACINGS = Direction.values(); + + private DirectionUtil() {} + + 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/remappedSrc/dan200/computercraft/shared/util/DropConsumer.java b/remappedSrc/dan200/computercraft/shared/util/DropConsumer.java new file mode 100644 index 000000000..43bb89d84 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/DropConsumer.java @@ -0,0 +1,105 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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 java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public final class DropConsumer +{ + private static Function dropConsumer; + private static List remainingDrops; + private static WeakReference dropWorld; + private static BlockPos dropPos; + private static Box dropBounds; + private static WeakReference dropEntity; + + private DropConsumer() + { + } + + public static void set( Entity entity, Function consumer ) + { + dropConsumer = consumer; + remainingDrops = new ArrayList<>(); + dropEntity = new WeakReference<>( entity ); + dropWorld = new WeakReference<>( entity.world ); + dropPos = null; + dropBounds = new Box( entity.getBlockPos() ).expand( 2, 2, 2 ); + } + + public static void set( World world, BlockPos pos, Function consumer ) + { + dropConsumer = consumer; + remainingDrops = new ArrayList<>( 2 ); + dropEntity = null; + dropWorld = new WeakReference<>( 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; + } + + public static boolean onHarvestDrops( World world, BlockPos pos, ItemStack stack ) + { + if( dropWorld != null && dropWorld.get() == world && dropPos != null && dropPos.equals( pos ) ) + { + handleDrops( stack ); + return true; + } + return false; + } + + private static void handleDrops( ItemStack stack ) + { + ItemStack remaining = dropConsumer.apply( stack ); + if( !remaining.isEmpty() ) + { + remainingDrops.add( remaining ); + } + } + + public static boolean onEntitySpawn( Entity entity ) + { + // Capture any nearby item spawns + if( dropWorld != null && dropWorld.get() == entity.getEntityWorld() && entity instanceof ItemEntity && dropBounds.contains( entity.getPos() ) ) + { + handleDrops( ((ItemEntity) entity).getStack() ); + return true; + } + return false; + } + + public static boolean onLivingDrops( Entity entity, ItemStack stack ) + { + if( dropEntity != null && entity == dropEntity.get() ) + { + handleDrops( stack ); + return true; + } + return false; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/FakeNetHandler.java b/remappedSrc/dan200/computercraft/shared/util/FakeNetHandler.java new file mode 100644 index 000000000..ff41f1f0c --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/FakeNetHandler.java @@ -0,0 +1,376 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import dan200.computercraft.api.turtle.FakePlayer; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.NetworkSide; +import net.minecraft.network.NetworkState; +import net.minecraft.network.Packet; +import net.minecraft.network.listener.PacketListener; +import net.minecraft.network.packet.c2s.play.*; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.text.Text; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.crypto.Cipher; + +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 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 onJigsawGenerating( JigsawGeneratingC2SPacket packet ) + { + } + + @Override + public void onMerchantTradeSelect( SelectMerchantTradeC2SPacket packet ) + { + } + + @Override + public void onBookUpdate( @Nonnull BookUpdateC2SPacket packet ) + { + } + + @Override + public void onRecipeBookData( RecipeBookDataC2SPacket packet ) + { + } + + @Override + public void onRecipeCategoryOptions( RecipeCategoryOptionsC2SPacket packet ) + { + super.onRecipeCategoryOptions( 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 onDisconnected( @Nonnull Text reason ) + { + } + + @Override + public void sendPacket( @Nonnull Packet packet ) + { + } + + @Override + public void sendPacket( @Nonnull Packet packet, @Nullable GenericFutureListener> whenSent ) + { + } + + @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 onCloseHandledScreen( CloseHandledScreenC2SPacket packet ) + { + } + + @Override + public void onClickSlot( ClickSlotC2SPacket 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 onConfirmScreenAction( ConfirmScreenActionC2SPacket 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 ) + { + closeReason = message; + } + + @Override + public void setupEncryption( Cipher cipher, Cipher cipher2 ) + { + } + + @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/remappedSrc/dan200/computercraft/shared/util/FixedPointTileEntityType.java b/remappedSrc/dan200/computercraft/shared/util/FixedPointTileEntityType.java new file mode 100644 index 000000000..2890f3540 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/FixedPointTileEntityType.java @@ -0,0 +1,61 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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 BlockEntityType} 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/remappedSrc/dan200/computercraft/shared/util/Holiday.java b/remappedSrc/dan200/computercraft/shared/util/Holiday.java new file mode 100644 index 000000000..2b1dd385b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/Holiday.java @@ -0,0 +1,12 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +public enum Holiday +{ + NONE, VALENTINES, APRIL_FOOLS_DAY, HALLOWEEN, CHRISTMAS, +} diff --git a/remappedSrc/dan200/computercraft/shared/util/HolidayUtil.java b/remappedSrc/dan200/computercraft/shared/util/HolidayUtil.java new file mode 100644 index 000000000..e24ce3610 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/HolidayUtil.java @@ -0,0 +1,42 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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/remappedSrc/dan200/computercraft/shared/util/IDAssigner.java b/remappedSrc/dan200/computercraft/shared/util/IDAssigner.java new file mode 100644 index 000000000..6a10e44fa --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/IDAssigner.java @@ -0,0 +1,122 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.fabric.mixin.WorldSavePathAccess; +import me.shedaniel.cloth.api.utils.v1.GameInstanceUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.WorldSavePath; + +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 = WorldSavePathAccess.createWorldSavePath( ComputerCraft.MOD_ID ); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting() + .create(); + private static final Type ID_TOKEN = new TypeToken>() + { + }.getType(); + private static Map ids; + private static WeakReference server; + private static Path idFile; + + private IDAssigner() + { + } + + public static synchronized int getNextId( String kind ) + { + MinecraftServer currentServer = getCachedServer(); + if( currentServer == null ) + { + // The server has changed, refetch our ID map + if( GameInstanceUtils.getServer() != null ) + { + server = new WeakReference<>( GameInstanceUtils.getServer() ); + + 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; + } + + private static MinecraftServer getCachedServer() + { + if( server == null ) + { + return null; + } + + MinecraftServer currentServer = server.get(); + if( currentServer == null ) + { + return null; + } + + if( currentServer != GameInstanceUtils.getServer() ) + { + return null; + } + return currentServer; + } + + public static File getDir() + { + return GameInstanceUtils.getServer() + .getSavePath( FOLDER ) + .toFile(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/ImpostorRecipe.java b/remappedSrc/dan200/computercraft/shared/util/ImpostorRecipe.java new file mode 100644 index 000000000..3422ba17e --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/ImpostorRecipe.java @@ -0,0 +1,99 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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 javax.annotation.Nonnull; + +public final class ImpostorRecipe extends ShapedRecipe +{ + public static final RecipeSerializer SERIALIZER = new RecipeSerializer() + { + @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 = ShapedRecipe.getItem( JsonHelper.getObject( json, "result" ) ); + return new ImpostorRecipe( identifier, group, recipe.getWidth(), recipe.getHeight(), recipe.getIngredients(), 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.getWidth() ); + buf.writeVarInt( recipe.getHeight() ); + buf.writeString( recipe.getGroup() ); + for( Ingredient ingredient : recipe.getIngredients() ) + { + ingredient.write( buf ); + } + buf.writeItemStack( recipe.getOutput() ); + } + }; + 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 RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + @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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/ImpostorShapelessRecipe.java b/remappedSrc/dan200/computercraft/shared/util/ImpostorShapelessRecipe.java new file mode 100644 index 000000000..a816b3f39 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/ImpostorShapelessRecipe.java @@ -0,0 +1,128 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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.ShapedRecipe; +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 javax.annotation.Nonnull; + +public final class ImpostorShapelessRecipe extends ShapelessRecipe +{ + public static final RecipeSerializer SERIALIZER = new RecipeSerializer() + { + @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 = ShapedRecipe.getItem( JsonHelper.getObject( json, "result" ) ); + 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.getIngredients() + .size() ); + + for( Ingredient ingredient : recipe.getIngredients() ) + { + ingredient.write( buffer ); + } + buffer.writeItemStack( recipe.getOutput() ); + } + }; + 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 RecipeSerializer getSerializer() + { + return SERIALIZER; + } + + @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; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/InventoryDelegate.java b/remappedSrc/dan200/computercraft/shared/util/InventoryDelegate.java new file mode 100644 index 000000000..41af4a582 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/InventoryDelegate.java @@ -0,0 +1,120 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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 +{ + @Override + default int size() + { + return getInventory().size(); + } + + Inventory getInventory(); + + @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 int count( @Nonnull Item stack ) + { + return getInventory().count( stack ); + } + + @Override + default boolean containsAny( @Nonnull Set set ) + { + return getInventory().containsAny( set ); + } + + @Override + default void clear() + { + getInventory().clear(); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/InventoryUtil.java b/remappedSrc/dan200/computercraft/shared/util/InventoryUtil.java new file mode 100644 index 000000000..8f8ec71eb --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/InventoryUtil.java @@ -0,0 +1,192 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.ChestBlock; +import net.minecraft.block.InventoryProvider; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.ChestBlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import org.apache.commons.lang3.tuple.Pair; + +import javax.annotation.Nonnull; + +public final class InventoryUtil +{ + private InventoryUtil() {} + // Methods for comparing things: + + public static boolean areItemsStackable( @Nonnull ItemStack a, @Nonnull ItemStack b ) + { + return a == b || (a.getItem() == b.getItem() && ItemStack.areNbtEqual( a, b )); + } + + // Methods for finding inventories: + + public static Inventory getInventory( World world, BlockPos pos, Direction side ) + { + // Look for tile with inventory + int y = pos.getY(); + if( y >= 0 && y < world.getHeight() ) + { + // Check if block is InventoryProvider + BlockState blockState = world.getBlockState( pos ); + Block block = blockState.getBlock(); + if( block instanceof InventoryProvider ) + { + return ((InventoryProvider) block).getInventory( blockState, world, pos ); + } + // Check if block is BlockEntity w/ Inventory + if( block.hasBlockEntity() ) + { + BlockEntity tileEntity = world.getBlockEntity( pos ); + + Inventory inventory = getInventory( tileEntity ); + if( inventory != null ) + { + return inventory; + } + } + } + + // 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 (Inventory) entity; + } + } + return null; + } + + public static Inventory getInventory( BlockEntity tileEntity ) + { + World world = tileEntity.getWorld(); + BlockPos pos = tileEntity.getPos(); + BlockState blockState = world.getBlockState( pos ); + Block block = blockState.getBlock(); + + if( tileEntity instanceof Inventory ) + { + Inventory inventory = (Inventory) tileEntity; + if( inventory instanceof ChestBlockEntity && block instanceof ChestBlock ) + { + return ChestBlock.getInventory( (ChestBlock) block, blockState, world, pos, true ); + } + return inventory; + } + + return null; + } + + @Nonnull + public static ItemStack storeItems( @Nonnull ItemStack itemstack, ItemStorage inventory, int begin ) + { + return storeItems( itemstack, inventory, 0, inventory.size(), begin ); + } + + // Methods for placing into inventories: + + @Nonnull + public static ItemStack storeItems( @Nonnull ItemStack stack, ItemStorage 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.store( slot, remainder, false ); + } + return areItemsEqual( stack, remainder ) ? stack : remainder; + } + + public static boolean areItemsEqual( @Nonnull ItemStack a, @Nonnull ItemStack b ) + { + return a == b || ItemStack.areEqual( a, b ); + } + + @Nonnull + public static ItemStack storeItems( @Nonnull ItemStack itemstack, ItemStorage inventory ) + { + return storeItems( itemstack, inventory, 0, inventory.size(), 0 ); + } + + // Methods for taking out of inventories + + @Nonnull + public static ItemStack takeItems( int count, ItemStorage inventory, int begin ) + { + return takeItems( count, inventory, 0, inventory.size(), begin ); + } + + @Nonnull + public static ItemStack takeItems( int count, ItemStorage inventory, int start, int range, int begin ) + { + ItemStack partialStack = ItemStack.EMPTY; + for( int i = 0; i < range; i++ ) + { + int slot = start + (i + begin - start) % range; + + if( count <= 0 ) + { + break; + } + + // If this doesn't slot, abort. + ItemStack extracted = inventory.take( slot, count, partialStack, false ); + if( extracted.isEmpty() ) + { + continue; + } + + count -= extracted.getCount(); + 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() ); + } + } + + return partialStack; + } + + @Nonnull + public static ItemStack takeItems( int count, ItemStorage inventory ) + { + return takeItems( count, inventory, 0, inventory.size(), 0 ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/IoUtil.java b/remappedSrc/dan200/computercraft/shared/util/IoUtil.java new file mode 100644 index 000000000..eb915596b --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/IoUtil.java @@ -0,0 +1,30 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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/remappedSrc/dan200/computercraft/shared/util/ItemStorage.java b/remappedSrc/dan200/computercraft/shared/util/ItemStorage.java new file mode 100644 index 000000000..2868ad395 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/ItemStorage.java @@ -0,0 +1,303 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.util; + +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SidedInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.Direction; + +import javax.annotation.Nonnull; + +/** + * The most cutesy alternative of {@code IItemHandler} the world has ever seen. + */ +public interface ItemStorage +{ + static ItemStorage wrap( Inventory inventory ) + { + return new InventoryWrapper( inventory ); + } + + static ItemStorage wrap( @Nonnull SidedInventory inventory, @Nonnull Direction facing ) + { + return new SidedInventoryWrapper( inventory, facing ); + } + + static ItemStorage wrap( @Nonnull Inventory inventory, @Nonnull Direction facing ) + { + return inventory instanceof SidedInventory ? new SidedInventoryWrapper( (SidedInventory) inventory, facing ) : new InventoryWrapper( inventory ); + } + + static boolean areStackable( @Nonnull ItemStack a, @Nonnull ItemStack b ) + { + return a == b || (a.getItem() == b.getItem() && ItemStack.areNbtEqual( a, b )); + } + + int size(); + + @Nonnull + ItemStack getStack( int slot ); + + @Nonnull + ItemStack take( int slot, int limit, @Nonnull ItemStack filter, boolean simulate ); + + @Nonnull + ItemStack store( int slot, @Nonnull ItemStack stack, boolean simulate ); + + default ItemStorage view( int start, int size ) + { + return new View( this, start, size ); + } + + class InventoryWrapper implements ItemStorage + { + private final Inventory inventory; + + InventoryWrapper( Inventory inventory ) + { + this.inventory = inventory; + } + + @Override + public int size() + { + return inventory.size(); + } + + @Override + @Nonnull + public ItemStack getStack( int slot ) + { + return inventory.getStack( slot ); + } + + @Override + @Nonnull + public ItemStack take( int slot, int limit, @Nonnull ItemStack filter, boolean simulate ) + { + ItemStack existing = inventory.getStack( slot ); + if( existing.isEmpty() || !canExtract( slot, existing ) || (!filter.isEmpty() && !areStackable( existing, filter )) ) + { + return ItemStack.EMPTY; + } + + if( simulate ) + { + existing = existing.copy(); + if( existing.getCount() > limit ) + { + existing.setCount( limit ); + } + return existing; + } + else if( existing.getCount() < limit ) + { + setAndDirty( slot, ItemStack.EMPTY ); + return existing; + } + else + { + ItemStack result = existing.split( limit ); + setAndDirty( slot, existing ); + return result; + } + } + + protected boolean canExtract( int slot, ItemStack stack ) + { + return true; + } + + private void setAndDirty( int slot, @Nonnull ItemStack stack ) + { + inventory.setStack( slot, stack ); + inventory.markDirty(); + } + + @Override + @Nonnull + public ItemStack store( int slot, @Nonnull ItemStack stack, boolean simulate ) + { + if( stack.isEmpty() || !inventory.isValid( slot, stack ) ) + { + return stack; + } + + ItemStack existing = inventory.getStack( slot ); + if( existing.isEmpty() ) + { + int limit = Math.min( stack.getMaxCount(), inventory.getMaxCountPerStack() ); + if( limit <= 0 ) + { + return stack; + } + + if( stack.getCount() < limit ) + { + if( !simulate ) + { + setAndDirty( slot, stack ); + } + return ItemStack.EMPTY; + } + else + { + stack = stack.copy(); + ItemStack insert = stack.split( limit ); + if( !simulate ) + { + setAndDirty( slot, insert ); + } + return stack; + } + } + else if( areStackable( stack, existing ) ) + { + int limit = Math.min( existing.getMaxCount(), inventory.getMaxCountPerStack() ) - existing.getCount(); + if( limit <= 0 ) + { + return stack; + } + + if( stack.getCount() < limit ) + { + if( !simulate ) + { + existing.increment( stack.getCount() ); + setAndDirty( slot, existing ); + } + return ItemStack.EMPTY; + } + else + { + stack = stack.copy(); + stack.decrement( limit ); + if( !simulate ) + { + existing.increment( limit ); + setAndDirty( slot, existing ); + } + return stack; + } + } + else + { + return stack; + } + } + } + + class SidedInventoryWrapper extends InventoryWrapper + { + private final SidedInventory inventory; + private final Direction facing; + + SidedInventoryWrapper( SidedInventory inventory, Direction facing ) + { + super( inventory ); + this.inventory = inventory; + this.facing = facing; + } + + @Override + protected boolean canExtract( int slot, ItemStack stack ) + { + return super.canExtract( slot, stack ) && inventory.canExtract( slot, stack, facing ); + } + + @Override + public int size() + { + return inventory.getAvailableSlots( facing ).length; + } + + @Nonnull + @Override + public ItemStack take( int slot, int limit, @Nonnull ItemStack filter, boolean simulate ) + { + int[] slots = inventory.getAvailableSlots( facing ); + return slot >= 0 && slot < slots.length ? super.take( slots[slot], limit, filter, simulate ) : ItemStack.EMPTY; + } + + @Nonnull + @Override + public ItemStack store( int slot, @Nonnull ItemStack stack, boolean simulate ) + { + int[] slots = inventory.getAvailableSlots( facing ); + if( slot < 0 || slot >= slots.length ) + { + return stack; + } + + int mappedSlot = slots[slot]; + if( !inventory.canInsert( slot, stack, facing ) ) + { + return stack; + } + return super.store( mappedSlot, stack, simulate ); + } + } + + class View implements ItemStorage + { + private final ItemStorage parent; + private final int start; + private final int size; + + View( ItemStorage parent, int start, int size ) + { + this.parent = parent; + this.start = start; + this.size = size; + } + + @Override + public int size() + { + return size; + } + + @Override + @Nonnull + public ItemStack getStack( int slot ) + { + if( slot < start || slot >= start + size ) + { + return ItemStack.EMPTY; + } + return parent.getStack( slot - start ); + } + + @Nonnull + @Override + public ItemStack take( int slot, int limit, @Nonnull ItemStack filter, boolean simulate ) + { + if( slot < start || slot >= start + size ) + { + return ItemStack.EMPTY; + } + return parent.take( slot - start, limit, filter, simulate ); + } + + @Nonnull + @Override + public ItemStack store( int slot, @Nonnull ItemStack stack, boolean simulate ) + { + if( slot < start || slot >= start + size ) + { + return stack; + } + return parent.store( slot - start, stack, simulate ); + } + + @Override + public ItemStorage view( int start, int size ) + { + return new View( parent, this.start + start, size ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/NBTUtil.java b/remappedSrc/dan200/computercraft/shared/util/NBTUtil.java new file mode 100644 index 000000000..38d4fb494 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/NBTUtil.java @@ -0,0 +1,268 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import dan200.computercraft.ComputerCraft; +import net.minecraft.nbt.*; +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; + +public final class NBTUtil +{ + public static final int TAG_END = 0; + public static final int TAG_BYTE = 1; + public static final int TAG_SHORT = 2; + public static final int TAG_INT = 3; + public static final int TAG_LONG = 4; + public static final int TAG_FLOAT = 5; + public static final int TAG_DOUBLE = 6; + public static final int TAG_BYTE_ARRAY = 7; + public static final int TAG_STRING = 8; + public static final int TAG_LIST = 9; + public static final int TAG_COMPOUND = 10; + public static final int TAG_INT_ARRAY = 11; + public static final int TAG_LONG_ARRAY = 12; + public static final int TAG_ANY_NUMERIC = 99; + + private NBTUtil() {} + + private static NbtElement toNBTTag( Object object ) + { + if( object == null ) + { + return null; + } + if( object instanceof Boolean ) + { + return NbtByte.of( (byte) ((boolean) (Boolean) object ? 1 : 0) ); + } + if( object instanceof Number ) + { + return NbtDouble.of( ((Number) object).doubleValue() ); + } + if( object instanceof String ) + { + return NbtString.of( object.toString() ); + } + if( object instanceof Map ) + { + Map m = (Map) object; + NbtCompound nbt = new NbtCompound(); + int i = 0; + for( Map.Entry entry : m.entrySet() ) + { + NbtElement key = toNBTTag( entry.getKey() ); + NbtElement 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 NbtCompound encodeObjects( Object[] objects ) + { + if( objects == null || objects.length <= 0 ) + { + return null; + } + + NbtCompound nbt = new NbtCompound(); + nbt.putInt( "len", objects.length ); + for( int i = 0; i < objects.length; i++ ) + { + NbtElement child = toNBTTag( objects[i] ); + if( child != null ) + { + nbt.put( Integer.toString( i ), child ); + } + } + return nbt; + } + + private static Object fromNBTTag( NbtElement tag ) + { + if( tag == null ) + { + return null; + } + switch( tag.getType() ) + { + case TAG_BYTE: + return ((NbtByte) tag).byteValue() > 0; + case TAG_DOUBLE: + return ((NbtDouble) tag).doubleValue(); + default: + case TAG_STRING: + return tag.asString(); + case TAG_COMPOUND: + NbtCompound c = (NbtCompound) 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( NbtElement tag ) + { + if( tag == null ) + { + return null; + } + + byte typeID = tag.getType(); + switch( typeID ) + { + case TAG_BYTE: + case TAG_SHORT: + case TAG_INT: + case TAG_LONG: + return ((AbstractNbtNumber) tag).longValue(); + case TAG_FLOAT: + case TAG_DOUBLE: + return ((AbstractNbtNumber) tag).doubleValue(); + case TAG_STRING: // String + return tag.asString(); + case TAG_COMPOUND: // Compound + { + NbtCompound compound = (NbtCompound) 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 TAG_LIST: + { + NbtList list = (NbtList) 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 TAG_BYTE_ARRAY: + { + byte[] array = ((NbtByteArray) 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 TAG_INT_ARRAY: + int[] array = ((NbtIntArray) 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( NbtCompound 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 NbtCompound 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( int b ) + { + digest.update( (byte) b ); + } + + @Override + public void write( @Nonnull byte[] b, int off, int len ) + { + digest.update( b, off, len ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/Palette.java b/remappedSrc/dan200/computercraft/shared/util/Palette.java new file mode 100644 index 000000000..2b4d65b52 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/Palette.java @@ -0,0 +1,135 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.PacketByteBuf; + +public class Palette +{ + public static final Palette DEFAULT = new Palette(); + private static final int PALETTE_SIZE = 16; + private final double[][] colours = new double[PALETTE_SIZE][3]; + + public Palette() + { + // Get the default palette + resetColours(); + } + + public void resetColours() + { + for( int i = 0; i < Colour.VALUES.length; i++ ) + { + resetColour( i ); + } + } + + public void resetColour( int i ) + { + if( i >= 0 && i < colours.length ) + { + setColour( i, Colour.VALUES[i] ); + } + } + + public void setColour( int i, Colour colour ) + { + setColour( i, colour.getR(), colour.getG(), colour.getB() ); + } + + 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 double[] getColour( int i ) + { + if( i >= 0 && i < colours.length ) + { + return colours[i]; + } + return null; + } + + 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 NbtCompound writeToNBT( NbtCompound 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 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 void readFromNBT( NbtCompound 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] ); + } + } + + public static double[] decodeRGB8( int rgb ) + { + return new double[] { + ((rgb >> 16) & 0xFF) / 255.0f, + ((rgb >> 8) & 0xFF) / 255.0f, + (rgb & 0xFF) / 255.0f, + }; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/RecipeUtil.java b/remappedSrc/dan200/computercraft/shared/util/RecipeUtil.java new file mode 100644 index 000000000..8093458c2 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/RecipeUtil.java @@ -0,0 +1,127 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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 net.minecraft.recipe.Ingredient; +import net.minecraft.util.JsonHelper; +import net.minecraft.util.collection.DefaultedList; + +import java.util.Map; +import java.util.Set; + +// TODO: Replace some things with Forge?? + +public final class RecipeUtil +{ + private RecipeUtil() {} + + 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 ); + } + + 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; + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/RecordUtil.java b/remappedSrc/dan200/computercraft/shared/util/RecordUtil.java new file mode 100644 index 000000000..30de0b841 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/RecordUtil.java @@ -0,0 +1,26 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import dan200.computercraft.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/remappedSrc/dan200/computercraft/shared/util/RedstoneUtil.java b/remappedSrc/dan200/computercraft/shared/util/RedstoneUtil.java new file mode 100644 index 000000000..ca27dcde5 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/RedstoneUtil.java @@ -0,0 +1,24 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import net.minecraft.block.BlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +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 ); + BlockPos neighbourPos = pos.offset( side ); + world.updateNeighbor( neighbourPos, block.getBlock(), pos ); + world.updateNeighborsExcept( neighbourPos, block.getBlock(), side.getOpposite() ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/SingleIntArray.java b/remappedSrc/dan200/computercraft/shared/util/SingleIntArray.java new file mode 100644 index 000000000..4e9141232 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/SingleIntArray.java @@ -0,0 +1,32 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import net.minecraft.screen.PropertyDelegate; + +@FunctionalInterface +public interface SingleIntArray extends PropertyDelegate +{ + @Override + default int get( int property ) + { + return property == 0 ? get() : 0; + } + + int get(); + + @Override + default void set( int property, int value ) + { + } + + @Override + default int size() + { + return 1; + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/StringUtil.java b/remappedSrc/dan200/computercraft/shared/util/StringUtil.java new file mode 100644 index 000000000..2c8dd68b8 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/StringUtil.java @@ -0,0 +1,44 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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/remappedSrc/dan200/computercraft/shared/util/ThreadUtils.java b/remappedSrc/dan200/computercraft/shared/util/ThreadUtils.java new file mode 100644 index 000000000..24f487d4f --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/ThreadUtils.java @@ -0,0 +1,80 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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; + } + + /** + * 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(); + } + + /** + * 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 ) ); + } + + /** + * 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 ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/TickScheduler.java b/remappedSrc/dan200/computercraft/shared/util/TickScheduler.java new file mode 100644 index 000000000..98e3a8036 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/TickScheduler.java @@ -0,0 +1,61 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import com.google.common.collect.MapMaker; +import dan200.computercraft.shared.common.TileGeneric; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; + +/** + * We use this when modems and other peripherals change a block in a different thread. + */ +public final class TickScheduler +{ + private static final Set toTick = Collections.newSetFromMap( new MapMaker().weakKeys() + .makeMap() ); + + private TickScheduler() + { + } + + public static void schedule( TileGeneric tile ) + { + World world = tile.getWorld(); + if( world != null && !world.isClient ) + { + toTick.add( tile ); + } + } + + public static void tick() + { + 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.isChunkLoaded( pos ) && world.getBlockEntity( pos ) == tile ) + { + world.getBlockTickScheduler() + .schedule( pos, + tile.getCachedState() + .getBlock(), + 0 ); + } + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/util/ValidatingSlot.java b/remappedSrc/dan200/computercraft/shared/util/ValidatingSlot.java new file mode 100644 index 000000000..aa1e50940 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/ValidatingSlot.java @@ -0,0 +1,27 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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/remappedSrc/dan200/computercraft/shared/util/WaterloggableHelpers.java b/remappedSrc/dan200/computercraft/shared/util/WaterloggableHelpers.java new file mode 100644 index 000000000..7703ab8ef --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/WaterloggableHelpers.java @@ -0,0 +1,64 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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/remappedSrc/dan200/computercraft/shared/util/WorldUtil.java b/remappedSrc/dan200/computercraft/shared/util/WorldUtil.java new file mode 100644 index 000000000..bb896d146 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/util/WorldUtil.java @@ -0,0 +1,213 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.util; + +import 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 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(); + + public static boolean isLiquidBlock( World world, BlockPos pos ) + { + if( !World.isInBuildLimit( 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.setPosition( 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; + } + + 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 Vec3d getRayEnd( PlayerEntity player ) + { + double reach = 5; + Vec3d look = player.getRotationVector(); + return getRayStart( player ).add( look.x * reach, look.y * reach, look.z * reach ); + } + + public static Vec3d getRayStart( LivingEntity entity ) + { + return entity.getCameraPosVec( 1 ); + } + + 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, 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 ); + } + + public static void dropItemStack( @Nonnull ItemStack stack, World world, Vec3d pos ) + { + dropItemStack( stack, world, pos, 0.0, 0.0, 0.0 ); + } +} diff --git a/remappedSrc/dan200/computercraft/shared/wired/InvariantChecker.java b/remappedSrc/dan200/computercraft/shared/wired/InvariantChecker.java new file mode 100644 index 000000000..ab790e922 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/wired/InvariantChecker.java @@ -0,0 +1,62 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 checkNetwork( WiredNetwork network ) + { + if( !ENABLED ) + { + return; + } + + for( WiredNode node : network.nodes ) + { + checkNode( node ); + } + } + + 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() ); + } + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/wired/WiredNetwork.java b/remappedSrc/dan200/computercraft/shared/wired/WiredNetwork.java new file mode 100644 index 000000000..ccbf63218 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/wired/WiredNetwork.java @@ -0,0 +1,567 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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; + } + + 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 ); + } + } + + @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(); + } + } + + 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 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; + } + + private static WiredNode checkNode( IWiredNode node ) + { + if( node instanceof WiredNode ) + { + return (WiredNode) node; + } + else + { + throw new IllegalArgumentException( "Unknown implementation of IWiredNode: " + node ); + } + } + + 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 ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/wired/WiredNetworkChange.java b/remappedSrc/dan200/computercraft/shared/wired/WiredNetworkChange.java new file mode 100644 index 000000000..fda849f10 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/wired/WiredNetworkChange.java @@ -0,0 +1,126 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 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 ); + } + + public static WiredNetworkChange changed( Map removed, Map added ) + { + return new WiredNetworkChange( Collections.unmodifiableMap( removed ), Collections.unmodifiableMap( added ) ); + } + + @Nonnull + @Override + public Map peripheralsRemoved() + { + return removed; + } + + @Nonnull + @Override + public Map peripheralsAdded() + { + return added; + } + + void broadcast( Iterable nodes ) + { + if( !isEmpty() ) + { + for( WiredNode node : nodes ) + { + node.element.networkChanged( this ); + } + } + } + + public boolean isEmpty() + { + return added.isEmpty() && removed.isEmpty(); + } + + void broadcast( WiredNode node ) + { + if( !isEmpty() ) + { + node.element.networkChanged( this ); + } + } +} diff --git a/remappedSrc/dan200/computercraft/shared/wired/WiredNode.java b/remappedSrc/dan200/computercraft/shared/wired/WiredNode.java new file mode 100644 index 000000000..03cd63820 --- /dev/null +++ b/remappedSrc/dan200/computercraft/shared/wired/WiredNode.java @@ -0,0 +1,168 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.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 +{ + final IWiredElement element; + final HashSet neighbours = new HashSet<>(); + Map peripherals = Collections.emptyMap(); + volatile WiredNetwork network; + private Set receivers; + + 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 ); + } + } + + @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(); + } + } + + private void acquireReadLock() + { + WiredNetwork currentNetwork = network; + while( true ) + { + Lock lock = currentNetwork.lock.readLock(); + lock.lock(); + if( currentNetwork == network ) + { + return; + } + + + lock.unlock(); + } + } + + 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 ); + } + } + } + } + + @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() + ")}"; + } +}